【问题标题】:Rails/ActiveRecord has_many through: association on unsaved objectsRails/ActiveRecord has_many through:对未保存对象的关联
【发布时间】:2014-12-26 10:10:44
【问题描述】:

让我们使用这些类:

class User < ActiveRecord::Base
    has_many :project_participations
    has_many :projects, through: :project_participations, inverse_of: :users
end

class ProjectParticipation < ActiveRecord::Base
    belongs_to :user
    belongs_to :project

    enum role: { member: 0, manager: 1 }
end

class Project < ActiveRecord::Base
    has_many :project_participations
    has_many :users, through: :project_participations, inverse_of: :projects
end

user 可以以membermanager 的角色参与许多projects。连接模型称为ProjectParticipation

我现在在使用未保存对象的关联时遇到问题。以下命令的工作方式与我认为的一样:

# first example

u = User.new
p = Project.new

u.projects << p

u.projects
=> #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil>]>

u.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil>]>

到目前为止一切顺利 - AR 自己创建了ProjectParticipation,我可以使用u.projects 访问userprojects

但如果我自己创建ProjectParticipation 则不起作用:

# second example

u = User.new
pp = ProjectParticipation.new
p = Project.new

pp.project = p # assign project to project_participation

u.project_participations << pp # assign project_participation to user

u.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil>]>

u.projects
=> #<ActiveRecord::Associations::CollectionProxy []>

为什么项目是空的?我无法像以前一样通过u.projects 访问项目。

但如果我直接通过参与,项目就会出现:

