Ruby-on-rails Rails 中同一个模型的多对多关系?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2168442/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 22:13:28  来源:igfitidea点击:

Many-to-many relationship with the same model in rails?

ruby-on-rails

提问by Victor

How can I make a many-to-many relationship with the same model in rails?

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

For example, each post is connected to many posts.

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

回答by Stéphan Kochen

There are several kinds of many-to-many relationships; you have to ask yourself the following questions:

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

  • Do I want to store additional information with the association? (Additional fields in the join table.)
  • Do the associations need to be implicitly bi-directional? (If post A is connected to post B, then post B is also connected to post A.)
  • 我想在关联中存储其他信息吗?(连接表中的其他字段。)
  • 关联是否需要隐式双向?(如果帖子 A 连接到帖子 B,那么帖子 B 也连接到帖子 A。)

That leaves four different possibilities. I'll walk over these below.

这就留下了四种不同的可能性。我将在下面介绍这些。

For reference: the Rails documentation on the subject. There's a section called “Many-to-many”, and of course the documentation on the class methods themselves.

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

Simplest scenario, uni-directional, no additional fields

最简单的场景,单向,无附加字段

This is the most compact in code.

这是代码中最紧凑的。

I'll start out with this basic schema for your posts:

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

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

For any many-to-many relationship, you need a join table. Here's the schema for that:

对于任何多对多关系,您都需要一个连接表。这是架构:

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

By default, Rails will call this table a combination of the names of the two tables we're joining. But that would turn out as posts_postsin this situation, so I decided to take post_connectionsinstead.

默认情况下,Rails 会将此表称为我们正在连接的两个表的名称的组合。但是posts_posts在这种情况下会变成这样,所以我决定post_connections改用。

Very important here is :id => false, to omit the default idcolumn. Rails wants that column everywhere excepton join tables for has_and_belongs_to_many. It will complain loudly.

这里非常重要的是:id => false,省略默认id列。轨希望该列无处不在,除了对连接的表进行has_and_belongs_to_many。它会大声抱怨。

Finally, notice that the column names are non-standard as well (not post_id), to prevent conflict.

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

Now in your model, you simply need to tell Rails about these couple of non-standard things. It will look as follows:

现在在你的模型中,你只需要告诉 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

And that should simply work! Here's an example irb session run through script/console:

这应该很简单!以下是运行的 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!">]

You'll find that assigning to the postsassociation will create records in the post_connectionstable as appropriate.

您会发现分配给posts关联将在post_connections适当的情况下在表中创建记录。

Some things to note:

一些注意事项:

  • You can see in the above irb session that the association is uni-directional, because after a.posts = [b, c], the output of b.postsdoes not include the first post.
  • Another thing you may have noticed is that there is no model PostConnection. You normally don't use models for a has_and_belongs_to_manyassociation. For this reason, you won't be able to access any additional fields.
  • 在上面的 irb session 中可以看到关联是单向的,因为 aftera.posts = [b, c]的输出b.posts不包括第一篇文章。
  • 您可能已经注意到的另一件事是没有模型PostConnection。您通常不会将模型用于has_and_belongs_to_many关联。因此,您将无法访问任何其他字段。

Uni-directional, with additional fields

单向,带有附加字段

Right, now... You've got a regular user who has today made a post on your site about how eels are delicious. This total stranger comes around to your site, signs up, and writes a scolding post on regular user's ineptitude. After all, eels are an endangered species!

是的,现在...您有一个普通用户,他今天在您的网站上发表了一篇关于鳗鱼美味的帖子。这个完全陌生的人来到您的网站,注册并写了一篇责骂普通用户无能的帖子。毕竟鳗鱼是濒危物种!

So you'd like to make clear in your database that post B is a scolding rant on post A. To do that, you want to add a categoryfield to the association.

因此,您希望在您的数据库中明确指出帖子 B 是对帖子 A 的责骂。为此,您需要向category关联添加一个字段。

What we need is no longer a has_and_belongs_to_many, but a combination of has_many, belongs_to, has_many ..., :through => ...and an extra model for the join table. This extra model is what gives us the power to add additional information to the association itself.

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

Here's another schema, very similar to the above:

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

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

