Найкращі практики поводження з маршрутами для підкласів STI в рейках


175

Мої Рейки думку і контролери завалені redirect_to, link_toі form_forвикликами методів. Іноді link_toі redirect_toявні в шляхах, які вони пов'язують (наприклад link_to 'New Person', new_person_path), але багато разів шляхи є неявними (наприклад link_to 'Show', person).

Я додаю деяку спадщину однієї таблиці (STI) до моєї моделі (скажімо Employee < Person), і всі ці методи розбиваються на екземпляр підкласу (скажімо Employee); коли рейки виконуються link_to @person, він помилки з undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails шукає маршрут, визначений назвою класу об'єкта, який є службовцем. Ці маршрути співробітників не визначені, і немає контролера працівника, тому дії також не визначені.

Це питання було задано раніше:

  1. У StackOverflow відповідь полягає в редагуванні кожного екземпляра link_to тощо у всій вашій кодовій базі та констатуйте шлях прямо
  2. На StackOverflow знову, двоє людей пропонують використовувати routes.rbдля зіставлення підкласу ресурсів на батьківський клас ( map.resources :employees, :controller => 'people'). Найвища відповідь у тому самому запитанні SO пропонує запропонувати тип-кастинг кожного об'єкта екземпляра в кодовій базі.becomes
  3. Ще одна у StackOverflow , головна відповідь - це спосіб у таборі Do Repeat Yourself, і пропонує створити дублікати лісів для кожного підкласу.
  4. Ось те саме запитання в SO, де головна відповідь здається просто неправильною (Rails magic Just Works!)
  5. Ще в Інтернеті я знайшов цю публікацію в блозі, де F2Andy рекомендує редагувати в коді всюди в коді.
  6. У публікації щоденника блогу Наспадкування єдиної таблиці та маршрути RESTful у дизайні логічної реальності рекомендується зіставити ресурси для підкласу до контролера надкласового класу, як у відповіді № 2 вище.
  7. Alex Рейснер має запис однієї таблиці Спадкування в Rails , в якому він виступає за проти картування ресурсів дочірніх класів батьківського класу routes.rb, так що тільки улови маршрутизації обриви від link_toі redirect_to, але не з form_for. Тому він рекомендує замість цього додати метод до батьківського класу, щоб отримати підкласи брехні про їх клас. Звучить добре, але його метод дав мені помилку undefined local variable or method `child' for #.

Таким чином, відповідь , який здається найбільш елегантно і має найбільший консенсус (але це ще не все , що елегантним, ні що багато консенсусу), є додавання ресурсів до вашого routes.rb. За винятком цього не працює form_for. Мені потрібна ясність! Щоб переганяти варіанти вище, мої варіанти є

  1. зіставити ресурси підкласу до контролера надкласу в routes.rb(і сподіваюся, мені не потрібно викликати form_for для будь-яких підкласів)
  2. Переосмислить рейкові внутрішні методи, щоб змусити класи брехати один одному
  3. Відредагуйте кожен екземпляр у коді, де шлях до дії об’єкта викликається неявно або явно, або змінюючи шлях, або об'єкт-кастинг.

З усіма цими суперечливими відповідями мені потрібна ухвала. Мені здається, що немає хорошої відповіді. Це невдача в дизайні рейок? Якщо так, чи може виправити помилку? А якщо ні, то я сподіваюсь, що хтось зможе мене зрозуміти, переглядати плюси і мінуси кожного з варіантів (або пояснити, чому це не варіант), і який з них є правильною відповіддю і чому. Або є правильна відповідь, яку я не знаходжу в Інтернеті?


1
У коді Алекса Рейснера була помилка друку, яку він виправив після того, як я прокоментував його блог. Тож сподіваємось, тепер рішення Алекса є життєздатним. Моє питання все ще стоїть: яке правильне рішення?
зигуризм

1
Хоча це вже близько трьох років, я знайшов цю публікацію в блозі на rookieonrails.blogspot.com/2008/01/… та пов’язану розмову із списку розсилки з рейки. Один із респондентів описує різницю між поліморфними помічниками та названими помічниками.
зигуризм

2
Варіант, який ви не перераховуєте, - це закріпити Rails так, щоб link_to, form_for і подібне місце було приємним при успадкуванні однієї таблиці. Це може бути важкою роботою, але я б хотів виправити це.
М. Скотт Форд

Відповіді:


140

Це найпростіше рішення, яке мені вдалося придумати з мінімальними побічними ефектами.

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

Тепер url_for @personбуде відображено так, contact_pathяк очікувалося.

Як це працює: помічники URL-адреси покладаються на те, YourModel.model_nameщоб замислитись над моделлю та створити (серед багатьох речей) сингулярні / множинні ключі маршруту. Ось Personв основному кажу, що я просто як Contactчувак, запитайте його .


4
Я думав зробити те саме, але переживав, що #model_name може використовуватися в інших місцях Rails і що ця зміна може заважати нормальному функціонуванню. Будь-які думки?
nkassis

3
Я повністю згоден з таємничим незнайомцем @nkassis. Це крутий злом, але як ти знаєш, що ти не перебиваєш інтер'єри рейок?
therif

6
Технічні характеристики Крім того, ми використовуємо цей код у виробництві, і я можу засвідчити, що він не псує: 1) взаємомодельні відносини, 2) інстанціювання моделі STI (через build_x/ create_x). З іншого боку, ціна гри з магією - ви ніколи не на 100% впевнені, що може змінитися.
Пратан Тананарт

10
Це порушує i18n, якщо ви намагаєтесь мати різні людські імена для атрибутів залежно від класу.
Руфо Санчес

4
Замість того, щоб повністю переосмислити подібне, ви можете просто змінити потрібні біти. Дивіться gist.github.com/sj26/5843855
sj26

47

У мене була така ж проблема. Після використання ІПСШ form_forметод передавав неправильну URL-адресу дитини.

NoMethodError (undefined method `building_url' for

