Najlepsze praktyki dotyczące obsługi tras dla podklas STI w szynach

175

Szyny moje poglądy i kontrolery są zaśmiecone redirect_to, link_toi form_forwywołań metod. Czasami link_toi redirect_tosą jawne w ścieżkach, które łączą (np. link_to 'New Person', new_person_path), Ale często ścieżki są niejawne (np link_to 'Show', person.).

Dodaję dziedziczenie pojedynczej tabeli (STI) do mojego modelu (powiedzmy Employee < Person) i wszystkie te metody przestaną działać dla instancji podklasy (powiedzmy Employee); gdy railsy są wykonywane link_to @person, wyświetla błędy undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Railsy poszukują trasy zdefiniowanej przez nazwę klasy obiektu, którym jest pracownik. Te trasy pracowników nie są zdefiniowane i nie ma kontrolera pracowniczego, więc akcje też nie są zdefiniowane.

To pytanie zostało już zadane:

  1. W StackOverflow odpowiedzią jest edycja każdego wystąpienia link_to itp. W całej bazie kodu i jawne określenie ścieżki
  2. W StackOverflow ponownie dwie osoby sugerują użycie routes.rbdo mapowania zasobów podklasy na klasę nadrzędną ( map.resources :employees, :controller => 'people'). Najlepsza odpowiedź na to samo pytanie SO sugeruje rzutowanie typów każdego obiektu instancji w bazie kodu przy użyciu.becomes
  3. Jeszcze inny w StackOverflow , najlepsza odpowiedź to sposób w obozie Powtarzaj się i sugeruje utworzenie zduplikowanego rusztowania dla każdej podklasy.
  4. Oto znowu to samo pytanie w SO, gdzie górna odpowiedź wydaje się po prostu błędna (magia Railsów po prostu działa!)
  5. W innym miejscu w sieci znalazłem ten wpis na blogu, w którym F2Andy zaleca edycję ścieżki w każdym miejscu kodu.
  6. W poście na blogu Dziedziczenie pojedynczej tabeli i RESTful Routes at Logical Reality Design zaleca się zmapowanie zasobów podklasy do kontrolera nadklasy, jak w odpowiedzi SO nr 2 powyżej.
  7. Alex Reisner ma posta Single Table Inheritance w Rails , w którym opowiada się przeciwko mapowanie zasobów zajęć dzieci do klasy dominującej routes.rb, ponieważ tylko od połowy trasy pęknięcia link_toi redirect_to, ale nie z form_for. Dlatego zaleca zamiast tego dodanie metody do klasy nadrzędnej, aby podklasy kłamały na temat swojej klasy. Brzmi dobrze, ale jego metoda dała mi błąd undefined local variable or method `child' for #.

Tak więc odpowiedzią, która wydaje się najbardziej elegancka i ma największy konsensus (ale nie jest to aż tak eleganckie ani aż tak dużo konsensusu), jest dodanie zasobów do swoich routes.rb. Tyle że to nie działa form_for. Potrzebuję jasności! Aby wydestylować powyższe opcje, moje opcje są

  1. zamapuj zasoby podklasy na kontroler nadklasy w routes.rb(i mam nadzieję, że nie będę musiał wywoływać form_for na żadnej podklasie)
  2. Zastąp wewnętrzne metody railsów, aby klasy okłamały się wzajemnie
  3. Edytuj każde wystąpienie w kodzie, w którym ścieżka do akcji obiektu jest wywoływana niejawnie lub jawnie, zmieniając ścieżkę lub rzutowanie typu obiektu.

Przy tych wszystkich sprzecznych odpowiedziach potrzebuję orzeczenia. Wydaje mi się, że nie ma dobrej odpowiedzi. Czy to błąd w projektowaniu szyn? Jeśli tak, czy jest to błąd, który można naprawić? A jeśli nie, to mam nadzieję, że ktoś może mi na to pozwolić, przeprowadzić mnie przez wady i zalety każdej opcji (lub wyjaśnić, dlaczego nie ma takiej opcji) i która z nich jest właściwą odpowiedzią i dlaczego. A może istnieje prawidłowa odpowiedź, której nie znajduję w sieci?

ziggurism
źródło
1
W kodzie Alexa Reisnera była literówka, którą naprawił po tym, jak skomentowałem jego blog. Mam więc nadzieję, że rozwiązanie Alexa jest teraz wykonalne. Moje pytanie wciąż jest aktualne: które rozwiązanie jest właściwe?
ziggurism
1
Mimo że ma około trzech lat, znalazłem ten post na blogu pod adresem rookieonrails.blogspot.com/2008/01/ ... i połączoną z nim konwersację z listy mailingowej railsów . Jeden z respondentów opisuje różnicę między pomocnikami polimorficznymi a pomocnikami nazwanymi.
ziggurism
2
Opcją, której nie wymieniasz, jest łatanie Railsów tak, aby link_to, form_for i tym podobne umieszczały ładnie z dziedziczeniem pojedynczej tabeli. To może być ciężka praca, ale chciałbym, żeby to naprawiono.
M. Scott Ford,

Odpowiedzi:

140

To najprostsze rozwiązanie, jakie udało mi się wymyślić, przy minimalnym efekcie ubocznym.

class Person < Contact
  def self.model_name
    Contact.model_name
  end
end

Teraz url_for @personzostanie zmapowany contact_pathzgodnie z oczekiwaniami.

Jak to działa: pomocnicy adresów URL polegają na YourModel.model_namerefleksji nad modelem i generowaniu (między innymi) kluczy tras w liczbie pojedynczej / mnogiej. Tutaj Personjest w zasadzie mówiąc jestem jak Contactkoleś, zapytaj go .

Prathan Thananart
źródło
4
Myślałem o zrobieniu tego samego, ale martwiłem się, że #model_name może być używane gdzie indziej w Railsach i że ta zmiana może zakłócić normalne funkcjonowanie. jakieś pomysły?
nkassis
3
Całkowicie zgadzam się z tajemniczym nieznajomym @nkassisem. To fajny hack, ale skąd wiesz, że nie niszczysz elementów wewnętrznych rails?
tsherif
6
Okular. Ponadto używamy tego kodu w produkcji i mogę zaświadczyć, że nie psuje: 1) relacje między modelami, 2) tworzenie instancji modelu STI (za pośrednictwem build_x/ create_x). Z drugiej strony cena zabawy magią jest taka, że ​​nigdy nie jesteś w 100% pewien, co może się zmienić.
Prathan Thananart
10
To zepsuje i18n, jeśli próbujesz mieć różne ludzkie imiona dla atrybutów w zależności od klasy.
Rufo Sanchez
4
Zamiast całkowicie zastępować w ten sposób, możesz po prostu zastąpić potrzebne bity. Zobacz gist.github.com/sj26/5843855
sj26
47

Miałem ten sam problem. Po użyciu STI form_formetoda wysyłała do niewłaściwego adresu URL podrzędnego.

NoMethodError (undefined method `building_url' for

