Лучшие практики для обработки маршрутов для подклассов STI в рельсах - PullRequest
168 голосов
/ 22 декабря 2010

Представления и контроллеры 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 ищет маршрут, определенный именем класса объекта-сотрудника. Эти маршруты сотрудников не определены, и нет контроллера сотрудников, поэтому действия также не определены.

Этот вопрос задавался ранее:

  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 в Logical Reality Design рекомендуется сопоставлять ресурсы для подкласса с контроллером суперкласса, как в ответе 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. Переопределить внутренние методы rails, чтобы классы лгали друг другу
  3. Редактировать каждый экземпляр в коде, где путь к действию объекта вызывается неявно или явно, либо изменяя путь, либо приводя тип к объекту.

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

Ответы [ 15 ]

130 голосов
/ 27 февраля 2012

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

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

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

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

46 голосов
/ 23 февраля 2011

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

31 голосов
/ 17 апреля 2012

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

ActiveRecord::Base#becomes
17 голосов
/ 15 мая 2014

Используйте введите в маршрутах:

resources :employee, controller: 'person', type: 'Employee' 

http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/

13 голосов
/ 30 октября 2012

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

11 голосов
/ 31 декабря 2016

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

form_for @list.becomes(List)

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

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

.becomes информация здесь: http://apidock.com/rails/ActiveRecord/Base/becomes

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

5 голосов
/ 12 июня 2013

Хорошо, у меня было множество разочарований в этой области 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'

Я также не переопределяю имя_модели, как предложено выше, потому что я использую 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

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

4 голосов
/ 01 февраля 2012

Я недавно задокументировал мои попытки получить стабильный шаблон 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 голосов
/ 21 июня 2016

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

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 голосов
/ 20 ноября 2013

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

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

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

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

...