如何通过尚未保存的父关联访问ActiveRecord祖父母关联?

我有一种情况,我希望在保存父对象之前访问相关的祖父母。 我可以想到几个黑客,但我正在寻找一种干净的方法来实现这一目标。 以下代码作为我的问题的说明:

class Company  :departments end class Department < ActiveRecord::Base belongs_to :company has_many :employees end class Employee  :department end company = Company.find(1) # =>  dept = company.departments.build # =>  empl = dept.employees.build # =>  empl.company # => Employee#company delegated to department.company, but department is nil 

我正在使用Rails 3.2.15。 我理解这里发生了什么,我理解为什么empl.department_id是零; 虽然我希望Rails在调用save之前直接引用预期关联,这样最后一行可以通过未保存的部门对象委派。 有干净的工作吗?

更新:我在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]] => # 2.0.0-p247 :002 > dept = company.departments.build => # 2.0.0-p247 :003 > empl = dept.employees.build => # 2.0.0-p247 :004 > empl.company RuntimeError: Employee#company delegated to department.company, but department is nil: # 2.0.0-p247 :005 > empl.department => nil 

更新2:这是github上的测试项目 。

请查看belongs_tohas_many:inverse_of选项。 在不同情况下构建和获取关联记录时,此选项将处理双向分配。

来自文档中的ActiveRecord::Associations::ClassMethods 的双向关联 :

在关联上指定:inverse_of选项可以告诉Active Record关于反向关系,它将优化对象加载。

我不喜欢这个解决方案,但这似乎解决了这个问题:

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

事实certificate,您可以传递一个块来构建,ActiveRecord将使用新创建的记录生成块。 谁知道ActiveRecord会带来什么样的怪异。 我现在打开这个问题,看看是否有更好的解决方案。

在一些控制台实验之后 – 你可以说employee.department.company ,即使部门尚未保存。 department_id可能是零,但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 => # 2.0.0-p195 :042 > d = c.departments.build => # 2.0.0-p195 :043 > e = d.employees.build => # 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) => # 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的设置点,我们可以在ActiveRecord::Associations::Association拦截target=

 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正在检查set_inverse_instance invertible_for?(record) ,(其中record是我们新的Employee实例。)这只是调用reflection.inverse_of ,这必须返回一个truthy值才能设置目标。

 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 => # 

那是非零的,所以当我调用d.employee.build时,@ target将在我的关联中设置,所以我可以调用e.department ,依此类推。 那么为什么它在这里是非零的,但对你来说是零(在我的另一台机器上呢?)如果我调用Employee.reflections ,我得到以下内容:

 > Employee.reflections => {:department=>#} 

这是belongs_to方法的产物 - 如果你看,它必须在那里。 那么为什么(在你的情况下)没有set_inverse_instance找到它?