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は、従業員であるオブジェクトのクラス名で定義されたルートを探しています。これらの従業員ルートは定義されておらず、従業員管理者もいないため、アクションも定義されていません。
この質問は以前に尋ねられました:
routes.rb
を使用してサブクラスリソースを親クラス(map.resources :employees, :controller => 'people'
)にマップすることを提案します。同じSO質問の一番上の答えは、.becomes
を使用してコードベース内のすべてのインスタンスオブジェクトを型キャストすることを示唆していますroutes.rb
とlink_to
からのルーティングの破損のみをキャッチするため、redirect_to
の子クラスのリソースを親クラスにマッピングすることを推奨していません。 form_for
から。そのため、代わりに親クラスにメソッドを追加して、サブクラスがそのクラスについて嘘をつくようにすることをお勧めします。良さそうに聞こえますが、彼の方法ではエラーundefined local variable or method `child' for #
が発生しました。したがって、最もエレガントであるように思われ、最もコンセンサスを持っている答え(しかし、それはすべてthatエレガントでもなく、that多くのコンセンサス)、routes.rb
にリソースを追加します。これはform_for
では機能しません。明確にする必要があります!上記の選択肢を抽出するために、私のオプションは
routes.rb
のサブクラスのリソースをスーパークラスのコントローラーにマップします(サブクラスでform_forを呼び出す必要がないことを願っています)これらすべての矛盾する答えがあるため、裁定が必要です。良い答えがないように思えます。これはRailsの設計で失敗していますか?もしそうなら、それは修正されるかもしれないバグですか?または、そうでない場合は、誰かが私にこれをまっすぐに設定し、各オプションの長所と短所を説明して(またはそれがオプションではない理由を説明し)、正しい答えと理由を説明してください。または、ウェブ上で見つけられない正しい答えはありますか?
これは、最小限の副作用で思い付いた最も簡単なソリューションです。
class Person < Contact
def self.model_name
Contact.model_name
end
end
これで、url_for @person
は期待どおりcontact_path
にマップされます。
仕組み: URLヘルパーはYourModel.model_name
に依存してモデルに反映し、(多くのことの中で)単一/複数のルートキーを生成します。ここでPerson
は基本的に私はContact
のようだ、彼に聞いてくださいと言っています。
同じ問題がありました。 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
で送信した後、私にとってはうまくいくようです。
https://stackoverflow.com/a/605172/445908 をご覧になることをお勧めします。この方法を使用すると、「form_for」を使用できます。
ActiveRecord::Base#becomes
ルートでtypeを使用します。
resources :employee, controller: 'person', type: 'Employee'
http://samurails.com/tutorial/single-table-inheritance-with-Rails-4-part-2/
@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にマップされます。
私もこの問題に苦労していたので、私たちの質問に似た質問でこの答えを見つけました。それは私のために働いた。
form_for @list.becomes(List)
ここに示されている回答: 同じコントローラーでSTIパスを使用
.becomes
メソッドは、主にform_for
1。
.becomes
info here: http://apidock.com/Rails/ActiveRecord/Base/becomes
非常に遅い応答ですが、これは私が見つけることができる最高の答えであり、私にとってはうまくいきました。これが何らかの助けになることを願っています。乾杯!
わかりました、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
他の人の経験や改善を聞いていつも幸せです。
最近 ドキュメント化 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アプローチで抱えていた他の多くの問題を回避します。
ネストされたルートがない場合、これを試すことができます:
resources :employee, path: :person, controller: :person
または、別の方法で、ここで説明するようなOOPマジックを使用することもできます。 https://coderwall.com/p/yijmuq
2番目の方法では、ネストされたすべてのモデルに対して同様のヘルパーを作成できます。
ここでは、使用するフォームやアプリケーション全体で安全に機能する安全な方法を紹介します。
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|
お役に立てれば。
この方法は私にはうまくいきます(基本クラスでこのメソッドを定義します):
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
次のような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
したがって、デフォルトのモデルを使用するモデルを簡単に選択することもできます。これは、サブクラスの定義に触れなくても実行できます。とても乾いた。
ルーティング目的のためにダミーの親オブジェクトを返すメソッドを作成できます
class Person < ActiveRecord::Base
def routing_object
Person.new(id: id)
end
end
そして、型なしでPersonクラスオブジェクトを返すform_for @ employee.routing_objectを呼び出すだけです
@ 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]
としてパラメーターを送信する連絡先データを含む各フォームが作成されます。