Рекомендации по обработке маршрутов для подклассов STI в рельсах

Представления и контроллеры My Rails заполняются вызовами методов 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); когда rails выполняет link_to @person, это ошибки с undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails ищет маршрут, определяемый именем класса объекта, который является сотрудником. Эти маршруты сотрудников не определены, и нет контроллера сотрудника, поэтому действия также не определены.

Этот вопрос задан раньше:

  • В qaru.site/info/39531/... ответ должен отредактировать каждый экземпляр link_to и т.д. во всей вашей кодовой базе и указать путь явно
  • В qaru.site/info/39531/... два человека предлагают использовать routes.rb для сопоставления ресурсов подкласса родительскому классу (map.resources :employees, :controller => 'people'). Верхний ответ в том же самом вопросе SO предполагает, что тип экземпляра каждого экземпляра в базе кода использует тип .becomes
  • Еще один из qaru.site/info/39531/..., лучший ответ - в лагере Do Repeat Yourself и предлагает создать дублирующие леса для каждого подкласса.
  • Вот и тот же вопрос снова в SO, где верхний ответ кажется просто неправильным (Rails magic Just Works!)
  • В другом месте в Интернете я нашел это сообщение в блоге, где F2Andy рекомендует редактировать в пути всюду в коде.
  • В сообщении в блоге Одиночные таблицы и RESTful Routes в Logical Reality Design рекомендуется сопоставить ресурсы для подкласса с суперклассом контроллер, как и в ответе "Ответ №2" выше.
  • У Alex Reisner есть сообщение Одиночное наследование таблиц в Rails, в котором он выступает против сопоставления ресурсов дочерних классов родительскому классу в routes.rb, так как это только ловит разрывы маршрутизации от link_to и redirect_to, но не от form_for. Поэтому он рекомендует вместо этого добавить метод к родительскому классу, чтобы заставить подклассы лгать об их классе. Звучит неплохо, но его метод дал мне ошибку undefined local variable or method `child' for #.

Таким образом, ответ, который кажется самым изящным и имеет наибольший консенсус (но это не все, что изящно и не так консенсус), добавляет ресурсы к вашему routes.rb. Кроме того, это не работает для form_for. Мне нужна ясность! Чтобы отменить выбор выше, мои параметры

  • сопоставить ресурсы подкласса с контроллером суперкласса в routes.rb (и, надеюсь, мне не нужно вызывать form_for для любых подклассов)
  • Переопределить внутренние методы рельсов, чтобы классы лежали друг с другом.
  • Редактировать каждый экземпляр кода, в котором путь к действию объекта вызывается неявно или явно, либо изменяя путь, либо тип-литье объекта.

Со всеми этими противоречивыми ответами мне нужно решение. Мне кажется, что нет хорошего ответа. Это неудача в дизайне рельсов? Если да, это ошибка, которая может быть исправлена? Или, если нет, то я надеюсь, что кто-то может направить меня прямо на это, проведет меня через плюсы и минусы каждого варианта (или объясните, почему это не вариант), и какой из них правильный ответ и почему. Или есть правильный ответ, который я не нахожу в Интернете?

Ответ 1

Это самое простое решение, с которым я смог придумать с минимальным побочным эффектом.

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

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

Как это работает: Помощники URL-адреса полагаются на YourModel.model_name, чтобы отражать модель и генерировать (среди многих) уникальные/множественные ключи маршрута. Здесь Person в основном говорит, что я просто как Contact чувак, спросите его.

Ответ 2

У меня была та же проблема. После использования 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.

Ответ 3

Я предлагаю вам взглянуть на: fooobar.com/questions/39526/..., используя этот метод, вы сможете использовать "form_for".

ActiveRecord::Base#becomes

Ответ 5

Следуя идее @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 будет сопоставлять контакт_path, как ожидалось.

Ответ 6

У меня тоже были проблемы с этой проблемой, и я пришел к этому ответу по вопросу, подобному нашему. Это сработало для меня.

form_for @list.becomes(List)

Ответ показан ниже: Использование пути STI с одним и тем же контроллером

Метод .becomes определяется как в основном используемый для решения проблем STI, таких как ваш form_for один.

.becomes info здесь: http://apidock.com/rails/ActiveRecord/Base/becomes

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

Ответ 7

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

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

Я реализовал подход, который поддерживает создание экземпляров подклассов модели и является SAFE из вышеперечисленной проблемы. Он очень похож на то, что делает рельсы 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)

После того, как вы попытались использовать различные подходы к загрузке sublclass в проблеме 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" для 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 resource_request_name как вспомогательный метод для представления:

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

Всегда рады услышать о других впечатлениях и улучшениях.

Ответ 8

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

Этот подход охватывает проблемы, которые вы перечисляете, а также ряд других проблем, которые другие имели с подходами STI.

Ответ 9

Вы можете попробовать это, если у вас нет вложенных маршрутов:

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

Или вы можете пойти другим путем и использовать некоторую ООП-магию, как описано здесь: https://coderwall.com/p/yijmuq

Во-вторых, вы можете создавать похожие помощники для всех ваших вложенных моделей.

Ответ 10

Вот безопасный чистый способ заставить его работать в формах и во всем используемом нами приложении.

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|

Надеюсь, что это поможет.

Ответ 11

Если я считаю наследование STI следующим:

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

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

Ответ 12

Этот способ работает для меня нормально (определите этот метод в базовом классе):

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

Ответ 13

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

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

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

Ответ 14

взломать, но еще один в список решений.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

работает с рельсами 2.x и 3.x