Я в кінцевому підсумку додав додаткові маршрути до дитячих класів і вказав їх на ті самі контролери

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

Додатково:

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

у цьому випадку структура - це фактично будівля (дочірній клас)

Здається, це працює для мене після того, як ви робите подання з form_for.


2
Це працює, але додає багато непотрібних шляхів на наших маршрутах. Чи не існує способу зробити це менш нав'язливим способом?
Андерс Кіндберг

1
Ви можете налаштувати маршрути програмно у файлі route.rb, щоб ви могли зробити невелике метапрограмування для налаштування дочірніх маршрутів. Однак у середовищах, де класи не кешовані (наприклад, розробка), потрібно попередньо завантажити класи. Тож так чи інакше потрібно десь вказати підкласи. Дивіться приклад на gist.github.com/1713398 .
Кріс Блум

У моєму випадку піддавати ім’я об’єкта (шлях) користувачеві небажано (і для користувача заплутано).
лафуст

33

Я пропоную вам поглянути на: https://stackoverflow.com/a/605172/445908 , використання цього методу дозволить вам використовувати "form_for".

ActiveRecord::Base#becomes

2
Мені довелося чітко встановити URL-адресу, щоб він міг відображати та зберігати належним чином. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
Люлалала

4
@lulalala Try<%= form_for @child.becomes(Parent)
Річард Джонс


14

Слідом за ідеєю @Prathan Thananart, але намагаючись нічого не знищити. (оскільки в цьому є багато магії)

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

Тепер url_for @person зробить карту contact_path, як очікувалося.


14

У мене також виникли проблеми з цією проблемою, і я отримав цю відповідь на питання, подібне до нашого. Це працювало для мене.

form_for @list.becomes(List)

Тут представлена ​​відповідь: Використання шляху STI з тим же контролером

.becomesМетод визначається як використовується в основному для вирішення проблем ІПСШ , як ваш form_forдруг.

.becomesінформація тут: http://apidock.com/rails/ActiveRecord/Base/become

Супер пізня відповідь, але це найкраща відповідь, яку я міг знайти, і вона добре працювала на мене. Сподіваюся, це допоможе комусь. Ура!


5

Гаразд, у мене було багато розчарувань у цій області Рейсів, і я прийшов до наступного підходу, можливо, це допоможе іншим.

По-перше, майте на увазі, що ряд рішень над та навколо мережі пропонують використовувати константизацію за наданими клієнтом параметрами. Це відомий вектор DoS-атаки, оскільки Ruby не збирає символи сміття, тим самим дозволяє зловмиснику створювати довільні символи та споживати наявну пам'ять.

Я реалізував нижченаведений підхід, який підтримує інстанціювання модельних підкласів, і є БЕЗПЕЧНИМ із вищезазначеної проблеми контантізу. Це дуже схоже на те, що робить рейка 4, але також дозволяє більше ніж один рівень підкласифікації (на відміну від рейок 4) і працює в 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)

