6

親オブジェクトが保存される前に、関連する祖父母にアクセスしたい状況があります。いくつかのハックを思いつくことができますが、これを達成するためのクリーンな方法を探しています。私の問題の実例として、次のコードを取り上げます。

class Company < ActiveRecord::Base
  has_many :departments
  has_many :custom_fields
  has_many :employees, :through => :departments
end
class Department < ActiveRecord::Base
  belongs_to :company
  has_many :employees
end
class Employee < ActiveRecord::Base
  belongs_to :department
  delegate :company, :to => :department
end

company = Company.find(1)           # => <Company id: 1>
dept = company.departments.build    # => <Department id: nil, company_id: 1>
empl = dept.employees.build         # => <Employee id: nil, department_id: nil>

empl.company   # => Employee#company delegated to department.company, but department is nil

Rails 3.2.15 を使用しています。ここで何が起こっているかを理解しています。また、empl.department_id が nil である理由も理解しています。ただし、save を呼び出す前に、Rails が将来の関連付けへの直接参照を保持して、保存されていない部門オブジェクトを介して最後の行を委任できるようにしてほしいと思います。クリーンな回避策はありますか?

更新: Rails 4 でもこれを試しました。コンソール セッションは次のとおりです。

2.0.0-p247 :001 > company = Company.find(1)
  Company Load (1.5ms)  SELECT "companies".* FROM "companies" WHERE "companies"."id" = ? LIMIT 1  [["id", 1]]
 => #<Company id: 1, name: nil, created_at: "2013-10-24 03:36:11", updated_at: "2013-10-24 03:36:11"> 
2.0.0-p247 :002 > dept = company.departments.build
 => #<Department id: nil, name: nil, company_id: 1, created_at: nil, updated_at: nil> 
2.0.0-p247 :003 > empl = dept.employees.build
 => #<Employee id: nil, name: nil, department_id: nil, created_at: nil, updated_at: nil> 
2.0.0-p247 :004 > empl.company
RuntimeError: Employee#company delegated to department.company, but department is nil: #<Employee id: nil, name: nil, department_id: nil, created_at: nil, updated_at: nil>
2.0.0-p247 :005 > empl.department
 => nil 

更新 2: これはgithub のテスト プロジェクトです。

4

3 に答える 3

5

との:inverse_ofオプションを調べてください。このオプションは、さまざまなケースで関連するレコードを作成および取得するときに、双方向の割り当てを処理します。belongs_tohas_many

ドキュメントの双方向の関連付けから:ActiveRecord::Associations::ClassMethods

関連付けのオプションを指定する:inverse_ofと、アクティブ レコードに逆の関係を伝えることができ、オブジェクトの読み込みが最適化されます。

于 2013-10-24T19:24:19.367 に答える
1

私はこの解決策が好きではありませんが、これは問題を解決するようです:

empl = dept.employees.build { |e| e.association(:department).target = dept}

構築するブロックを渡すことができることが判明し、ActiveRecord は新しく作成されたレコードを含むブロックに譲ります。そのような ActiveRecord の奇妙さがもたらすものは誰にもわかりません。より良い解決策があるかどうかを確認するために、今のところ質問を開いたままにしています。

于 2013-10-24T18:50:19.917 に答える
0

employee.department.companyコンソールでいくつかの実験を行った後、部門がまだ保存されていなくても、単に と言うことができます。department_idnil かもしれませんが、department関連性はあります。

2.0.0-p195 :041 > c = Company.create 
   (0.4ms)  begin transaction
  SQL (0.9ms)  INSERT INTO "companies" DEFAULT VALUES
   (486.4ms)  commit transaction
 => #<Company id: 4, department: nil, custom_fields: nil> 
2.0.0-p195 :042 > d = c.departments.build
 => #<Department id: nil, company_id: 4, employee_id: nil> 
2.0.0-p195 :043 > e = d.employees.build
 => #<Employee id: nil, department_id: nil> 
2.0.0-p195 :044 > e.department === d
 => true 
2.0.0-p195 :045 > e.department.company === c
 => true

編集:したがって、これは別のクリーンなRails 4アプリを備えた別のマシンでは機能しませんでした。ただし、私のラップトップではまだ動作します...クリーンなRails 4アプリでも動作します。何が違うのか考えてみましょう!

e.method(:department)
=> #<Method: Employee(Employee::GeneratedFeatureMethods)#department> 

e.method(:department).source_location
=> ["/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord-  
    4.0.0/lib/active_record/associations/builder/association.rb", 69] 

ここで私たちを導きます:

def define_readers
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
    def #{name}(*args)
      association(:#{name}).reader(*args)
    end
  CODE
end

本当に驚くことではありません。これは、と呼ばれるメソッドを定義します:department

def department *args
  association(:department).reader(*args)
end

への呼び出しreaderは、アソシエーションの @target が存在する場合はそれを返し、ID が手元にある場合はそれを読み取ろうとします。私の場合、@targetは department に設定されていdます。@target が設定されているポイントを見つけるために、次のようにインターセプトできtarget=ますActiveRecord::Associations::Association

class ActiveRecord::Associations::Association 
  alias :_target= :target=
  def target= t
    puts "#{caller} set the target!"
    _target = t
  end
end

今、私たちが呼び出すd.employees.buildと、これが得られます...

"/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord-4.0.0/lib/active_record/associations/association.rb:112:in `set_inverse_instance'", 
"/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord-4.0.0/lib/active_record/associations/collection_association.rb:376:in `add_to_target'",
"/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord-4.0.0/lib/active_record/associations/collection_association.rb:114:in `build'"

set_inverse_instanceをチェックしています (新しい Employee インスタンスはinvertible_for?(record)ここにあります)。これは を呼び出すだけであり、ターゲットを設定するには真の値を返す必要があります。recordreflection.inverse_of

def inverse_of
  return unless inverse_name

  @inverse_of ||= klass.reflect_on_association inverse_name
end

それでは、試してみましょう...

2.0.0-p195 :055 > Employee.reflect_on_association :department
 => #<ActiveRecord::Reflection::AssociationReflection:0xa881788 @macro=:belongs_to, @name=:department, @scope=nil, @options={}, @active_record=Employee(id: integer, department_id: integer), @plural_name="departments", @collection=false, @class_name="Department", @foreign_key="department_id"> 

これは nil ではないので、 を呼び出すと @target がアソシエーションに設定されるため、 を呼び出すd.employee.buildことができますe.department。では、なぜここでは nil ではないのに、あなたにとっては nil なのですか (そして私の他のマシンでは?) を呼び出すとEmployee.reflections、次の結果が得られます。

> Employee.reflections
 => {:department=>#<ActiveRecord::Reflection::AssociationReflection:0x9a04598 @macro=:belongs_to, @name=:department, @scope=nil, @options={}, @active_record=Employee(id: integer, department_id: integer), @plural_name="departments", @collection=false, @class_name="Department", @foreign_key="department_id">} 

これはbelongs_to方法の産物です。見ればそこにあるに違いありません。では、なぜ(あなたの場合)それがset_inverse_instance見つからないのですか?

于 2013-10-23T22:09:09.177 に答える