u.project_participations.map(&:project)
=> [#<Project id: nil>]

它不应该像第一个示例一样直接工作吗:u.projects 将所有项目返回给我,而不取决于我是否自己创建连接对象?或者我怎样才能让 AR 意识到这一点?

【问题讨论】:

    标签: ruby-on-rails activerecord has-many-through


    【解决方案1】:

    冒着过于简单化的风险,让我试着解释一下发生了什么

    大多数其他答案试图告诉您的是,这些对象尚未通过活动记录链接,直到它们被持久保存在数据库中。因此,您所期望的关联行为并未完全连接起来。

    请注意,您的第一个示例中的这一行

     u.project_participations
     => #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil>]>
    

    与第二个示例的结果相同

    u.project_participations
    => #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil,   project_id: nil, role: nil>]>
    

    您对您认为 rails 正在做的事情的分析中的以下陈述是不准确的:

    到目前为止一切顺利 - AR 自己和我创建了 ProjectParticipation 可以使用 u.projects 访问用户的项目。

    AR 记录尚未创建 ProjectParticipation。您已经在模型中声明了这种关系。 AR 只是返回它在未来某个时间点拥有的集合的代理,当填充分配等时,您将能够迭代并查询其成员等。

    这样做的原因:

    u.projects << p
    
    u.projects
    => #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil>]>
    

    但这不是

    pp.project = p # assign project to project_participation
    
    u.project_participations << pp # assign project_participation to user
    
    u.project_participations
    => #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil>]>
    
    u.projects
    => #<ActiveRecord::Associations::CollectionProxy []>
    

    在第一种情况下,您只是将对象添加到您的用户实例可以直接访问的数组中。在第二个示例中,has_many_through 关系反映了发生在数据库级别的关系。在第二个示例中,为了让您的用户可以访问您的项目,AR 必须实际运行一个查询来连接表并返回您正在查找的数据。由于这些对象都没有被持久化,所以数据库查询还不能发生,所以你得到的只是代理。

    最后一段代码具有误导性,因为它实际上并没有按照您的想法进行。

    u.project_participations.map(&:project)
    => [#<Project id: nil>]
    

    在这种情况下,您有一个用户直接持有一个 ProjectParticipations 数组,其中一个直接持有一个项目,因此它可以工作。它实际上并没有像你想象的那样使用 has_many_through 机制。

    同样,这有点过于简单化了,但这是大意。

    【讨论】:

      【解决方案2】:

      简答:不,第二个示例不会像第一个示例那样工作。您必须使用第一个示例的方式直接与用户和项目对象创建中间关联。

      长答案

      在开始之前,我们应该知道has_many :throughActiveRecord::Base 中是如何被处理的。因此,让我们从调用其关联builder herehas_many(name, scope = nil, options = {}, &amp;extension) 方法开始,在方法的末尾返回reflection,然后将反射添加到hash 作为缓存with key-value pair here

      现在的问题是,这些关联是如何被激活的?!?!

      这是因为association(name) 方法。它调用association_class 方法,它实际调用并返回这个常量:Associations::HasManyThroughAssociation,这使得this line 自动加载active_record/associations/has_many_through_association.rbinstantiate its instancehere。这是在创建关联时保存owner and reflection 的位置,并且在调用下一个重置方法时,该方法在子类ActiveRecord::Associations::CollectionAssociation here 中被调用。

      为什么这个重置调用很重要?因为,它将@target 设置为一个数组。这个@target 是一个数组,当您进行查询时所有关联对象都存储在该数组中,然后当您在代码中重用它而不是进行新查询时将其用作缓存。这就是为什么调用user.projects(其中用户和项目持续存在于数据库中,即调用:user = User.find(1) 然后user.projects)会进行数据库查询而再次调用它不会。

      因此,当您对关联进行 reader 调用时,例如:user.projects,它是 invokes the collectionProxy,然后从 load_target 填充 @target

      这只是触及表面而已。但是,您会了解如何使用构建器(根据条件创建 different reflection)构建关联并创建代理以读取目标变量中的数据。

      tl;dr

      您的第一个和第二个示例之间的区别在于它们的关联构建器被调用以创建关联的反射(based on macro)、代理和目标实例变量的方式。

      第一个例子:

      u = User.new
      p = Project.new
      u.projects << p
      
      u.association(:projects)
      #=> ActiveRecord::Associations::HasManyThroughAssociation object
      #=> @proxy = #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil, name: nil, created_at: nil, updated_at: nil>]>
      #=> @target = [#<Project id: nil, name: nil, created_at: nil, updated_at: nil>]
      
      u.association(:project_participations)
      #=> ActiveRecord::Associations::HasManyAssociation object
      #=> @proxy = #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>]>
      #=> @target = [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>]
      
      u.project_participations.first.association(:project)
      #=> ActiveRecord::Associations::BelongsToAssociation object
      #=> @target = #<Project id: nil, name: nil, created_at: nil, updated_at: nil>
      

      第二个例子:

      u = User.new
      pp = ProjectParticipation.new
      p = Project.new
      
      pp.project = p # assign project to project_participation
      
      u.project_participations << pp # assign project_participation to user
      
      u.association(:projects)
      #=> ActiveRecord::Associations::HasManyThroughAssociation object
      #=> @proxy = nil
      #=> @target = []
      
      u.association(:project_participations)
      #=> ActiveRecord::Associations::HasManyAssociation object
      #=> @proxy = #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>
      #=> @target = [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>]
      
      u.project_participations.first.association(:project)
      #=> ActiveRecord::Associations::BelongsToAssociation object
      #=> @target = #<Project id: nil, name: nil, created_at: nil, updated_at: nil>
      

      BelongsToAssociation 没有代理,只有target and owner

      但是,如果您真的倾向于使您的第二个示例工作,您只需要这样做:

      u.association(:projects).instance_variable_set('@target', [p])
      

      现在:

      u.projects
      #=>  #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil, name: nil, created_at: nil, updated_at: nil>]>
      

      在我看来,这是一种非常糟糕的创建/保存关联的方式。所以,坚持第一个例子本身。

      【讨论】:

      • 感谢源链接和.association 方法。这可以深入了解关联是如何工作的,并且是最有帮助的答案。
      • 第一个例子中如何设置关联的role?所以我想在项目中添加一个角色为membermanager 的用户,但关联仍然有效。
      • 你可以这样做:user = User.new{ |u| u.role = Role.new(name: 'member') },然后是p = Project.newuser.projects &lt;&lt; p
      • 我没有Role - role 在连接表中project_participationsenum role: { member: 0, manager: 1 },因为用户可以在不同的项目中拥有不同的角色)。所以一般来说,在将用户分配给项目时,我想从project_participations 表中设置一些属性。
      • 我明白了,我认为不能这样做。您必须通过调用 u.project_participations.first.role = 'manager' 来明确更新项目参与情况。
      【解决方案3】:

      这更像是 ruby​​ 数据结构级别的 rails 结构。 为了简化它,让我们这样说。 首先把 User 想象成一个数据结构包含:

      1. project_participations 数组
      2. 项目数组

      和项目

      1. 用户数组
      2. project_participations 数组

      现在,当您将关系标记为 :through 另一个(user.projects 到 user.project_participations)

      Rails 暗示当您将记录添加到第一个关系 (user.projects) 时,它必须在第二个关系 (user.project_participations) 中创建另一个记录,这就是“通过”挂钩的全部效果

      所以在这种情况下,

      user.projects << project
      #will proc the 'through'
      #user.project_participations << new_entry
      

      请记住,project.users 仍然没有更新,因为它是一个完全不同的数据结构,您没有引用它。

      让我们看看第二个例子会发生什么

      u.project_participations << pp
      #this has nothing hooked to it so it operates like a normal array
      

      因此,总而言之,这就像 ruby​​ 数据结构级别上的单向绑定,每当您保存和刷新对象时,它都会按照您想要的方式运行。

      【讨论】:

        【解决方案4】:

        关联在数据库级别上定义,并使用数据库表的主键(在polymorphic 的情况下,类名)。在has_many :through 的情况下,关联查找(例如,UserProjects)是:

        1. 获取所有User-Project 对,其user_id 为特定值(数据库中现有User 的主键)
        2. 从这些对中获取所有 project_id(项目的主键)
        3. 通过结果键获取所有Projects

        当然,这些都是简单的术语,在数据库方面它更短,并且使用更复杂的抽象,例如inner join,但本质是一样的。

        当您通过new 创建一个新对象时,它还没有保存在数据库中,因此没有主键(它是nil)。也就是说,如果该对象尚未在数据库中,则您无法从任何 ActiveRecord 的关联中引用它。

        旁注:
        然而,一个新创建的(但尚未保存的)对象可能会表现得好像某些东西与之相关:它可能会显示属于NULL 的条目。这通常意味着您的数据库架构中存在允许此类事情发生的错误,但假设您可以设计他的数据库来利用这一点。

        【讨论】:

        • 在我的问题中的两个示例中,没有任何东西有主键,因为所有内容都未保存,但第一个仍然有效,第二个无效。
        • 未保存的条目对于每个关联都有自己的临时集合。但它们是为直接定义关联而设计的,作为保存操作的说明。 has_many :through 假定一个 隐式 连接:要获取间接定义的条目,它必须循​​环通过 through 模型并获取其中引用的所有 Projects。相反,它只访问临时关联的集合,该集合仅包含显式添加到该模型的项目。
        • 从某种角度来看,这可能被认为是一个错误。但事实是,这仍然允许您为大多数(如果不是全部)实际案例构建数据。
        猜你喜欢
        • 1970-01-01
        • 2015-07-31
        • 2013-09-16
        • 1970-01-01
        • 2010-12-13
        • 1970-01-01
        • 1970-01-01
        • 2018-09-02
        • 2011-09-28
        相关资源
        最近更新 更多