Notice how, in this situation, post_connectionsdoeshave an idcolumn. (There's no:id => falseparameter.) This is required, because there'll be a regular ActiveRecord model for accessing the table.

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

I'll start with the PostConnectionmodel, because it's dead simple:

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

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

The only thing going on here is :class_name, which is necessary, because Rails cannot infer from post_aor post_bthat we're dealing with a Post here. We have to tell it explicitly.

这里发生的唯一的事情是:class_name,这是必要的,由于Rails不能推断post_a或者post_b说我们正在处理的一则讯息。我们必须明确地告诉它。

Now the Postmodel:

现在Post模型:

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

With the first has_manyassociation, we tell the model to join post_connectionson posts.id = post_connections.post_a_id.

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

With the second association, we are telling Rails that we can reach the other posts, the ones connected to this one, through our first association post_connections, followed by the post_bassociation of PostConnection.

使用第二个关联,我们告诉 Rails 我们可以通过我们的第一个关联post_connections,然后是 的post_b关联访问其他帖子,即与此关联的帖子PostConnection

There's just one more thingmissing, and that is that we need to tell Rails that a PostConnectionis dependent on the posts it belongs to. If one or both of post_a_idand post_b_idwere NULL, then that connection wouldn't tell us much, would it? Here's how we do that in our Postmodel:

还缺少一件事,那就是我们需要告诉 Rails aPostConnection依赖于它所属的帖子。如果一个或两个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

Besides the slight change in syntax, two real things are different here:

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

  • The has_many :post_connectionshas an extra :dependentparameter. With the value :destroy, we tell Rails that, once this post disappears, it can go ahead and destroy these objects. An alternative value you can use here is :delete_all, which is faster, but will not call any destroy hooks if you are using those.
  • We've added a has_manyassociation for the reverseconnections as well, the ones that have linked us through post_b_id. This way, Rails can neatly destroy those as well. Note that we have to specify :class_namehere, because the model's class name can no longer be inferred from :reverse_post_connections.
  • has_many :post_connections有一个额外的:dependent参数。通过 value :destroy,我们告诉 Rails,一旦这个帖子消失,它可以继续销毁这些对象。您可以在此处使用的替代值是:delete_all,它更快,但如果您正在使用它们,则不会调用任何销毁钩子。
  • 我们还has_many反向连接添加了一个关联,这些关联通过post_b_id. 这样,Rails 也可以巧妙地销毁它们。请注意,我们必须在:class_name此处指定,因为无法再从 推断出模型的类名:reverse_post_connections

With this in place, I bring you another irb session through script/console:

有了这个,我给你带来了另一个 irb 会议script/console

>> 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

Instead of creating the association and then setting the category separately, you can also just create a PostConnection and be done with it:

除了创建关联然后单独设置类别之外,您还可以创建一个 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!">]

And we can also manipulate the post_connectionsand reverse_post_connectionsassociations; it will neatly reflect in the postsassociation:

我们还可以操纵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
=> []

Bi-directional looped associations

双向循环关联

In normal has_and_belongs_to_manyassociations, the association is defined in bothmodels involved. And the association is bi-directional.

在正常has_and_belongs_to_many关联中,关联是在所涉及的两个模型中定义的。并且关联是双向的。

But there is just one Post model in this case. And the association is only specified once. That's exactly why in this specific case, associations are uni-directional.

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

The same is true for the alternative method with has_manyand a model for the join table.

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

This is best seen when simply accessing the associations from irb, and looking at the SQL that Rails generates in the log file. You'll find something like the following:

这在简单地从 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 )

To make the association bi-directional, we'd have to find a way to make Rails ORthe above conditions with post_a_idand post_b_idreversed, so it will look in both directions.

为了使关联是双向的,我们必须找到一种方法使 RailsOR满足上述条件post_a_idpost_b_id反转,因此它会在两个方向上查找。

