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

Mar*_*kus 8 activerecord ruby-on-rails 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
Run Code Online (Sandbox Code Playgroud)

A user可以参与许多projects角色扮演a member或a manager.调用连接模型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>]>
Run Code Online (Sandbox Code Playgroud)

到目前为止好- AR创建的ProjectParticipation本身,我可以访问projectsuser使用u.projects.

但是,如果我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 []>
Run Code Online (Sandbox Code Playgroud)

为什么项目是空的?我不能u.projects像以前那样访问项目.

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

u.project_participations.map(&:project)
=> [#<Project id: nil>]
Run Code Online (Sandbox Code Playgroud)

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

Sur*_*rya 10

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

答案很长:

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

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

这是因为association(name)方法.哪个调用association_class方法实际调用并返回此常量:Associations::HasManyThroughAssociation这使得该行自动加载active_record/associations/has_many_through_association.rb在此实例化其实例 .这是业主和反思在创建关联时被保存,并在接下来的复位方法被调用它得到在子类中调用这里.ActiveRecord::Associations::CollectionAssociation

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

因此,当您在一个关联上调用读者时,例如:user.projects,它会在填充from 之前调用collectionProxy.@targetload_target

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

TL;博士

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

第一个例子:

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>
Run Code Online (Sandbox Code Playgroud)

第二个例子:

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>
Run Code Online (Sandbox Code Playgroud)

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

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

u.association(:projects).instance_variable_set('@target', [p])
Run Code Online (Sandbox Code Playgroud)

现在:

u.projects
#=>  #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil, name: nil, created_at: nil, updated_at: nil>]>
Run Code Online (Sandbox Code Playgroud)

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