与轨道中的同一模型的多对多关系?

我怎样才能与轨道中的同一模型多对多的关系?

例如,每个post都连接到许多post。

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

  • 我想要与协会存储更多信息吗? (连接表中的其他字段。)
  • 这些关联是否需要隐式双向? (如果职位A连接到职位B,则职位B也连接到职位A.)

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

供参考: 关于这个主题的Rails文档 。 有一个叫做“多对多”的部分,当然还有关于类方法本身的文档。

最简单的情况下,单向,没有额外的领域

这是代码中最紧凑的。

我将从您的post的基本模式开始:

create_table "posts", :force => true do |t| t.string "name", :null => false end 

对于任何多对多的关系,你需要一个连接表。 以下是这个模式:

 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会调用这个表格,我们join的两个表的名字的组合。 但是,在这种情况下,这将成为posts_posts ,所以我决定采取post_connections

这里非常重要的是:id => false ,省略默认的id列。 除了 has_and_belongs_to_many连接表之外 ,Rails需要这个列。 它会大声抱怨。

最后,注意列名也是非标准的(不是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表中创buildlogging。

有些事情要注意:

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

单向,附加字段

对,现在…你已经有一个普通用户今天在你的网站发表了一篇关于鳗鱼好吃的post。 这个完全陌生的人来到你的网站,注册,并写一个普通用户的无能的责骂。 毕竟,鳗鱼是一种濒危物种!

所以你想在你的数据库中明确指出,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上joinposts.id = post_connections.post_a_id

通过第二个协会,我们告诉Rails,我们可以通过我们的第一个关联post_connectionspost_b连接PostConnection来到达与这个关联的其他职位。

还有一件事缺失了,那就是我们需要告诉Rails一个PostConnection依赖于它所属的post。 如果post_a_idpost_b_id中的一个或两个都是NULL ,那么这个连接就不会告诉我们多less,是吗? 以下是我们在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,一旦这个post消失了,它就可以继续并且销毁这些对象。 你可以在这里使用的另一个值是: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 

而不是创build关联,然后分别设置类别,您也可以创build一个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关联中,关联在两个模型中被定义。 而且这个协会是双向的。

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

为了使这个关联成为双向的,我们必须find一种方法来使Rails OR上面的条件与post_a_idpost_b_id颠倒过来,所以它将在两个方向上看起来。

不幸的是,我所知道的唯一方法是相当黑客。 你必须手动指定你的SQL使用选项has_and_belongs_to_many ,如:finder_sql:delete_sql等,这不是很好。 (我也接受这方面的build议,任何人?)

为了回答Shteef提出的问题:

双向循环关联

用户间的follower-followee关系是一个双向循环关联的好例子。 一个用户可以有很多:

  • 追随者作为其追随者
  • 作为追随者的追随者。

以下是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 

以下是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 

要注意的最重要的事情可能是:followee_follows中的:follower_follows:followee_follows 。 以一个工厂(非循环)关联为例,一个团队可能有许多players :contracts 。 对于一个可能有很多球员的球员来说也是如此:teams通过:contracts (在球员的职业生涯中)。 但在这种情况下,只存在一个命名模型(即用户 ),命名through:关系相同(例如through: :follow ,或者像上面在post的例子中所做的那样through: :post_connections )会导致为连接表的不同使用情况命名冲突(或访问点)。 :follower_follows:followee_follows是为了避免这种命名冲突而创build的。 现在,一个用户可以通过:follower_follows:followees:followee_follows多个:followee_follows

为了确定一个用户 :followees(在@user.followees对数据库的调用),Rails现在可以通过以下方式查看每个class_name的实例:“Follow”,其中这个用户是跟随者(即foreign_key: :follower_id )这样的用户 :followee_follows。 为了确定一个用户 :followers(在@user.followers调用数据库时),Rails现在可以通过以下方式查看每个class_name的实例:“Follow”,其中这个用户是followee(即foreign_key: :followee_id )这样的用户 :follower_follows。

如果有人来这里试图找出如何在Rails中build立朋友关系,那么我会把他们引用到我最终决定使用的东西,即复制“社区引擎”所做的事情。

你可以参考:

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 

..

 # friendship.rb belongs_to :user belongs_to :friend, :class_name => "User", :foreign_key => "friend_id" 

对于双向的belongs_to_and_has_many ,请参阅已发布的重要答案,然后使用不同的名称创build另一个关联,将外键颠倒并确保将class_name设置为指向正确的模型。 干杯。

如果任何人有问题得到良好的工作答案,如:

(对象不支持#inspect)
=>

要么

NoMethodError:未定义的方法`拆分'为:任务:符号

然后解决scheme是replace:PostConnection"PostConnection" ,当然replace你的类名。

受@StéphanKochen启发,这可以用于双向关联

 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_name => Post, :join_table => "post_connections", :foreign_key => "post_b_id", :association_foreign_key => "post_a_id") end 

那么post.posts && post.reversed_posts都应该工作,至less为我工作。