我有一种情况,我希望在保存父对象之前访问相关的祖父母。我可以想到几个黑客,但我正在寻找一种干净的方法来实现这一目标。以下代码为例说明我的问题:
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是零;虽然我希望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]]
=> #<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:这是test project on github。
答案 0 :(得分:5)
请查看belongs_to
和has_many
的:inverse_of
选项。在不同情况下构建和获取关联记录时,此选项将处理双向分配。
来自文档中ActiveRecord::Associations::ClassMethods
的{{3}}:
在关联上指定
:inverse_of
选项可让您告诉Active Record有关反向关系的信息,它将优化对象加载。
答案 1 :(得分:1)
我不喜欢这个解决方案,但这似乎解决了这个问题:
empl = dept.employees.build { |e| e.association(:department).target = dept}
事实证明,您可以传递一个块来构建,ActiveRecord将使用新创建的记录生成块。谁知道ActiveRecord会带来什么怪异。我现在打开这个问题,看看是否有更好的解决方案。
答案 2 :(得分:0)
经过一些控制台实验 - 您可以说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
=> #<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
设置为部门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
正在检查invertible_for?(record)
,(其中record
是我们的新员工实例。)这只是调用reflection.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">
那是非零的,所以当我拨打d.employee.build
时,@ target会在我的关联中设置,所以我可以拨打e.department
,依此类推。那么为什么它在这里是非零的,但对你来说是零(在我的另一台机器上呢?)如果我打电话给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
找到它?