Rails中相同模型的多对多关系? [英] Many-to-many relationship with the same model in rails?

查看:17
本文介绍了Rails中相同模型的多对多关系?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

For example, each post is connected to many posts.

推荐答案

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

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

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

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

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

参考:Rails 文档.有一个名为多对多"的部分,当然还有关于类方法本身的文档.

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.

这是代码中最紧凑的.

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

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

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

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_posts in this situation, so I decided to take post_connections instead.

这里非常重要的是 :id =>false,省略默认的 id 列.Rails 想要 has_and_belongs_to_many 的连接表上的所有 except 列.它会大声抱怨.

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

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

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

现在在你的模型中,你只需要告诉 Rails 这两个非标准的东西.如下所示:

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

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

这应该很有效!这是一个通过 script/console 运行的示例 irb 会话:

And that should simply work! Here's an example irb session run through 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 表中适当地创建记录.

You'll find that assigning to the posts association will create records in the post_connections table as appropriate.

注意事项:

  • 在上面的irb session中可以看到关联是单向的,因为在a.posts = [b, c]之后,b.posts 不包括第一篇文章.
  • 您可能注意到的另一件事是没有模型PostConnection.您通常不会将模型用于 has_and_belongs_to_many 关联.因此,您将无法访问任何其他字段.
  • You can see in the above irb session that the association is uni-directional, because after a.posts = [b, c], the output of b.posts does 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_many association. For this reason, you won't be able to access any 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!

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

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 category field to the association.

我们需要的不再是has_and_belongs_to_many,而是has_manybelongs_tohas_many ...、的组合:通过=>... 和连接表的额外模型.这个额外的模型使我们能够向关联本身添加额外的信息.

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.

这是另一个架构,与上面的非常相似:

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

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

Notice how, in this situation, post_connections does have an id column. (There's no :id => false parameter.) This is required, because there'll be a regular ActiveRecord model for accessing the table.

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

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

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 推断出我们是在这里处理一个帖子.我们必须明确地告诉它.

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

现在是 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 关联,我们告诉模型在 posts.id = post_connections.post_a_id 上加入 post_connections.

With the first has_many association, we tell the model to join post_connections on posts.id = post_connections.post_a_id.

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

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_b association of PostConnection.

还有一件事丢失了,那就是我们需要告诉 Rails PostConnection 依赖于它所属的帖子.如果 post_a_idpost_b_id 之一或两者为 NULL,那么该连接不会告诉我们太多信息,不是吗?以下是我们在 Post 模型中的做法:

There's just one more thing missing, and that is that we need to tell Rails that a PostConnection is dependent on the posts it belongs to. If one or both of post_a_id and post_b_id were NULL, then that connection wouldn't tell us much, would it? Here's how we do that in our Post model:

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:

  • has_many :post_connections 有一个额外的:dependent 参数.使用 :destroy 值,我们告诉 Rails,一旦这篇文章消失,它就可以继续销毁这些对象.您可以在此处使用的替代值是 :delete_all,它更快,但如果您正在使用这些钩子,则不会调用任何销毁钩子.
  • 我们还为 reverse 连接添加了 has_many 关联,这些关联通过 post_b_id 链接我们.这样,Rails 也可以巧妙地破坏它们.请注意,我们必须在此处指定 :class_name,因为无法再从 :reverse_post_connections 推断模型的类名.
  • The has_many :post_connections has an extra :dependent parameter. 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_many association for the reverse connections 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_name here, because the model's class name can no longer be inferred from :reverse_post_connections.

有了这个,我通过 script/console 为您带来另一个 irb 会话:

With this in place, I bring you another irb session through 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

您也可以只创建一个 PostConnection 并完成它,而不是创建关联然后单独设置类别:

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

>> 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 关联中:

And we can also manipulate the post_connections and reverse_post_connections associations; it will neatly reflect in the posts association:

>> 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 关联中,关联在所涉及的both 模型中定义.而且关联是双向的.

Bi-directional looped associations

In normal has_and_belongs_to_many associations, the association is defined in both models involved. And the association is bi-directional.

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

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.

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

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

这在简单地从 irb 访问关联并查看 Rails 在日志文件中生成的 SQL 时最容易看到.您会发现类似以下内容:

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:

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

为了实现双向关联,我们必须想办法让 Rails ORpost_a_idpost_b_id 反转,所以它会朝两个方向看.

To make the association bi-directional, we'd have to find a way to make Rails OR the above conditions with post_a_id and post_b_id reversed, so it will look in both directions.

不幸的是,据我所知,这样做的唯一方法是相当老套.您必须使用 has_and_belongs_to_many 的选项手动指定 SQL,例如 :finder_sql:delete_sql 等.这并不漂亮.(我也愿意接受这里的建议.有人吗?)

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_many such as :finder_sql, :delete_sql, etc. It's not pretty. (I'm open to suggestions here too. Anyone?)

这篇关于Rails中相同模型的多对多关系?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