多对多关系与相同的模型在轨道?

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

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

30808 次浏览

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

  • Do I want to store additional information with the association? (Additional fields in the join table.)
  • 这些关联是否需要隐式的双向性? (如果 A 柱连接到 B 柱,那么 B 柱也连接到 A 柱。)

剩下四种可能性,我在下面看看。

参考资料: the Rails documentation on the subject。有一节叫做“ Many-to-many”,当然还有关于类方法本身的文档。

最简单的方案,单向的,没有额外的字段

这是代码中最紧凑的。

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代替。

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) ,以防止冲突。

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

这应该很简单! 下面是一个通过 script/console运行的 irb 会话示例:

>> 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关联使用模型。因此,您将无法访问任何其他字段。

单向的,带有附加字段

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字段。

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

通过第二个关联,我们告诉 Rails,我们可以通过第一个关联 post_connections,然后是 PostConnectionpost_b关联,到达其他的帖子,那些连接到这个帖子的帖子。

只有 还有一件事不见了,那就是我们需要告诉 Rails 一个 PostConnection依赖于它所属的帖子。如果 post_a_idpost_b_id中的一个或者两个都是 NULL,那么这种联系不会告诉我们太多,对吗?我们在 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参数。使用值 :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关联中,关联定义在所涉及的 both模型中,且关联是双向的。

但在这种情况下,只有一个 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 )

为了实现双向关联,我们必须找到一种方法,在 post_a_idpost_b_id颠倒的情况下,使 Rails OR达到上述条件,这样它就可以看到两个方向。

不幸的是,据我所知,唯一的方法是相当粗糙的。您必须使用 has_and_belongs_to_many的选项(如 :finder_sql:delete_sql等)手动指定 SQL。一点都不好看。(我也愿意听取建议。有人吗?)

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

如果有人来这里试图找出如何在 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/user.rb

了解更多信息。

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"

如果有人在工作中得不到好的答案,比如:

(对象不支持 # 检查)
=>

或者

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

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

回答施蒂夫提出的问题:

双向循环关联

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

  • 以被追随者的身份追随者
  • 以跟随者的身份跟随。

Here's how the code for User.rb might look:

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

下面是 跟我来的代码:

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

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

为了确定 User的: Follow (对数据库的 @user.followees调用) ,Rails 现在可以查看 class _ name 的每个实例: “ Follow”,其中这样的 User 是 Follow (即 foreign_key: :follower_id)到: such User的: Follow _ Follow。为了确定 User的: Follow (对数据库的 @user.followers调用) ,Rails 现在可以查看 class _ name 的每个实例: “ Follow”,其中这样的 User是通过: such User的: Follow _ Follow。

灵感来自@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_namy: Post,
join_table: 'post_connections',
foreign_key: 'post_b_id',
association_foreign_key: 'post_a_id')
end

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