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

时间:2014-10-30 11:29:22

标签: ruby-on-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可以参与许多projects,其角色为membermanager。连接模型称为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,我可以使用projects访问user的{​​{1}}。

但如果我自己创建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 []> 访问项目。

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

u.projects

不应该直接像第一个例子那样工作:u.project_participations.map(&:project) => [#<Project id: nil>] 不依赖于我是否自己创建连接对象而返回所有项目?或者我怎样才能让AR意识到这一点?

4 个答案:

答案 0 :(得分:9)

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

答案很长

在开始之前,我们应该知道has_many :throughActiveRecord::Base的处理方式。所以,让我们从调用其关联has_many(name, scope = nil, options = {}, &extension)builder here方法开始,在方法结束处返回reflection,然后将反射添加到hash作为缓存{{3 }}。

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

这是因为with key-value pair here方法。哪个调用association(name)方法,实际调用并返回此常量:association_class,使Associations::HasManyThroughAssociation自动加载 active_record / associations / has_many_through_association.rb 和{{3 }} this line。这是在创建关联时保存instantiate its instance的位置,并且正在调用下一个在子类ActiveRecord::Associations::CollectionAssociation here中调用的重置方法。

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

因此,当您在owner and reflection填充user.projects之前,对某个关联进行here调用,例如:@targetreader。 / p>

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

<强> TL;博士

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

第一个例子:

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没有代理,只有different reflection

但是,如果你真的倾向于让你的第二个例子起作用,你就必须这样做:

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

现在:

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

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

答案 1 :(得分:2)

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

  1. project_participations Array
  2. projects Array
  3. 和项目

    1. users Array
    2. project_participations Array
    3. 现在,当您将关系标记为:通过另一个(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数据结构级别的单向绑定,无论何时保存和刷新对象,都会按照您想要的方式运行。

答案 2 :(得分: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机制。

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

答案 3 :(得分:0)

数据库级上定义关联,并使用数据库表的主键(在polymorphic个案例中,类名 )。如果是has_many :through,则关联查询(例如,User的{​​{1}} s)是:

  1. 获取所有Project - User对,其Project是特定值(数据库中现有user_id的主键)
  2. 从这些对中获取所有User(项目的主键)
  3. 按结果键获取所有project_id
  4. 当然,这些都是简单的术语,在数据库方面它更短,并且使用更复杂的抽象,例如Project,但实质是相同的。

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

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