STI, ein Controller

Ich bin neu bei Rails und ich bleibe irgendwie bei diesem Designproblem, das vielleicht leicht zu lösen ist, aber ich komme nirgendwohin: Ich habe zwei verschiedene Arten von Werbung: Highlights und Schnäppchen. Beide haben die gleichen Attribute: Titel, Beschreibung und ein Bild (mit Büroklammer). Sie haben auch die gleichen Aktionen, die auf sie angewendet werden: Index, Neu, Bearbeiten, Erstellen, Aktualisieren und Zerstören.

Ich habe eine STI so eingestellt:

Anzeigenmodell: ad.rb

class Ad < ActiveRecord::Base end 

Schnäppchenmodell: bargain.rb

 class Bargain < Ad end 

Highlight-Modell: highlight.rb

 class Highlight < Ad end 

Das Problem ist, dass ich nur einen Controller ( AdsController ) haben AdsController , der die Aktionen ausführt, die ich auf Schnäppchen oder Highlights je nach URL gesagt habe, zB www.foo.com/bargains [/ …] oder www.foo. com / highlights [/ …].

Beispielsweise:

  • GET www.foo.com/highlights => eine Liste aller Anzeigen, die Highlights sind.
  • GET www.foo.com/highlights/new => form, um ein neues highlight zu erstellen etc …

Wie kann ich das machen?

Vielen Dank!

Zuerst. Fügen Sie neue Routen hinzu:

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

Und beheben Sie einige Aktionen in AdsController . Beispielsweise:

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

Für den besten Ansatz für alle diese Controller-Job sehen Sie diesen Kommentar

Das ist alles. Jetzt können Sie zu localhost:3000/highlights/new gehen localhost:3000/highlights/new und new Highlight wird initialisiert.

Die Indexaktion kann folgendermaßen aussehen:

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

Gehen Sie zu localhost:3000/highlights und die Liste der Highlights wird angezeigt.
Gleicher Weg für Schnäppchen: 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 %> 

um polymorph zu sein 🙂

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

fl00r hat eine gute Lösung, allerdings würde ich eine Anpassung vornehmen.

Dies kann in Ihrem Fall erforderlich sein oder auch nicht. Es hängt davon ab, welches Verhalten sich in Ihren STI-Modellen ändert, insbesondere validationen und Lebenszyklus-Hooks.

Fügen Sie Ihrem Controller eine private Methode hinzu, um Ihren Typparameter in die tatsächliche classnkonstante zu konvertieren, die Sie verwenden möchten:

 def ad_type params[:type].constantize end 

Das Obige ist jedoch unsicher. Fügen Sie eine Whitelist von Typen hinzu:

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

Mehr über die Methode constantsize hier: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize

Dann können Sie in den Controller-Aktionen Folgendes tun:

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

Und jetzt verwenden Sie die tatsächliche class mit dem richtigen Verhalten anstelle der übergeordneten class mit dem festgelegten Attributtyp.

Ich wollte diesen Link nur einfügen, weil es eine Reihe von interessanten Tricks zu diesem Thema gibt.

Alex Reisner – inheritance einzelner Tabellen in Rails

[Umgeschrieben mit einfacherer Lösung, die voll funktioniert:]

Bei den anderen Antworten habe ich die folgende Lösung für einen einzelnen Controller mit Single Table Inheritance gefunden, die gut mit Strong Parameters in Rails 4.1 funktioniert. Einschließlich: type als zulässiger Parameter verursachte einen ActiveRecord::SubclassNotFound Fehler, wenn ein ungültiger Typ eingegeben wurde. Darüber hinaus wird der Typ nicht aktualisiert, da die SQL-Abfrage explizit nach dem alten Typ sucht. Stattdessen muss :type separat mit update_column aktualisiert werden, wenn es sich von dem aktuellen Satz unterscheidet und ein gültiger Typ ist. Beachten Sie auch, dass es mir gelungen ist, alle Artenlisten zu trocknen.

 # 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 

Und in der Steuerung:

  # 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 

Und Routen:

 # 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' 

Schließlich empfehle ich, das Responder- Gem und das Scaffolding zu verwenden, um einen Responder-Controller zu verwenden, der mit STI kompatibel ist. Konfig für Gerüstbau ist:

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

Ich weiß, das ist eine alte Frage, denn hier ist ein Muster, das ich mag, das die Antworten von @flOOr und @Alan_Peabody enthält. (Getestet in Rails 4.2, funktioniert wahrscheinlich in Rails 5)

In Ihrem Modell erstellen Sie Ihre Whitelist beim Start. In dev muss dies eifrig geladen werden.

 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 

Jetzt können wir diese Whitelist in jedem Controller referenzieren, um den richtigen Bereich zu erstellen, sowie in einer Sammlung für einen: type select auf einem Formular usw.

 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 

Wir müssen mit unseren Formularen umgehen, da das Routing trotz des tatsächlichen Typs des Objekts an den Basisklassencontroller gesendet werden sollte. Dazu verwenden wir "wird", um den Formulargenerator in das richtige Routing zu überführen, und die statement ": as", um zu erzwingen, dass die Eingabenamen auch die Basisklasse bilden. Diese Kombination erlaubt es uns, unmodifizierte Routen (Ressourcen: Anzeigen) zu verwenden, sowie die starke Params-Prüfung der params [: ad], die aus dem Formular zurückkommen.

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