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

查看:32
本文介绍了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 的连接表上的任何地方.它会大声抱怨.

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 列.(没有 :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_connectionspost_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,它更快,但如果您正在使用它们,则不会调用任何销毁钩子.
  • 我们还为 反向 连接添加了一个 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 关联中,关联是在两个 模型中定义的.并且关联是双向的.

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 OR 上述条件与 post_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天全站免登陆