在Rails中使用同一模型进行多对多关系?


107

如何在Rails中使用同一模型建立多对多关系?

例如,每个帖子都连接到许多帖子。

Answers:


276

多对多关系有几种。您必须问自己以​​下问题:

  • 我是否要在关联中存储其他信息?(联接表中的其他字段。)
  • 关联是否需要是隐式双向的?(如果帖子A连接到帖子B,则帖子B也连接到帖子A。)

这留下了四种不同的可能性。我将在下面浏览这些内容。

供参考:关于该主题的Rails文档。有一个称为“多对多”的部分,当然还有有关类方法本身的文档。

最简单的情况,单向,没有其他字段

这是代码中最紧凑的代码。

我将从您的帖子的基本架构开始:

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end

对于任何多对多关系,您都需要一个联接表。这是该模式:

create_table "post_connections", :force => true, :id => false do |t|
  t.integer "post_a_id", :null => false
  t.integer "post_b_id", :null => false
end

默认情况下,Rails会将此表称为我们要加入的两个表的名称的组合。但是事实证明,posts_posts在这种情况下,我决定改post_connections而采取。

这里非常重要的是:id => false,忽略默认id列。Rails希望该列无处不在,除了的联接表上has_and_belongs_to_many。它会大声抱怨。

最后,请注意,列名也是非标准的(不是post_id),以防止发生冲突。

现在,在您的模型中,您只需要将这些非标准的事情告诉Rails。它看起来如下:

class Post < ActiveRecord::Base
  has_and_belongs_to_many(:posts,
    :join_table => "post_connections",
    :foreign_key => "post_a_id",
    :association_foreign_key => "post_b_id")
end

那应该工作!这是一个运行irb会话的示例script/console

