web-dev-qa-db-ja.com

railsでSTIサブクラスのルートを処理するためのベストプラクティス

Railsビューとコントローラーには、redirect_tolink_to、およびform_forメソッド呼び出しが散らばっています。 link_toredirect_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 で再び、2人はroutes.rbを使用してサブクラスリソースを親クラス(map.resources :employees, :controller => 'people')にマップすることを提案します。同じSO質問の一番上の答えは、.becomesを使用してコードベース内のすべてのインスタンスオブジェクトを型キャストすることを示唆しています
  3. StackOverflow のもう1つの答えは、Do Repeat Yourselfキャンプの方法であり、すべてのサブクラスに対して重複する足場を作成することをお勧めします。
  4. ここにあります SOでも同じ質問です。トップの答えは間違っているようです(Rails magic Just Works!)
  5. Webのその他の場所で このブログ投稿 が見つかりました。F2Andyは、コード内のあらゆる場所での編集を推奨しています。
  6. ブログ投稿で Single Table Inheritance and RESTful Routes Logical Reality Designで、上記のSO回答番号2のように、サブクラスのリソースをスーパークラスコントローラーにマップすることをお勧めします。
  7. Alex Reisnerの投稿 Railsの単一テーブル継承 では、routes.rblink_toからのルーティングの破損のみをキャッチするため、redirect_toの子クラスのリソースを親クラスにマッピングすることを推奨していません。 form_forから。そのため、代わりに親クラスにメソッドを追加して、サブクラスがそのクラスについて嘘をつくようにすることをお勧めします。良さそうに聞こえますが、彼の方法ではエラーundefined local variable or method `child' for #が発生しました。

したがって、最もエレガントであるように思われ、最もコンセンサスを持っている答え(しかし、それはすべてthatエレガントでもなく、that多くのコンセンサス)、routes.rbにリソースを追加します。これはform_forでは機能しません。明確にする必要があります!上記の選択肢を抽出するために、私のオプションは

  1. routes.rbのサブクラスのリソースをスーパークラスのコントローラーにマップします(サブクラスでform_forを呼び出す必要がないことを願っています)
  2. Rails内部メソッドをオーバーライドして、クラスが相互に関係するようにします
  3. オブジェクトのアクションへのパスが暗黙的または明示的に呼び出されるコード内のすべてのインスタンスを編集します。パスを変更するか、オブジェクトを型キャストします。

これらすべての矛盾する答えがあるため、裁定が必要です。良い答えがないように思えます。これはRailsの設計で失敗していますか?もしそうなら、それは修正されるかもしれないバグですか?または、そうでない場合は、誰かが私にこれをまっすぐに設定し、各オプションの長所と短所を説明して(またはそれがオプションではない理由を説明し)、正しい答えと理由を説明してください。または、ウェブ上で見つけられない正しい答えはありますか?

168
ziggurism

これは、最小限の副作用で思い付いた最も簡単なソリューションです。

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

これで、url_for @personは期待どおりcontact_pathにマップされます。

仕組み: URLヘルパーはYourModel.model_nameに依存してモデルに反映し、(多くのことの中で)単一/複数のルートキーを生成します。ここでPersonは基本的に私はContactのようだ、彼に聞いてくださいと言っています。

130

同じ問題がありました。 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で送信した後、私にとってはうまくいくようです。

46
James

https://stackoverflow.com/a/605172/445908 をご覧になることをお勧めします。この方法を使用すると、「form_for」を使用できます。

ActiveRecord::Base#becomes
31

ルートでtypeを使用します。

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

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

17
jobwat

@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にマップされます。

13
eloyesp

私もこの問題に苦労していたので、私たちの質問に似た質問でこの答えを見つけました。それは私のために働いた。

form_for @list.becomes(List)

ここに示されている回答: 同じコントローラーでSTIパスを使用

.becomesメソッドは、主にform_for 1。

.becomes info here: http://apidock.com/Rails/ActiveRecord/Base/becomes

非常に遅い応答ですが、これは私が見つけることができる最高の答えであり、私にとってはうまくいきました。これが何らかの助けになることを願っています。乾杯!

11
Marcus

わかりました、IveはRailsのこの分野で多くの不満を抱いており、次のアプローチにたどり着きました。おそらくこれは他の人を助けるでしょう。

最初に、ネット上のさまざまなソリューションが、クライアントが提供するパラメーターで定数化を使用することを提案していることに注意してください。 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)

上記の提案によく似た「開発問題でのサブクラスのロード」にさまざまなアプローチを試みた後、確実に機能する唯一のことは、モデルクラスで「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を使用し、異なるサブクラスの属性に異なる文字列が必要なため、上記のようにmodel_nameをオーバーライドしません。たとえば、:tax_identifierはOrganisationでは 'ABN'になり、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

他の人の経験や改善を聞いていつも幸せです。

5
Andrew Hacking

最近 ドキュメント化 Rails 3.0アプリで動作する安定したSTIパターンを取得しようとしています。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アプローチで抱えていた他の多くの問題を回避します。

4
Chris Bloom

ネストされたルートがない場合、これを試すことができます:

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

または、別の方法で、ここで説明するようなOOPマジックを使用することもできます。 https://coderwall.com/p/yijmuq

2番目の方法では、ネストされたすべてのモデルに対して同様のヘルパーを作成できます。

2
everm1nd

ここでは、使用するフォームやアプリケーション全体で安全に機能する安全な方法を紹介します。

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

それから私は私の形で持っています。このために追加された部分は、as::districtです。

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

お役に立てれば。

2
Jake

この方法は私にはうまくいきます(基本クラスでこのメソッドを定義します):

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
ka8725

次のような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

したがって、デフォルトのモデルを使用するモデルを簡単に選択することもできます。これは、サブクラスの定義に触れなくても実行できます。とても乾いた。

1
Laurent B.

ルーティング目的のためにダミーの親オブジェクトを返すメソッドを作成できます

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

そして、型なしでPersonクラスオブジェクトを返すform_for @ employee.routing_objectを呼び出すだけです

1
mingle

@ prathan-thananartの回答に従い、複数のSTIクラスについては、親モデルに次を追加できます->

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

これにより、params[:contact]params[:contact_person]の代わりにparams[:contact_whatever]としてパラメーターを送信する連絡先データを含む各フォームが作成されます。

0