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


175

私の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再び、二人が使用することをお勧めroutes.rb(親クラスにサブクラスのリソースをマッピングするためにmap.resources :employees, :controller => 'people')。同じSOの質問の一番上の答えは、コードベースのすべてのインスタンスオブジェクトを使用して型キャストすることを示唆しています.becomes
  3. StackOverflowのもう1つの方法は、トップの答えはDo Repeat Yourselfキャンプでの方法であり、すべてのサブクラスに重複する足場を作成することを提案しています。
  4. ここでだ同じ質問がトップの答えはちょうど間違っているように思わSO、再び(Railsの魔法だけで動作します!)
  5. Webの他の場所で、F2Andyがコード内のすべてのパスで編集することを推奨しているこのブログ投稿を見つけました。
  6. ブログ投稿「単一テーブルの継承と論理的現実の設計でのRESTfulルート」では、上記のSOの回答番号2のように、サブクラスのリソースをスーパークラスコントローラーにマップすることをお勧めします。
  7. Alex Reisnerが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
Alex Reisnerのコードにタイプミスがありましたが、私が彼のブログにコメントした後に修正されました。うまくいけば、今アレックスの解決策は実行可能です。私の質問はまだ残っています:正しい解決策はどれですか?
ジググリズム2010

1
約3年前ですが、このブログの投稿はrookieonrails.blogspot.com/2008/01/…にあり、railsメーリングリストからのリンクされた会話も参考になりました。レスポンダーの1つは、ポリモーフィックヘルパーと名前付きヘルパーの違いについて説明しています。
ジググリズム2010

2
リストしないオプションは、Railsにパッチを適用して、link_to、form_forなどを単一のテーブル継承で適切に配置することです。それは大変な仕事かもしれませんが、修正してもらいたいと思います。
M.スコットフォード

回答:


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%確信できないことです。
Prathan Thananart 2012

10
これは、クラスに応じて属性に異なる人名を付けようとすると、i18nを壊します。
Rufo Sanchez

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
これは機能しますが、ルートに多くの不要なパスを追加します。これをより煩わしくない方法で行う方法はありませんか?
Anders Kindberg、2011

1
ルートをroutes.rbファイルでプログラム的に設定できるため、小さなメタプログラミングを実行して子ルートを設定できます。ただし、クラスがキャッシュされていない環境(開発など)では、最初にクラスをプリロードする必要があります。したがって、いずれかの方法でサブクラスをどこかに指定する必要があります。例については、gist.github.com / 1713398を参照してください。
Chris Bloom

私の場合、オブジェクト名(パス)をユーザーに公開することは望ましくありません(ユーザーを混乱させることにもなります)。
laffuste 2014年

33

このメソッドを使用すると、「form_for」を使用できるようになります。https//stackoverflow.com/a/605172/445908をご覧ください

ActiveRecord::Base#becomes

2
from 適切に保存するために、urlを明示的に設定する必要がありました。<%= form_for @child, :as => :child, url: @child.becomes(Parent)
lulalala

4
@lulalala試してみる<%= 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方法は、主にあなたのようなSTI問題を解決するために使用されると定義されていますform_for

.becomesこちらの情報:http : //apidock.com/rails/ActiveRecord/Base/becomes

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


5

OK、IveはRailsのこの領域に非常に不満を感じており、次のアプローチにたどり着きました。おそらくこれは他の人を助けるでしょう。

まず、ネット上およびネット上の多くのソリューションが、クライアントが提供するパラメーターでconstantizeを使用することを提案していることに注意してください。Rubyはシンボルのガベージコレクションを行わないため、これは既知のDoS攻撃ベクトルであり、攻撃者が任意のシンボルを作成して利用可能なメモリを消費することを可能にします。

私は、モデルサブクラスのインスタンス化をサポートする以下のアプローチを実装しました。これは、上記のインスタンス化問題の安全です。これは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をオーバーライドしません。

上記のように、タイプを設定してルートマッピングも使用します。

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

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


私の経験では(少なくともRails 3.0.9では)、ストリングで指定された定数がまだ存在しない場合、constantizeは失敗します。それでは、それを使用して任意の新しいシンボルを作成する方法を教えてください。
Lex Lindsey

4

最近、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アプローチに関して持っていた他の多くの問題を回避します。


2

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

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

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

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


2

これは、フォームおよびアプリケーション全体で機能させる安全でクリーンな方法です。

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

それから私は自分の姿をしています。このために追加された部分は、:: districtです。

= 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

私がこのような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

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

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

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

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[:contact]params[:contact_person]、の代わりにparamsを送信するようになりますparams[:contact_whatever]


-6

ハックですが、ソリューションのリストのもう1つです。

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

レール2.xおよび3.xで動作します


5
これにより1つの問題は修正されますが、別の問題が発生します。これを実行しようとするChild.newParent、サブクラスではなくクラスが返されます。これはtype、type属性を明示的に設定しない限り、コントローラーを介した一括割り当てを介してサブクラスを作成できないことを意味します(デフォルトでは保護属性であるため)。
Chris Bloom
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.