Rails / ActiveRecord has_many through:未保存对象的关联

让我们使用这些类:

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 <

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

到目前为止一直很好 – 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 < #<ActiveRecord::Associations::CollectionProxy [#]> u.projects => # 

为什么这些项目是空的? 我不能像以前一样通过u.projects访问项目。

但如果我直接参与,项目会显示:

 u.project_participations.map(&:project) => [#] 

它不应该直接像第一个例子那样工作: u.projects返回我所有的项目,不依赖于我是否自己创建了连接对象? 或者我怎样才能让AR意识到这一点?

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

答案很长

在开始之前,我们应该知道如何在ActiveRecord::Base处理has_many :through 。 所以,让我们从has_many(name, scope = nil, options = {}, &extension)方法开始 ,在这里调用它的关联构建器 ,在方法结束时返回reflection ,然后将reflection添加到哈希作为具有键值的缓存在这里配对 。

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

这是因为association(name)方法。 其中调用association_class方法,它实际调用并返回此常量: Associations::HasManyThroughAssociation ,使该行自动加载active_record / associations / has_many_through_association.rb并在此处 实例化其实例 。 这是在创建关联时以及在调用下一个重置方法时保存所有者和reflection的位置,该方法在子类ActiveRecord::Associations::CollectionAssociation被调用。

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

因此,当您在一个关联上进行读者调用时,例如: user.projects ,它会在从load_target填充@target之前调用collectionProxy 。

这几乎没有触及表面。 但是,您了解如何使用构建器构建关联(根据条件创建不同的reflection )并创建用于读取目标变量中的数据的代理。

TL;博士

第一个和第二个示例之间的区别在于它们的关联构建器被调用以创建关联的reflection(基于宏) ,代理和目标实例变量。

第一个例子:

 u = User.new p = Project.new u.projects << p u.association(:projects) #=> ActiveRecord::Associations::HasManyThroughAssociation object #=> @proxy = #]> #=> @target = [#] u.association(:project_participations) #=> ActiveRecord::Associations::HasManyAssociation object #=> @proxy = #]> #=> @target = [#] u.project_participations.first.association(:project) #=> ActiveRecord::Associations::BelongsToAssociation object #=> @target = # 

第二个例子:

 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 = # #=> @target = [#] u.project_participations.first.association(:project) #=> ActiveRecord::Associations::BelongsToAssociation object #=> @target = # 

BelongsToAssociation没有代理,它只是目标和所有者 。

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

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

现在:

 u.projects #=> #]> 

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

这更像是ruby数据结构层面的rails结构。 为了简化它,我们就这样说吧。 首先想象用户作为数据结构包含:

  1. project_participations数组
  2. 项目数组

和项目

  1. 用户数组
  2. project_participations数组

现在当你将关系标记为:通过另一个(user.projects through user.project_participations)

Rails意味着当你向第一个关系(user.projects)添加一条记录时,它必须在第二个realation(user.project_participations)中创建另一个,这是’through’钩子的所有效果

所以在这种情况下,

 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数据结构级别的单向绑定,每当您保存和刷新对象时,这将按照您想要的方式运行。

冒着严重过度简化的风险让我试着解释发生了什么

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

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

  u.project_participations => #]> 

与第二个示例的结果相同

 u.project_participations => #]> 

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

到目前为止一直很好 – AR自己创建了ProjectParticipation,我 可以使用u.projects访问用户的项目。

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

这有效的原因:

 u.projects << p u.projects => #]> 

但事实并非如此

 pp.project = p # assign project to project_participation u.project_participations << pp # assign project_participation to user u.project_participations => #]> u.projects => # 

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

最后一点代码是误导性的,因为它实际上并没有按照你的想法行事。

 u.project_participations.map(&:project) => [#] 

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

再次这有点过于简单化,但这是一般的想法。

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

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

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

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

边注:
但是,有可能新创建的(并且尚未保存)对象将表现为与其相关联的某些内容:它可能显示属于NULL条目。 这通常意味着您的数据库模式中存在错误,允许这样的事情发生,但假设可以设计他的数据库来使用它。