Rails 中同一个模型的多对多关系? [英] Many-to-many relationship with the same model in 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 ofb.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 ahas_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_many
、belongs_to
、has_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_a
或 post_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_id
和post_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 throughpost_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_connections
和 reverse_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_id
和 post_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屋!