【问题标题】:How can I access an ActiveRecord grandparent association through a parent association that has not yet been saved?如何通过尚未保存的父关联访问 ActiveRecord 祖父关联?
【发布时间】:2013-10-23 21:53:38
【问题描述】:

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

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;虽然我希望 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

【问题讨论】:

  • 多么奇怪。我必须稍后再试一次。昨晚工作得很愉快。检查empl.association(:department) == Employee.new.association(:department)...
  • 该表达式的结果是false。我看到关联上有一个实例变量@target,它似乎总是nil(即使是保存的记录)。我不知道@target 应该是什么,但它包含我想要的参考对我来说是有意义的。

标签: ruby-on-rails ruby activerecord


【解决方案1】:

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

来自Bi-directional associations 文档中的ActiveRecord::Associations::ClassMethods

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

【讨论】:

  • inverse_of 非常适合使用 accepts_nested_attributes_for 在嵌套表单上进行自定义验证,例如 ActiveModel::EachValidator,当您需要从子级访问 has_many 关系的父级但父级或子级都不是保存。没有它,child.parent 将是 nil
【解决方案2】:

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

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

事实证明,您可以传递一个块来构建,ActiveRecord 将让步给具有新创建记录的块。谁知道这样会带来什么 ActiveRecord 怪异。我暂时保留这个问题,看看是否有更好的解决方案。

【讨论】:

    【解决方案3】:

    在一些控制台实验之后——你可以说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的点,我们可以在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 正在检查invertible_for?(record),(其中record 是我们的新Employee 实例。)这只是调用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,等等。那么为什么这里不是 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 找到它?

    【讨论】:

    • 所以,我在实际尝试后改变了这个答案:)
    • 嗯。不适合我。你用什么版本的 Rails 试过?
    • 那是 Rails 4。也就是说,在 Rails 4 中 ActiveRecord 的“显着变化”中没有提到这一点,所以它应该在 3 中可用。我没有 Rails 3手头的环境,否则我会试一试。看看你从employee.association(:department) 得到了什么,看看引用是否被埋在那里。
    • 我也完全尝试了您在 Rails 4 中显示的内容。 e.department 为 nil,并且 e.department.company 失败,正如您所期望的 nil 那样。您是否对类定义进行了任何更改?
    • 无——我只是将它们放到上面的控制台中,然后继续执行这些命令。 (我不得不猜测这些表的外观,但那里并没​​有什么特别之处。)稍后我会在使用笔记本电脑时分享我的迁移,但实际上——它们是最普通的。
    猜你喜欢
    • 1970-01-01
    • 2016-07-23
    • 2019-05-25
    • 2011-06-20
    • 1970-01-01
    • 2023-03-16
    • 2016-08-24
    • 2019-08-06
    • 2011-06-03
    相关资源
    最近更新 更多