Skończyło się na dodaniu dodatkowych tras dla klas podrzędnych i skierowaniu ich do tych samych kontrolerów

 resources :structures
 resources :buildings, :controller => 'structures'
 resources :bridges, :controller => 'structures'

Do tego:

<% form_for(@structure, :as => :structure) do |f| %>

w tym przypadku struktura jest właściwie budynkiem (klasa potomna)

Wydaje się, że działa dla mnie po przesłaniu form_for.

James
źródło
2
To działa, ale dodaje wiele niepotrzebnych ścieżek na naszych trasach. Czy nie ma sposobu, aby to zrobić w mniej inwazyjny sposób?
Anders Kindberg,
1
Możesz ustawić trasy programowo w pliku Routes.rb, więc możesz zrobić trochę programowania meta, aby ustawić trasy podrzędne. Jednak w środowiskach, w których klasy nie są buforowane (np. Programowanie), musisz najpierw załadować klasy. Tak czy inaczej, musisz gdzieś określić podklasy. Przykład można znaleźć na gist.github.com/1713398 .
Chris Bloom
W moim przypadku ujawnianie nazwy obiektu (ścieżki) użytkownikowi nie jest pożądane (i mylące dla użytkownika).
laffuste
33

Proponuję zajrzeć na: https://stackoverflow.com/a/605172/445908 , użycie tej metody umożliwi Ci użycie „form_for”.

