与轨道中的同一模型的多对多关系?
我怎样才能与轨道中的同一模型多对多的关系?
例如,每个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_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 
 请注意,在这种情况下, 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_a或post_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_connections和post_b连接PostConnection来到达与这个关联的其他职位。 
 还有一件事缺失了,那就是我们需要告诉Rails一个PostConnection依赖于它所属的post。 如果post_a_id和post_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_connections和reverse_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_id和post_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为我工作。