Спробувавши різні підходи до "завантаження підкласу у випуску розробки", багато подібного до запропонованого вище, я знайшов єдине, що надійно працювало, - використовувати "Requ_dependency" у своїх модельних класах. Це забезпечує, що завантаження класів працює належним чином у процесі розробки та не викликає проблем у виробництві. У процесі розробки, без 'Requirdependence', AR не буде знати про всі підкласи, що впливає на SQL, що випромінюється для відповідності на стовпчик типу. Крім того, без 'requ_dependency' ви також можете опинитися в ситуації з декількома версіями модельних класів одночасно! (наприклад, це може статися, коли ви змінюєте базовий чи проміжний клас, підкласи не завжди перезавантажуються і залишаються підкласами зі старого класу)

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

require_dependency 'person'
require_dependency 'organisation'

Я також не перекриваю model_name, як було запропоновано вище, тому що я використовую I18n і мені потрібні різні рядки для атрибутів різних підкласів, наприклад: tax_identifier стає "ABN" для організації, а "TFN" для Person (в Австралії).

Я також використовую відображення маршруту, як було запропоновано вище, встановлюючи тип:

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

На додаток до відображення маршруту, я використовую InheritedResources та SimpleForm, і я використовую наступну загальну форму обгортки для нових дій:

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

... і для дій редагування:

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

І щоб зробити цю роботу, у своїй базі ResourceContoller я викриваю ім’я ресурсу_реквізування InheritedResource як допоміжний метод для перегляду:

helper_method :resource_request_name 

Якщо ви не використовуєте InheritedResources, тоді використовуйте щось подібне у своєму "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

Завжди раді почути досвід та вдосконалення інших.


На мій досвід (принаймні, в Rails 3.0.9), константизація виходить з ладу, якщо константа, названа рядком, вже не існує. То як це можна використовувати для створення довільних нових символів?
Лекс Ліндсі

4

Нещодавно я задокументував свої спроби отримати стабільну схему ІПСШ, працюючи в додатку Rails 3.0. Ось версія 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

Цей підхід оточує проблеми, які ви перераховуєте, а також ряд інших питань, які мали інші підходи до ІПСШ.


2

Ви можете спробувати це, якщо у вас немає вкладених маршрутів:

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

Або ви можете піти іншим шляхом і скористатись OOP-магією, як описано тут: https://coderwall.com/p/yijmuq

По-друге, ви можете зробити подібні помічники для всіх своїх вкладених моделей.


2

Ось безпечний чистий спосіб його роботи у формах та у всій програмі, яку ми використовуємо.

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

Тоді я маю свою форму. Доданий фрагмент для цього є:: район.

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

Сподіваюся, це допомагає.


2

Найчистіше рішення, яке я знайшов, - це додати до базового класу наступне:

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

Він працює для всіх підкласів і набагато безпечніший, ніж перекриття всього об'єкта імені моделі. Націлюючись лише на ключі маршруту, ми вирішуємо проблеми маршрутизації, не порушуючи I18n або ризикуючи будь-якими потенційними побічними ефектами, викликаними переосмисленням назви моделі, визначеної Rails.


1

Якщо я вважаю спадщину ІПСШ так:

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

у додаток / моделі / a_model.rb я додаю:

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

А потім у класі 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

Тому я навіть легко можу вибрати, яку модель я хочу використовувати за замовчуванням, і це, навіть не торкаючись визначення підкласу. Дуже сухий.


1

Цей спосіб працює для мене добре (визначте цей метод у базовому класі):

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

1

Ви можете створити метод, який повертає фіктивний батьківський об'єкт для маршрутизації purpouse

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

а потім просто зателефонуйте form_for @ співробітник.routing_object, який без типу поверне об'єкт класу Person


1

Після відповіді @ prathan-thananart та для кількох класів ІПСШ ви можете додати наступне до батьківської моделі ->

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

Це зробить кожну форму з даними Контакту для відправки Params , як params[:contact]замість того , щоб params[:contact_person], params[:contact_whatever].


-6

хакі, але лише ще один до списку рішень.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

працює на рейках 2.x і 3.x


5
Це виправляє одну проблему, але створює іншу. Тепер при спробі зробити Child.newце повертає Parentклас, а не підклас. Це означає, що ви не можете створити підкласи за допомогою масового призначення через контролер (оскільки typeце захищений атрибут за замовчуванням), якщо ви також не встановите явно атрибут типу.
Кріс Блум
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.