Answers:
多对多关系有几种。您必须问自己以下问题:
这留下了四种不同的可能性。我将在下面浏览这些内容。
供参考:关于该主题的Rails文档。有一个称为“多对多”的部分,当然还有有关类方法本身的文档。
这是代码中最紧凑的代码。
我将从您的帖子的基本架构开始:
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会将此表称为我们要加入的两个表的名称的组合。但是事实证明,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
那应该工作!这是一个运行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!">]
您会发现,分配给posts
关联将在post_connections
表中适当地创建记录。
注意事项:
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
请注意如何,在这种情况下,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
协会,我们要告诉模型加入post_connections
的posts.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
除了语法上的细微变化外,这里有两个不同之处:
has_many :post_connections
有一个额外的:dependent
参数。使用value :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_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 )
为了使关联双向,我们必须找到一种使Rails OR
满足以上条件post_a_id
并post_b_id
反转的方法,因此它将在两个方向上都可以看到。
不幸的是,据我所知,唯一的方法就是hacky。你必须使用你的SQL选项来手动指定has_and_belongs_to_many
,如:finder_sql
,:delete_sql
等它不漂亮。(我也愿意在这里提出建议。有人吗?)
要回答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
这是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
需要注意的最重要的事情可能是术语:follower_follows
和:followee_follows
user.rb。以运行工厂(非循环)关联为例,一个团队可能有许多:players
至:contracts
。对于一个玩家来说,这也没有什么不同,在这个玩家的职业生涯中,他也可能有很多:teams
经历。但是在这种情况下,如果仅存在一个命名模型(即User),则对through:关系进行相同的命名(例如,或者像上面的post示例中所做的那样)会导致()的不同用例的命名冲突或访问点进入)联接表。和:contracts
through: :follow
through: :post_connections
:follower_follows
:followee_follows
是为了避免这种命名冲突而创建的。现在,用户可以有很多:followers
通过:follower_follows
和许多:followees
通过:followee_follows
。
为了确定用户的:followees(在@user.followees
调用数据库时),Rails现在可以查看class_name:“关注”的每个实例,其中,该用户通过以下方式成为关注者(即foreign_key: :follower_id
):该用户的:followee_follows。为了确定用户的:followers(在@user.followers
调用数据库时),Rails现在可以查看class_name:“关注”的每个实例,其中,该用户是以下对象的关注者(即foreign_key: :followee_id
):该用户的:follower_follows。
如果有人来这里尝试找出如何在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
..
# friendship.rb
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
受@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
应该都可以工作,至少对我有用。
对于双向belongs_to_and_has_many
,请参考已经发布的好答案,然后创建另一个具有不同名称的关联,将外键反向,并确保已class_name
设置指向正确的模型。干杯。
:foreign_key
上的has_many :through
并不是必须的,我添加了有关如何使用非常方便的:dependent
参数的说明has_many
。