ActiveRecord::Base#becomes
Siwei Shen 申思维
źródło
2
Musiałem jawnie ustawić adres URL, aby poprawnie renderował tekst źródłowy i zapisywał. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
lulalala
4
@lulalala Try<%= form_for @child.becomes(Parent)
Richard Jones
14

Idąc za ideą @Prathan Thananart, ale starając się niczego nie zniszczyć. (ponieważ w grę wchodzi tak dużo magii)

class Person < Contact
  model_name.class_eval do
    def route_key
     "contacts"
    end
    def singular_route_key
      superclass.model_name.singular_route_key
    end
  end
end

Teraz url_for @person będzie mapowane na contact_path zgodnie z oczekiwaniami.

eloyesp
źródło
14

Ja też miałem problem z tym problemem i otrzymałem tę odpowiedź na pytanie podobne do naszego. U mnie to zadziałało.

form_for @list.becomes(List)

Odpowiedź pokazana tutaj: Używanie ścieżki STI z tym samym kontrolerem

.becomesMetoda jest zdefiniowana jako stosowane głównie do rozwiązywania problemów STI jak twój form_forjeden.

.becomesinformacje tutaj: http://apidock.com/rails/ActiveRecord/Base/becomes

Bardzo późna odpowiedź, ale to najlepsza odpowiedź, jaką udało mi się znaleźć i działała dobrze. Mam nadzieję, że to komuś pomoże. Twoje zdrowie!

Marcus
źródło
5

Ok, miałem mnóstwo frustracji w tym obszarze Railsów i doszedłem do następującego podejścia, być może to pomoże innym.

Przede wszystkim należy pamiętać, że wiele rozwiązań w sieci i na całym świecie sugeruje użycie stałej na parametrach podanych przez klienta. Jest to znany wektor ataku DoS, ponieważ Ruby nie zbiera śmieci, co pozwala atakującemu na tworzenie dowolnych symboli i zużywanie dostępnej pamięci.

Zaimplementowałem poniższe podejście, które obsługuje tworzenie instancji podklas modelu i jest BEZPIECZNE od powyższego problemu contantize. Jest bardzo podobny do tego, co robi rails 4, ale pozwala także na więcej niż jeden poziom podklas (w przeciwieństwie do Rails 4) i działa w Rails 3.

# initializers/acts_as_castable.rb
module ActsAsCastable
  extend ActiveSupport::Concern

  module ClassMethods

    def new_with_cast(*args, &block)
      if (attrs = args.first).is_a?(Hash)
        if klass = descendant_class_from_attrs(attrs)
          return klass.new(*args, &block)
        end
      end
      new_without_cast(*args, &block)
    end

    def descendant_class_from_attrs(attrs)
      subclass_name = attrs.with_indifferent_access[inheritance_column]
      return nil if subclass_name.blank? || subclass_name == self.name
      unless subclass = descendants.detect { |sub| sub.name == subclass_name }
        raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
      end
      subclass
    end

    def acts_as_castable
      class << self
        alias_method_chain :new, :cast
      end
    end
  end
end

ActiveRecord::Base.send(:include, ActsAsCastable)

Po wypróbowaniu różnych podejść do problemu „ładowania podklasy w programowaniu”, wielu podobnych do tego, co zasugerowano powyżej, stwierdziłem, że jedyną rzeczą, która działała niezawodnie, było użycie parametru „require_dependency” w moich klasach modeli. Gwarantuje to, że ładowanie klas działa poprawnie podczas programowania i nie powoduje problemów w środowisku produkcyjnym. W fazie rozwoju, bez „require_dependency” AR nie będzie wiedział o wszystkich podklasach, co ma wpływ na kod SQL emitowany do dopasowania w kolumnie typu. Ponadto bez „require_dependency” możesz również znaleźć się w sytuacji z wieloma wersjami klas modelu w tym samym czasie! (np. może się to zdarzyć, gdy zmienisz klasę podstawową lub pośrednią, podklasy nie zawsze wydają się ponownie ładować i pozostają podklasy ze starej klasy)

