Лучшие практики для обработки маршрутов для подклассов 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-маршруты» в проекте « Логическая реальность» рекомендуется сопоставить ресурсы для подкласса с контроллером суперкласса, как в ответе SO 2 выше.
  7. У Алекса Рейснера есть пост- наследование таблиц в 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/… и связанный с ним разговор из списка рассылки rails информативным. Один из респондентов описывает разницу между полиморфными помощниками и именованными помощниками.
зиггуризм

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. Это крутой хак, но откуда ты знаешь, что ты не разрушаешь внутренности рельсов?
tsherif

6
Спекуляции. Кроме того, мы используем этот код в производстве, и я могу засвидетельствовать, что он не портит: 1) межмодельные отношения, 2) создание экземпляров модели STI (через build_x/ create_x). С другой стороны, цена игры с магией в том, что вы никогда не уверены на 100%, что может измениться.
Пратан Тананарт

10
Это ломает i18n, если вы пытаетесь использовать разные человеческие имена для атрибутов в зависимости от класса.
Руфо Санчес

4
Вместо того, чтобы полностью переопределять, как это, вы можете просто переопределить нужные биты. См. Gist.github.com/sj26/5843855
sj26

47

У меня такая же проблема. После использования STI 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
Вы можете настроить маршруты программно в вашем файле rout.rb, так что вы можете сделать небольшое метапрограммирование для настройки дочерних маршрутов. Однако в средах, где классы не кэшируются (например, при разработке), вам необходимо предварительно загрузить классы. Так или иначе, вам нужно где-то указать подклассы. См. Gist.github.com/1713398 для примера.
Крис Блум

В моем случае раскрытие имени объекта (пути) пользователю нежелательно (и вводит в заблуждение пользователя).
laffuste

33

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

ActiveRecord::Base#becomes

2
Я должен был явно указать url, чтобы он правильно отображал from и сохранял. <%= 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/becomes

Супер поздний ответ, но это лучший ответ, который я мог найти, и он работал хорошо для меня. Надеюсь, это кому-нибудь поможет. Ура!


5

Хорошо, у меня было множество разочарований в этой области Rails, и я пришел к следующему подходу, возможно, это поможет другим.

Во-первых, имейте в виду, что ряд решений выше и вокруг сети предлагают использовать constantize для предоставленных клиентом параметров. Это известный вектор DoS-атак, поскольку Ruby не собирает символы мусора, что позволяет злоумышленнику создавать произвольные символы и использовать доступную память.

Ниже реализован подход, который поддерживает создание экземпляров подклассов модели и является БЕЗОПАСНЫМ из вышеописанной проблемы. Это очень похоже на то, что делает rails 4, но также позволяет использовать более одного уровня подклассов (в отличие от Rails 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)

Попробовав различные подходы к «загрузке подкласса в проблеме devlopment», многие из которых были похожи на предложенные выше, я обнаружил, что единственное, что надежно работает, - это использование require_dependency в моих модельных классах. Это гарантирует, что загрузка классов работает должным образом при разработке и не вызывает проблем в производстве. В процессе разработки без 'require_dependency' AR не будет знать обо всех подклассах, что влияет на SQL, выдаваемый для сопоставления в столбце типа. Кроме того, без 'require_dependency' вы также можете оказаться в ситуации с несколькими версиями классов модели одновременно! (например, это может произойти, когда вы изменяете базовый или промежуточный класс, кажется, что подклассы не всегда перезагружаются и остаются подклассами из старого класса)

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

require_dependency 'person'
require_dependency 'organisation'

Я также не перезаписываю model_name, как предложено выше, потому что я использую I18n и мне нужны разные строки для атрибутов разных подклассов, например: tax_identifier становится 'ABN' для организации и 'TFN' для персоны (в Австралии).

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

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), constantize завершается неудачно, если константа, названная строкой, еще не существует. Так как же это можно использовать для создания произвольных новых символов?
Лекс Линдси

4

Недавно я задокументировал свои попытки заставить стабильный шаблон STI работать в приложении 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

Или вы можете пойти другим путем и использовать некоторую ООП-магию, как описано здесь: 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

в 'app / models / 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

Вы можете создать метод, который возвращает фиктивный родительский объект для маршрутизации

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

а затем просто вызовите form_for @ employee.routing_object, который без типа вернет объект класса Person


1

После ответа @ prathan-thananart и для нескольких классов STI вы можете добавить следующее в родительскую модель ->

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по умолчанию это защищенный атрибут), если вы также не установили атрибут type явно.
Крис Блум
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.