Unfortunately, the only way to do this that I know of is rather hacky. You'll have to manually specify your SQL using options to has_and_belongs_to_manysuch as :finder_sql, :delete_sql, etc. It's not pretty. (I'm open to suggestions here too. Anyone?)

不幸的是,我所知道的做到这一点的唯一方法相当笨拙。你必须使用你的SQL选项来手动指定has_and_belongs_to_many,如:finder_sql:delete_sql等它不漂亮。(我也愿意在这里接受建议。有人吗?)

回答by jbmilgrom

To answer the question posed by Shteef:

回答谢蒂夫提出的问题:

Bi-directional looped associations

双向循环关联

The follower-followee relationship among Usersis a good example of a Bi-directional looped association. A Usercan have many:

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

  • followers in its capacity as followee
  • followees in its capacity as follower.
  • 追随者以其被追随者的身份
  • 追随者以其追随者的身份。

Here's how the code for user.rbmight look:

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

Here's how the code for follow.rb:

以下是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

The most important things to note are probably the terms :follower_followsand :followee_followsin user.rb. To use a run of the mill (non-looped) association as an example, a Teammay have many :playersthrough :contracts. This is no different for a Player, who may have many :teamsthrough :contractsas well (over the course of such Player's career). But in this case, where only one named model exists (i.e. a User), naming the through: relationship identically (e.g. through: :follow, or, like was done above in the posts example, through: :post_connections) would result in a naming collision for different use cases of (or access points into) the join table. :follower_followsand :followee_followswere created to avoid such a naming collision. Now, a Usercan have many :followersthrough :follower_followsand many :followeesthrough :followee_follows.

需要注意的最重要的事情可能是user.rb 中的术语:follower_follows:followee_follows。以工厂运行(非循环)关联为例,一个团队可能有许多 :players:contracts。这对于Player没有什么不同,他可能也有很多:teams通过:contracts(在这样的Player的职业生涯中)。但在这种情况下,只有一个命名模型存在(即一个User),相同地命名 through: 关系(例如through: :follow,或者,就像上面在帖子示例中所做的那样,through: :post_connections)将导致不同用例的命名冲突(或接入点)连接表。:follower_follows:followee_follows创建是为了避免这种命名冲突。现在,一个User可以有 many :followersthrough:follower_follows和 many :followeesthrough :followee_follows

To determine a User's :followees (upon an @user.followeescall to the database), Rails may now look at each instance of class_name: “Follow” where such User is the the follower (i.e. foreign_key: :follower_id) through: such User's :followee_follows. To determine a User's :followers (upon an @user.followerscall to the database), Rails may now look at each instance of class_name: “Follow” where such Useris the the followee (i.e. foreign_key: :followee_id) through: such User's :follower_follows.

为了确定User的 :followees(在@user.followees调用数据库时),Rails 现在可以查看 class_name:“Follow”的每个实例,其中此类 User 是跟随者(即foreign_key: :follower_id)通过:此类User的 :followee_follows。为了确定User的 :followers(在@user.followers调用数据库时),Rails 现在可以查看 class_name: “Follow”的每个实例,其中此类User是被关注者(即foreign_key: :followee_id)通过:此类User的 :follower_follows。

回答by hrdwdmrbl

If anyone came here to try to find out how to create friend relationships in Rails, then I would refer them to what I finally decided to use, which is to copy what 'Community Engine' did.

如果有人来这里试图找出如何在 Rails 中创建朋友关系,那么我会向他们推荐我最终决定使用的东西,即复制“社区引擎”所做的。

You can refer to:

你可以参考:

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

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

and

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

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

for more information.

想要查询更多的信息。

TL;DR

TL; 博士

# 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"

回答by Alba Hoo

Inspired by @Stéphan Kochen, this could work for bi-directional associations

受@Stéphan Kochen 的启发,这适用于双向关联

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

then post.posts&& post.reversed_postsshould both works, at least worked for me.

那么post.posts&&post.reversed_posts应该都有效,至少对我有用。

回答by Zhenya Slabkovski

For bi-directional belongs_to_and_has_many, refer to the great answer already posted, and then create another association with a different name, the foreign keys reversed and ensure that you have class_nameset to point back to the correct model. Cheers.

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

回答by user2303277

If anyone had issues getting the excellent answer to work, such as:

如果有人在获得出色的工作答案时遇到问题,例如:

(Object doesn't support #inspect)
=>

(对象不支持#inspect)
=>

or

或者

NoMethodError: undefined method `split' for :Mission:Symbol

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

Then the solution is to replace :PostConnectionwith "PostConnection", substituting your classname of course.

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