# contact.rb
class Contact < ActiveRecord::Base
  acts_as_castable
end

require_dependency 'person'
require_dependency 'organisation'

Nie nadpisuję również nazwa_modelu, jak sugerowano powyżej, ponieważ używam I18n i potrzebuję różnych ciągów dla atrybutów różnych podklas, np .: identyfikator_podatku staje się „ABN” dla organizacji i „TFN” dla osoby (w Australii).

Korzystam też z mapowania tras, jak zasugerowałem powyżej, ustawiając typ:

resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }

Oprócz mapowania tras używam InheritedResources i SimpleForm oraz używam następującego ogólnego opakowania formularza dla nowych akcji:

simple_form_for resource, as: resource_request_name, url: collection_url,
      html: { class: controller_name, multipart: true }

... a do edycji:

simple_form_for resource, as: resource_request_name, url: resource_url,
      html: { class: controller_name, multipart: true }

Aby to zadziałało, w moim podstawowym ResourceContoller ujawniam resource_request_name InheritedResource jako metodę pomocniczą dla widoku:

helper_method :resource_request_name 

Jeśli nie używasz InheritedResources, użyj czegoś podobnego do następującego w swoim „ResourceController”:

# controllers/resource_controller.rb
class ResourceController < ApplicationController

protected
  helper_method :resource
  helper_method :resource_url
  helper_method :collection_url
  helper_method :resource_request_name

  def resource
    @model
  end

  def resource_url
    polymorphic_path(@model)
  end

  def collection_url
    polymorphic_path(Model)
  end

  def resource_request_name
    ActiveModel::Naming.param_key(Model)
  end
end

Zawsze chętnie słucham doświadczeń i ulepszeń innych.

Andrew Hacking
źródło
Z mojego doświadczenia (przynajmniej w Railsach 3.0.9) wynika, że ​​Constantize zawodzi, jeśli stała nazwana przez ciąg nie istnieje. Jak więc można go użyć do tworzenia dowolnych nowych symboli?
Lex Lindsey
4

Niedawno udokumentowałem moje próby uzyskania stabilnego wzorca STI działającego w aplikacji Rails 3.0. Oto wersja TL; DR:

# app/controllers/kase_controller.rb
class KasesController < ApplicationController

  def new
    setup_sti_model
    # ...
  end

  def create
    setup_sti_model
    # ...
  end

private

  def setup_sti_model
    # This lets us set the "type" attribute from forms and querystrings
    model = nil
    if !params[:kase].blank? and !params[:kase][:type].blank?
      model = params[:kase].delete(:type).constantize.to_s
    end
    @kase = Kase.new(params[:kase])
    @kase.type = model
  end
end

# app/models/kase.rb
class Kase < ActiveRecord::Base
  # This solves the `undefined method alpha_kase_path` errors
  def self.inherited(child)
    child.instance_eval do
      def model_name
        Kase.model_name
      end
    end
    super
  end  
end

# app/models/alpha_kase.rb
# Splitting out the subclasses into separate files solves
# the `uninitialize constant AlphaKase` errors
class AlphaKase < Kase; end

# app/models/beta_kase.rb
class BetaKase < Kase; end

# config/initializers/preload_sti_models.rb
if Rails.env.development?
  # This ensures that `Kase.subclasses` is populated correctly
  %w[kase alpha_kase beta_kase].each do |c|
    require_dependency File.join("app","models","#{c}.rb")
  end
end

Takie podejście pozwala obejść wymienione problemy, a także szereg innych problemów, które inni napotkali w związku z podejściami do chorób przenoszonych drogą płciową.

Chris Bloom
źródło
2

Możesz spróbować tego, jeśli nie masz zagnieżdżonych tras:

resources :employee, path: :person, controller: :person

