STI, um controlador

Sou novato nos rails e estou meio preso a esse problema de design, que pode ser fácil de resolver, mas não chego a lugar nenhum: tenho dois tipos diferentes de anúncios: destaques e barganhas. Ambos possuem os mesmos atributos: título, descrição e uma imagem (com clipe de papel). Eles também têm o mesmo tipo de ação para aplicar: indexar, editar, criar, atualizar e destruir.

Eu configurei um STI como este:

Modelo de anúncio: ad.rb

class Ad < ActiveRecord::Base end 

Modelo de barganha: bargain.rb

 class Bargain < Ad end 

Modelo de destaque: destaque.rb

 class Highlight < Ad end 

O problema é que eu gostaria de ter apenas um controlador ( AdsController ) que executa as ações que eu disse em pechinchas ou destaques dependendo do URL, digamos www.foo.com/bargains [/ …] ou www.foo. com / destaques [/ …].

Por exemplo:

  • GET www.foo.com/highlights => uma lista de todos os anúncios que são destaques.
  • GET www.foo.com/highlights/new => formulário para criar um novo destaque etc …

Como eu posso fazer isso?

Obrigado!

Primeiro. Adicione algumas novas rotas:

 resources :highlights, :controller => "ads", :type => "Highlight" resources :bargains, :controller => "ads", :type => "Bargain" 

E corrija algumas ações no AdsController . Por exemplo:

 def new @ad = Ad.new() @ad.type = params[:type] end 

Para melhor abordagem para todo esse trabalho do controlador, veja este comentário

Isso é tudo. Agora você pode ir para localhost:3000/highlights/new e new Highlight será inicializado.

A ação do índice pode ser assim:

 def index @ads = Ad.where(:type => params[:type]) end 

Vá para localhost:3000/highlights e lista de destaques irá aparecer.
Mesma maneira de barganhas: localhost:3000/bargains

etc

URLS

 <%= link_to 'index', :highlights %> <%= link_to 'new', [:new, :highlight] %> <%= link_to 'edit', [:edit, @ad] %> <%= link_to 'destroy', @ad, :method => :delete %> 

por ser polimorfo 🙂

 <%= link_to 'index', @ad.class %> 

fl00r tem uma boa solução, porém eu faria um ajuste.

Isso pode ou não ser necessário no seu caso. Depende de qual comportamento está mudando em seus modelos de STI, especialmente validações e ganchos do ciclo de vida.

Adicione um método privado ao seu controlador para converter seu tipo de parâmetro na constante de class real que você deseja usar:

 def ad_type params[:type].constantize end 

O acima é inseguro, no entanto. Adicione uma lista de permissions de tipos:

 def ad_types [MyType, MyType2] end def ad_type params[:type].constantize if params[:type].in? ad_types end 

Mais sobre os rails constantes método aqui: http://api.rubyonrails.org/classs/ActiveSupport/Inflector.html#method-i-constantize

Então, nas ações do controlador, você pode fazer:

 def new ad_type.new end def create ad_type.new(params) # ... end def index ad_type.all end 

E agora você está usando a class real com o comportamento correto em vez da class pai com o conjunto de tipos de atributos.

Eu só queria include este link porque há vários truques interessantes relacionados a este tópico.

Alex Reisner – Herança de Tabela Única em rails

[Reescrito com uma solução mais simples que funciona totalmente:]

Iterando sobre as outras respostas, eu propus a seguinte solução para um único controlador com Herança de Tabela Única que funciona bem com o Strong Parameters in Rails 4.1. Apenas incluindo: type como um parâmetro permitido causou um erro ActiveRecord::SubclassNotFound se um tipo inválido for inserido. Além disso, o tipo não é atualizado porque a consulta SQL procura explicitamente o tipo antigo. Em vez disso,: :type precisa ser atualizado separadamente com update_column se for diferente do que é definido atualmente e é um tipo válido. Note também que consegui secar todas as listas de tipos.

 # app/models/company.rb class Company < ActiveRecord::Base COMPANY_TYPES = %w[Publisher Buyer Printer Agent] validates :type, inclusion: { in: COMPANY_TYPES, :message => "must be one of: #{COMPANY_TYPES.join(', ')}" } end Company::COMPANY_TYPES.each do |company_type| string_to_eval = <<-heredoc class #{company_type} < Company def self.model_name # http://stackoverflow.com/a/12762230/1935918 Company.model_name end end heredoc eval(string_to_eval, TOPLEVEL_BINDING) end 

E no controlador:

  # app/controllers/companies_controller.rb def update @company = Company.find(params[:id]) # This separate step is required to change Single Table Inheritance types new_type = params[:company][:type] if new_type != @company.type && Company::COMPANY_TYPES.include?(new_type) @company.update_column :type, new_type end @company.update(company_params) respond_with(@company) end 

E rotas:

 # config/routes.rb Rails.application.routes.draw do resources :companies Company::COMPANY_TYPES.each do |company_type| resources company_type.underscore.to_sym, type: company_type, controller: 'companies', path: 'companies' end root 'companies#index' 

Por fim, recomendo usar a gema de responders e configurar o scaffolding para usar um responders_controller, compatível com o STI. Configuração para andaimes é:

 # config/application.rb config.generators do |g| g.scaffold_controller "responders_controller" end 

Eu sei que esta é uma questão antiga por aqui é um padrão que eu gosto, que inclui as respostas de @flOOr e @Alan_Peabody. (Testado no Rails 4.2, provavelmente funciona no Rails 5)

Em seu modelo, crie sua lista de permissions na boot. Em dev isso deve estar ansioso carregado.

 class Ad < ActiveRecord::Base Rails.application.eager_load! if Rails.env.development? TYPE_NAMES = self.subclasses.map(&:name) #You can add validation like the answer by @dankohn end 

Agora podemos fazer referência a essa lista de permissions em qualquer controlador para criar o escopo correto, bem como em uma coleção para um tipo: selecione em um formulário, etc.

 class AdsController < ApplicationController before_action :set_ad, :only => [:show, :compare, :edit, :update, :destroy] def new @ad = ad_scope.new end def create @ad = ad_scope.new(ad_params) #the usual stuff comes next... end private def set_ad #works as normal but we use our scope to ensure subclass @ad = ad_scope.find(params[:id]) end #return the scope of a Ad STI subclass based on params[:type] or default to Ad def ad_scope #This could also be done in some kind of syntax that makes it more like a const. @ad_scope ||= params[:type].try(:in?, Ad::TYPE_NAMES) ? params[:type].constantize : Ad end #strong params check works as expected def ad_params params.require(:ad).permit({:foo}) end end 

Precisamos manipular nossos formulários porque o roteamento deve ser enviado ao controlador da class base, apesar do tipo real do object. Para fazer isso usamos "torna-se" para enganar o construtor de formulários no roteamento correto, e a diretiva: as para forçar os nomes de input a serem a class base também. Essa combinação nos permite usar rotas não modificadas (resources: anúncios), bem como a verificação de parâmetros fortes nos parâmetros [: ad] voltando do formulário.

 #/views/ads/_form.html.erb <%= form_for(@ad.becomes(Ad), :as => :ad) do |f| %>