181

Rails のビューとコントローラーにはredirect_to、 、link_to、およびform_forメソッド呼び出しが散らばっています。とは、リンクしているパスで明示的であることもlink_toありますが (例: )、多くの場合、パスは暗黙的です (例: )。redirect_tolink_to 'New Person', new_person_pathlink_to 'Show', person

モデルにいくつかの単一テーブル継承 (STI) を追加すると (たとえばEmployee < Person)、これらのメソッドはすべてサブクラスのインスタンスに対して壊れます (たとえばEmployee)。rails を実行するlink_to @personと、 でエラーが発生しundefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>ます。Rails は、employee であるオブジェクトのクラス名によって定義されたルートを探しています。これらの従業員ルートは定義されておらず、従業員コントローラーがないため、アクションも定義されていません。

この質問は以前に尋ねられました:

  1. StackOverflowでの答えは、コードベース全体で link_to などのすべてのインスタンスを編集し、パスを明示的に記述することです
  2. 再びStackOverflowroutes.rbで、サブクラスのリソースを親クラスにマップするために使用することを2人が提案しています ( map.resources :employees, :controller => 'people')。同じSOの質問の一番の答えは、コードベース内のすべてのインスタンスオブジェクトを型キャストすることを提案しています.becomes
  3. StackOverflowでのさらに別の回答では、一番の答えは Do Repeat Yourself キャンプの方法であり、すべてのサブクラスに複製の足場を作成することを提案しています。
  4. ここでも同じ質問が SO にあり、一番上の答えが間違っているようです (Rails マジック Just Works!)。
  5. Web の他の場所で、F2Andy がコード内のあらゆる場所でパスを編集することを推奨しているこのブログ投稿を見つけました。
  6. ブログ記事「Single Table Inheritance and RESTful Routes at Logical Reality Design」では、上記の SO の回答 2 のように、サブクラスのリソースをスーパークラス コントローラーにマップすることをお勧めします。
  7. Alex Reisner は、Single Table Inheritance in Railsの記事を投稿しています。その中で彼は、子クラスのリソースを親クラスにマップすることに反対することを提唱していroutes.rbます。そのため、代わりに、親クラスにメソッドを追加して、サブクラスがそのクラスについて嘘をつくようにすることをお勧めします。良さそうですが、彼の方法ではエラーが発生しました。link_toredirect_toform_forundefined local variable or method `child' for #

したがって、最もエレガントに見え、最もコンセンサスが得られる答えは (ただし、それほどエレガントではなく、コンセンサスもそれほど多くありません)、リソースを に追加することroutes.rbです。ただし、これは では機能しませんform_for。私はいくつかの明確さが必要です!上記の選択肢を要約すると、私のオプションは次のとおりです。

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

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

4

18 に答える 18

143

これは、最小限の副作用で思いついた最も簡単な解決策です。

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

url_for @person期待どおりにマップされるようになりましcontact_pathた。

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

于 2012-02-27T10:31:39.313 に答える
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です。

于 2011-02-23T07:32:14.780 に答える
35

https://stackoverflow.com/a/605172/445908をご覧になることをお勧めします。このメソッドを使用すると、「form_for」を使用できるようになります。

ActiveRecord::Base#becomes
于 2012-04-17T00:18:09.467 に答える
18

ルートでタイプを使用します。

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

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

于 2014-05-15T08:29:36.310 に答える
16

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

于 2012-10-30T00:59:33.483 に答える
16

私もこの問題に問題があり、私たちと同様の質問でこの回答にたどり着きました。それは私のために働いた。

form_for @list.becomes(List)

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

この.becomes方法は、主にあなたのようなSTIの問題を解決するために使用されると定義されていますform_for

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

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

于 2016-12-31T13:25:11.300 に答える
5

わかりました、私は Rails のこの分野で多くのフラストレーションを抱えていましたが、次のアプローチにたどり着きました。おそらくこれは他の人を助けるでしょう。

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

モデルのサブクラスのインスタンス化をサポートする以下のアプローチを実装しました。これは上記の contantize の問題から安全です。これは 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 は組織の場合は「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 の 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

他の人の経験や改善点を聞くことは常に幸せです.

于 2013-06-12T02:52:20.530 に答える
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

このアプローチは、あなたが列挙した問題だけでなく、他の人が STI アプローチで抱えていた他の多くの問題を回避します。

于 2012-02-01T09:04:56.027 に答える
2

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

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

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

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

于 2013-11-20T17:33:29.890 に答える
2

@prathan-thananart answerに従って、複数の 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]

于 2019-04-04T15:01:21.407 に答える
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

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

于 2013-04-27T09:35:30.663 に答える
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
于 2013-05-21T14:57:28.750 に答える
0

オーバーライドmodel_nameは危険なようです。を使用.becomesする方が安全なオプションのようです。

問題の 1 つは、扱っているモデル (つまり、基本モデル) がわからない場合です。

そのような場合に使用できることを共有したかっただけです:

foo.becomes(foo.class.base_class)

使いやすくするために、このメソッドを my に追加しましたApplicationRecord:

def becomes_base
  becomes(self.class.base_class)
end

いくつかのルート ヘルパー メソッドに追加.becomes_baseすることは、私には大したことではないように思えます。

于 2020-09-04T08:33:35.490 に答える
-6

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

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

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

于 2012-01-23T13:52:53.913 に答える