>> a = Post.create :name => 'First post!'
=> #<Post id: 1, name: "First post!">
>> b = Post.create :name => 'Second post?'
=> #<Post id: 2, name: "Second post?">
>> c = Post.create :name => 'Definitely the third post.'
=> #<Post id: 3, name: "Definitely the third post.">
>> a.posts = [b, c]
=> [#<Post id: 2, name: "Second post?">, #<Post id: 3, name: "Definitely the third post.">]
>> b.posts
=> []
>> b.posts = [a]
=> [#<Post id: 1, name: "First post!">]

您会发现,分配给posts关联将在post_connections表中适当地创建记录。

注意事项:

  • 您可以在上面的irb会话中看到关联是单向的,因为在之后a.posts = [b, c],的输出b.posts不包括第一篇文章。
  • 您可能已经注意到的另一件事是,没有模型PostConnection。您通常不使用模型进行has_and_belongs_to_many关联。因此,您将无法访问任何其他字段。

单向,带有附加字段

对了,现在……您有一个普通用户,他今天在您的网站上发布了有关鳗鱼如何美味的文章。这名陌生人来到您的网站,进行注册,并就普通用户的无礼发表评论。毕竟,鳗鱼是濒临灭绝的物种!

因此,您想在数据库中清楚地表明,帖子B是帖子A上的骂人。为此,您想向category关联中添加一个字段。

我们需要的不再是一个has_and_belongs_to_many,而是一种组合has_manybelongs_tohas_many ..., :through => ...并为额外的模型连接表。这种额外的模型使我们有能力向协会本身添加其他信息。

这是另一个模式,与上面的非常相似:

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end

create_table "post_connections", :force => true do |t|
  t.integer "post_a_id", :null => false
  t.integer "post_b_id", :null => false
  t.string  "category"
end

请注意如何,在这种情况下,post_connections 确实有一个id列。(没有 :id => false参数。)这是必需的,因为将使用常规的ActiveRecord模型来访问表。

我将从PostConnection模型开始,因为它非常简单:

class PostConnection < ActiveRecord::Base
  belongs_to :post_a, :class_name => :Post
  belongs_to :post_b, :class_name => :Post
end

这里唯一要做的是:class_name,这是必需的,因为Rails无法从中推断出post_apost_b我们正在此处处理Post。我们必须明确地告诉它。

现在的Post模型:

class Post < ActiveRecord::Base
  has_many :post_connections, :foreign_key => :post_a_id
  has_many :posts, :through => :post_connections, :source => :post_b
end

随着第一个has_many协会,我们要告诉模型加入post_connectionsposts.id = post_connections.post_a_id

随着第二个关联,我们告诉Rails的,我们可以到达其他职位,连接到这一块的,通过我们的第一联想post_connections,其次是post_b关联PostConnection

仅缺少一件事,那就是我们需要告诉Rails a PostConnection取决于它所属的帖子。如果一个或两个post_a_idpost_b_idNULL,那么该连接不会告诉我们很多,不是吗?这是我们在Post模型中执行的操作:

class Post < ActiveRecord::Base
  has_many(:post_connections, :foreign_key => :post_a_id, :dependent => :destroy)
  has_many(:reverse_post_connections, :class_name => :PostConnection,
      :foreign_key => :post_b_id, :dependent => :destroy)

  has_many :posts, :through => :post_connections, :source => :post_b
end

除了语法上的细微变化外,这里有两个不同之处:

  • has_many :post_connections有一个额外的:dependent参数。使用value :destroy,我们告诉Rails,一旦该帖子消失,它就可以继续销毁这些对象。您可以在此处使用的另一个值是:delete_all,它的速度更快,但是如果您使用它们,则不会调用任何销毁钩子。
  • 我们还has_many反向连接添加了一个关联,这些关联通过链接了我们post_b_id。这样,Rails也可以巧妙地销毁它们。请注意,我们必须在:class_name此处指定,因为无法再从推断模型的类名:reverse_post_connections

有了这个,我通过script/console以下方式为您带来了另一个irb会议:

>> a = Post.create :name => 'Eels are delicious!'
=> #<Post id: 16, name: "Eels are delicious!">
>> b = Post.create :name => 'You insensitive cloth!'
=> #<Post id: 17, name: "You insensitive cloth!">
>> b.posts = [a]
=> [#<Post id: 16, name: "Eels are delicious!">]
>> b.post_connections
=> [#<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>]
>> connection = b.post_connections[0]
=> #<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>
>> connection.category = "scolding"
=> "scolding"
>> connection.save!
=> true

除了创建关联然后单独设置类别外,您还可以创建一个PostConnection并完成它:

>> b.posts = []
=> []
>> PostConnection.create(
?>   :post_a => b, :post_b => a,
?>   :category => "scolding"
>> )
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> b.posts(true)  # 'true' means force a reload
=> [#<Post id: 16, name: "Eels are delicious!">]

我们还可以操纵post_connectionsreverse_post_connections关联;它将整齐地反映在posts关联中:

>> a.reverse_post_connections
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> a.reverse_post_connections = []
=> []
>> b.posts(true)  # 'true' means force a reload
=> []

双向循环关联

在普通has_and_belongs_to_many关联中,在两个涉及的模型中定义了关联。并且关联是双向的。

但是在这种情况下只有一个Post模型。并且关联仅指定一次。这就是在这种特定情况下关联是单向的原因。

对于带有has_many连接表和模型的替代方法,也是如此。

当仅从irb访问关联并查看Rails在日志文件中生成的SQL时,最好地看到这一点。您会发现类似以下内容:

SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )

为了使关联双向,我们必须找到一种使Rails OR满足以上条件post_a_idpost_b_id反转的方法,因此它将在两个方向上都可以看到。

不幸的是,据我所知,唯一的方法就是hacky。你必须使用你的SQL选项来手动指定has_and_belongs_to_many,如:finder_sql:delete_sql等它不漂亮。(我也愿意在这里提出建议。有人吗?)


谢谢你的好评!:)我做了一些进一步的编辑。具体来说,:foreign_key上的has_many :through并不是必须的,我添加了有关如何使用非常方便的:dependent参数的说明has_many
斯蒂芬Kochen

@Shtééf甚至大规模分配(update_attributes)在双向关联的情况下也无法工作,例如:postA.update_attributes({:post_b_ids => [2,3,4]})任何想法或解决方法?
Lohith MV

伙伴5次非常不错的回答5次{puts“ +1”}
Rahul

@Shtééf我从这个答案中学到了很多,谢谢!我试着问,在这里回答你的双向关联的问题:stackoverflow.com/questions/25493368/...
jbmilgrom

17

要回答Shteef提出的问题:

双向循环关联

用户之间的关注者-跟随者关系是双向循环关联的一个很好的例子。一个用户可以有很多:

  • 追随者身份
  • 作为追随者的追随者。

这是user.rb的代码的外观:

class User < ActiveRecord::Base
  # follower_follows "names" the Follow join table for accessing through the follower association
  has_many :follower_follows, foreign_key: :followee_id, class_name: "Follow" 
  # source: :follower matches with the belong_to :follower identification in the Follow model 
  has_many :followers, through: :follower_follows, source: :follower

  # followee_follows "names" the Follow join table for accessing through the followee association
  has_many :followee_follows, foreign_key: :follower_id, class_name: "Follow"    
  # source: :followee matches with the belong_to :followee identification in the Follow model   
  has_many :followees, through: :followee_follows, source: :followee
end

这是follow.rb的代码:

class Follow < ActiveRecord::Base
  belongs_to :follower, foreign_key: "follower_id", class_name: "User"
  belongs_to :followee, foreign_key: "followee_id", class_name: "User"
end

需要注意的最重要的事情可能是术语:follower_follows:followee_followsuser.rb。以运行工厂(非循环)关联为例,一个团队可能有许多:players:contracts。对于一个玩家来说,这也没有什么不同,在这个玩家的职业生涯中,他也可能有很多:teams经历。但是在这种情况下,如果仅存在一个命名模型(即User),则对through:关系进行相同的命名(例如,或者像上面的post示例中所做的那样)会导致()的不同用例的命名冲突或访问点进入)联接表。和:contractsthrough: :followthrough: :post_connections:follower_follows:followee_follows是为了避免这种命名冲突而创建的。现在,用户可以有很多:followers通过:follower_follows和许多:followees通过:followee_follows

为了确定用户的:followees(在@user.followees调用数据库时),Rails现在可以查看class_name:“关注”的每个实例,其中,该用户通过以下方式成为关注者(即foreign_key: :follower_id):该用户的:followee_follows。为了确定用户的:followers(在@user.followers调用数据库时),Rails现在可以查看class_name:“关注”的每个实例,其中,该用户是以下对象的关注者(即foreign_key: :followee_id):该用户的:follower_follows。


1
正是我需要的!谢谢!(我还建议列出数据库迁移;我必须从接受的答案中收集该信息)
Adam Denoon

6

如果有人来这里尝试找出如何在Rails中创建朋友关系,那么我将向他们介绍我最终决定使用的方法,即复制“社区引擎”所做的事情。

您可以参考:

https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb

https://github.com/bborn/communityengine/blob/master/app/models/user.rb

想要查询更多的信息。

TL; DR

# user.rb
has_many :friendships, :foreign_key => "user_id", :dependent => :destroy
has_many :occurances_as_friend, :class_name => "Friendship", :foreign_key => "friend_id", :dependent => :destroy

..

# friendship.rb
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"

2

受@StéphanKochen的启发,这可能适用于双向关联

class Post < ActiveRecord::Base
  has_and_belongs_to_many(:posts,
    :join_table => "post_connections",
    :foreign_key => "post_a_id",
    :association_foreign_key => "post_b_id")

  has_and_belongs_to_many(:reversed_posts,
    :class_name => Post,
    :join_table => "post_connections",
    :foreign_key => "post_b_id",
    :association_foreign_key => "post_a_id")
 end

那么post.posts&& post.reversed_posts应该都可以工作,至少对我有用。


1

对于双向belongs_to_and_has_many,请参考已经发布的好答案,然后创建另一个具有不同名称的关联,将外键反向,并确保已class_name设置指向正确的模型。干杯。


2
您能在帖子中显示一个例子吗?我已经按照您的建议尝试了多种方法,但是似乎无法做到。
achabacha322

0

如果有人遇到无法获得最佳答案的问题,例如:

(对象不支持#inspect)
=>

要么

NoMethodError::Mission:Symbol的未定义方法`split'

然后解决方案是将替换:PostConnection"PostConnection",当然要替换您的类名。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.