Lub możesz pójść inną drogą i użyć magii OOP, jak opisano tutaj: https://coderwall.com/p/yijmuq

Po drugie, możesz stworzyć podobne pomocniki dla wszystkich zagnieżdżonych modeli.

everm1nd
źródło
2

Oto bezpieczny, czysty sposób, aby działał w formularzach i w całej aplikacji, z której korzystamy.

resources :districts
resources :district_counties, controller: 'districts', type: 'County'
resources :district_cities, controller: 'districts', type: 'City'

Wtedy mam w swojej formie. Dodatkowym elementem jest as:: district.

= form_for(@district, as: :district, html: { class: "form-horizontal",         role: "form" }) do |f|

Mam nadzieję że to pomoże.

Jake
źródło
2

Najczystszym rozwiązaniem, jakie znalazłem, jest dodanie następujących elementów do klasy bazowej:

def self.inherited(subclass)
  super

  def subclass.model_name
    super.tap do |name|
      route_key = base_class.name.underscore
      name.instance_variable_set(:@singular_route_key, route_key)
      name.instance_variable_set(:@route_key, route_key.pluralize)
    end
  end
end

Działa dla wszystkich podklas i jest znacznie bezpieczniejszy niż przesłanianie całego obiektu nazwy modelu. Celując tylko w klucze tras, rozwiązujemy problemy z routingiem bez przerywania I18n lub ryzyka jakichkolwiek potencjalnych skutków ubocznych spowodowanych zastąpieniem nazwy modelu zdefiniowanej przez Railsy.

Aleksandra Makarenko
źródło
1

Jeśli rozważę takie dziedziczenie STI:

class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end

w 'app / models / a_model.rb' dodaję:

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

A potem w klasie AModel:

class AModel < ActiveRecord::Base
  def self.instanciate_STI
    managed_deps = { 
      :b_model => true,
      :c_model => true,
      :d_model => true,
      :e_model => true
    }
    managed_deps.each do |dep, managed|
      require_dependency dep.to_s
      klass = dep.to_s.camelize.constantize
      # Inject behavior to be managed at AModel level for classes I chose
      klass.send(:extend, ManagedAtAModelLevel) if managed
    end
  end

  instanciate_STI
end

Dlatego mogę nawet łatwo wybrać, który model chcę użyć domyślnego, i to nawet bez dotykania definicji podklasy. Bardzo suchy.

Laurent B.
źródło
1

U mnie to działa w ten sposób (zdefiniuj tę metodę w klasie bazowej):

def self.inherited(child)
  child.instance_eval do
    alias :original_model_name :model_name
    def model_name
      Task::Base.model_name
    end
  end
  super
end
ka8725
źródło
1

Możesz stworzyć metodę, która zwraca fikcyjny obiekt Parent w celu routingu

class Person < ActiveRecord::Base      
  def routing_object
    Person.new(id: id)
  end
end

a następnie po prostu wywołaj form_for @ worker.routing_object, który bez typu zwróci obiekt klasy Person

mieszać
źródło
1

Po odpowiedzi @ prathan-thananart i dla wielu klas STI możesz dodać następujące elementy do modelu nadrzędnego ->

class Contact < ActiveRecord::Base
  def self.model_name
    ActiveModel::Name.new(self, nil, 'Contact')
  end
end

Które uczynią każdy formularz z danymi kontaktowymi wysłać params jak params[:contact]zamiast params[:contact_person], params[:contact_whatever].

Zlatko Alomerovic
źródło
-6

hackish, ale tylko kolejny do listy rozwiązań.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

działa na szynach 2.x i 3.x

Ohad Levy
źródło
5
To rozwiązuje jeden problem, ale tworzy inny. Teraz, gdy spróbujesz to zrobić Child.new, zwraca Parentklasę, a nie podklasę. Oznacza to, że nie można tworzyć podklas poprzez przypisanie masowe za pośrednictwem kontrolera (ponieważ typejest to domyślnie chroniony atrybut), chyba że jawnie ustawisz atrybut type.
Chris Bloom