在轨道中使用相同型号的多对多关系?

Vic*_*tor 105 ruby-on-rails

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

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

Sté*_*hen 269

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

  • 我想在关联中存储其他信息吗?(连接表中的其他字段.)
  • 这些关联需要隐含双向吗?(如果帖子A连接到帖子B,那么帖子B也连接到帖子A.)

留下了四种不同的可能性.我会在下面走过这些.

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

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

这是代码中最紧凑的.

我将从你的帖子的这个基本架构开始:

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end
Run Code Online (Sandbox Code Playgroud)

对于任何多对多关系,您需要一个连接表.这是以下的架构:

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
Run Code Online (Sandbox Code Playgroud)

默认情况下,Rails会将此表称为我们要加入的两个表的名称的组合.但是posts_posts在这种情况下会出现这种情况,所以我决定post_connections改用它.

这里非常重要的是:id => false省略默认id列.Rails希望除了连接表之外的任何地方都有该列has_and_belongs_to_many.它会大声抱怨.

最后,请注意列名称也是非标准的(不是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
Run Code Online (Sandbox Code Playgroud)

这应该只是工作!这是一个运行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!">]
Run Code Online (Sandbox Code Playgroud)

您将发现分配给posts关联将post_connections根据需要在表中创建记录.

有些事情需要注意:

  • 您可以在上面的irb会话中看到该关联是单向的,因为之后a.posts = [b, c],输出b.posts不包括第一个帖子.
  • 您可能注意到的另一件事是没有模型PostConnection.您通常不使用模型进行has_and_belongs_to_many关联.因此,您将无法访问任何其他字段.

单向,带有额外的字段

是的,现在......你有一个普通用户今天在你的网站上发布了关于鳗鱼如何美味的帖子.这个完全陌生的人来到你的网站,注册,并写一个责任帖子对普通用户的无能.毕竟,鳗鱼是一种濒临灭绝的物种!

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

我们需要的不再是一个has_and_belongs_to_many,而是一种组合has_many,belongs_to,has_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
Run Code Online (Sandbox Code Playgroud)

请注意如何,在这种情况下,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
Run Code Online (Sandbox Code Playgroud)

这里唯一发生的事情是:class_name,这是必要的,因为Rails无法推断post_a或者post_b我们在这里处理帖子.我们必须明确告诉它.

现在的Post模型:

class Post < ActiveRecord::Base
  has_many :post_connections, :foreign_key => :post_a_id
  has_many :posts, :through => :post_connections, :source => :post_b
end
Run Code Online (Sandbox Code Playgroud)

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

通过第二个关联,我们告诉Rails我们可以通过我们的第一个关联到达其他帖子,与之相关的帖子post_connections,然后是post_b关联PostConnection.

还有一件事缺失了,那就是我们需要告诉Rails a PostConnection依赖于它所属的帖子.如果一个或两个post_a_id,并post_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
Run Code Online (Sandbox Code Playgroud)

除了语法上的细微变化外,这里有两个不同的东西:

  • has_many :post_connections有一个额外的:dependent参数.有了这个值:destroy,我们告诉Rails,一旦这个帖子消失,它就可以继续销毁这些对象.你可以在这里使用的另一个值是:delete_all,它更快,但如果你正在使用它们,则不会调用任何destroy钩子.
  • 我们已经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
Run Code Online (Sandbox Code Playgroud)

您可以创建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!">]
Run Code Online (Sandbox Code Playgroud)

我们也可以操纵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
=> []
Run Code Online (Sandbox Code Playgroud)

双向循环协会

在正常has_and_belongs_to_many关联中,关联在所涉及的两个模型中定义.这种关联是双向的.

但在这种情况下只有一个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 )
Run Code Online (Sandbox Code Playgroud)

为了使关联成为双向,我们必须找到一种方法使Rails OR成为上述条件post_a_idpost_b_id使其反转,因此它将向两个方向看.

不幸的是,我所知道的唯一方法是相当hacky.你必须使用选项来手动指定SQL has_and_belongs_to_many:finder_sql,:delete_sql等它不漂亮.(我也欢迎在这里提出建议.任何人?)


jbm*_*rom 15

回答Shteef提出的问题:

双向循环协会

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

  • 追随者以跟随者的身份
  • 以跟随者的身份跟进.

以下是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
Run Code Online (Sandbox Code Playgroud)

以下是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
Run Code Online (Sandbox Code Playgroud)

需要注意的最重要的事情可能是条款:follower_follows:followee_followsuser.rb. 例如,要使用mill(非循环)关联的运行,团队可能有很多:playersthrough :contracts.这是一个没有什么不同球员,谁可能有很多:teams通过:contracts,以及(在这样的过程中球员的职业生涯).但在这种情况下,只存在一个命名模型(即用户),以相同方式命名through:relationship(例如through: :follow,或者,就像在帖子示例中上面所做的那样through: :post_connections),将导致不同用例的命名冲突(或访问点到连接表.:follower_follows:followee_follows创建以避免这种命名冲突.现在,用户可以:followers通过:follower_follows很多:followees通过:followee_follows.

要确定用户:followees(在@user.followees调用数据库时),Rails现在可以查看class_name的每个实例:"Follow",其中User是跟随者(即foreign_key: :follower_id)通过:这样的用户:followee_follows.为了确定用户:关注者(在@user.followers调用数据库时),Rails现在可以查看class_name的每个实例:"Follow",其中User是followee(即foreign_key: :followee_id):这样的User 's:follower_follows.


hrd*_*rbl 6

如果有人来到这里试图找出如何在Rails中创建朋友关系,那么我会将它们引用到我最终决定使用的内容,即复制"社区引擎"所做的事情.

你可以参考:

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

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

欲获得更多信息.

TL; 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
Run Code Online (Sandbox Code Playgroud)

..

# friendship.rb
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
Run Code Online (Sandbox Code Playgroud)