Câu trả lời:
Có một số loại mối quan hệ nhiều-nhiều; bạn phải tự hỏi mình những câu hỏi sau:
Điều đó để lại bốn khả năng khác nhau. Tôi sẽ đi qua những điều này bên dưới.
Để tham khảo: tài liệu Rails về chủ đề này . Có một phần được gọi là "Nhiều-nhiều", và tất nhiên là tài liệu về chính các phương thức của lớp.
Đây là mã nhỏ gọn nhất.
Tôi sẽ bắt đầu với giản đồ cơ bản này cho các bài đăng của bạn:
create_table "posts", :force => true do |t|
t.string "name", :null => false
end
Đối với bất kỳ mối quan hệ nhiều-nhiều nào, bạn cần có một bảng tham gia. Đây là giản đồ cho điều đó:
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
Theo mặc định, Rails sẽ gọi bảng này là sự kết hợp của tên của hai bảng mà chúng ta đang tham gia. Nhưng điều đó sẽ xảy ra posts_posts
trong tình huống này, vì vậy tôi quyết định post_connections
thay thế.
Điều rất quan trọng ở đây là :id => false
bỏ qua id
cột mặc định . Rails muốn cột đó ở mọi nơi ngoại trừ trên các bảng tham gia cho has_and_belongs_to_many
. Nó sẽ kêu ca ầm ĩ.
Cuối cùng, lưu ý rằng tên cột cũng không phải là tiêu chuẩn (không phải post_id
), để tránh xung đột.
Bây giờ trong mô hình của bạn, bạn chỉ cần nói với Rails về vài điều không chuẩn này. Nó sẽ trông như sau:
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
Và điều đó sẽ đơn giản hoạt động! Đây là một phiên irb ví dụ chạy qua 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!">]
Bạn sẽ thấy rằng việc gán cho posts
liên kết sẽ tạo các bản ghi trong post_connections
bảng nếu thích hợp.
Một số điều cần lưu ý:
a.posts = [b, c]
, đầu ra của b.posts
không bao gồm bài đăng đầu tiên.PostConnection
. Bạn thường không sử dụng mô hình cho một has_and_belongs_to_many
hiệp hội. Vì lý do này, bạn sẽ không thể truy cập bất kỳ trường bổ sung nào.Ngay bây giờ ... Bạn có một người dùng thường xuyên đã đăng bài trên trang web của bạn về cách ăn lươn ngon. Người hoàn toàn xa lạ này đến thăm trang web của bạn, đăng ký và viết một bài đăng trách móc về thái độ thiếu cẩn trọng của người dùng thông thường. Xét cho cùng, lươn là một loài có nguy cơ tuyệt chủng!
Vì vậy, bạn muốn làm rõ trong cơ sở dữ liệu của mình rằng bài đăng B là một lời mắng nhiếc trên bài A. Để làm điều đó, bạn muốn thêm một category
trường vào liên kết.
Những gì chúng ta cần là không còn là một has_and_belongs_to_many
, nhưng là một sự kết hợp của has_many
, belongs_to
, has_many ..., :through => ...
và một mô hình bổ sung cho bảng tham gia. Mô hình bổ sung này là thứ cho chúng tôi sức mạnh để thêm thông tin bổ sung vào chính hiệp hội.
Đây là một lược đồ khác, rất giống với lược đồ trên:
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
Thông báo như thế nào, trong tình huống này, post_connections
không có một id
cột. (Không có :id => false
tham số.) Điều này là bắt buộc, vì sẽ có một mô hình ActiveRecord thông thường để truy cập bảng.
Tôi sẽ bắt đầu với PostConnection
mô hình, vì nó rất đơn giản:
class PostConnection < ActiveRecord::Base
belongs_to :post_a, :class_name => :Post
belongs_to :post_b, :class_name => :Post
end
Điều duy nhất đang xảy ra ở đây là :class_name
, điều này là cần thiết, bởi vì Rails không thể suy ra từ post_a
hay post_b
rằng chúng ta đang xử lý một Bài đăng ở đây. Chúng tôi phải nói với nó một cách rõ ràng.
Bây giờ là Post
mô hình:
class Post < ActiveRecord::Base
has_many :post_connections, :foreign_key => :post_a_id
has_many :posts, :through => :post_connections, :source => :post_b
end
Với has_many
hiệp hội đầu tiên , chúng tôi yêu cầu người mẫu tham gia post_connections
tiếp posts.id = post_connections.post_a_id
.
Với liên kết thứ hai, chúng tôi đang nói với Rails rằng chúng tôi có thể tiếp cận các bài đăng khác, những bài được kết nối với liên kết này, thông qua liên kết đầu tiên của chúng tôi post_connections
, tiếp theo là post_b
liên kết của PostConnection
.
Chỉ còn thiếu một điều nữa , và đó là chúng ta cần nói với Rails rằng a PostConnection
phụ thuộc vào các bài viết mà nó thuộc về. Nếu một hoặc cả hai post_a_id
và post_b_id
đều NULL
, thì mối liên hệ đó sẽ không cho chúng ta biết nhiều, phải không? Đây là cách chúng tôi làm điều đó trong Post
mô hình của mình:
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
Bên cạnh sự thay đổi nhỏ về cú pháp, hai điều thực sự khác nhau ở đây:
has_many :post_connections
một :dependent
tham số phụ . Với giá trị :destroy
, chúng tôi nói với Rails rằng, khi bài đăng này biến mất, nó có thể tiếp tục và phá hủy các đối tượng này. Một giá trị thay thế mà bạn có thể sử dụng ở đây là :delete_all
, nhanh hơn, nhưng sẽ không gọi bất kỳ móc hủy nào nếu bạn đang sử dụng chúng.has_many
liên kết cho các kết nối ngược lại , những liên kết đã liên kết chúng tôi với nhau post_b_id
. Bằng cách này, Rails cũng có thể phá hủy chúng một cách gọn gàng. Lưu ý rằng chúng ta phải chỉ định :class_name
ở đây, bởi vì tên lớp của mô hình không còn có thể được suy ra từ :reverse_post_connections
.Với điều này tại chỗ, tôi mang đến cho bạn một phiên irb khác thông qua script/console
:
>> 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
Thay vì tạo liên kết và sau đó đặt danh mục riêng biệt, bạn cũng có thể chỉ tạo PostConnection và thực hiện với nó:
>> 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!">]
Và chúng ta cũng có thể thao túng các post_connections
và reverse_post_connections
liên kết; nó sẽ phản ánh gọn gàng trong posts
liên kết:
>> 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
=> []
Bình thường has_and_belongs_to_many
hiệp hội , hiệp hội được xác định trong cả hai mô hình liên quan. Và sự liên kết là hai chiều.
Nhưng chỉ có một mô hình Đăng trong trường hợp này. Và hiệp hội chỉ được chỉ định một lần. Đó chính xác là lý do tại sao trong trường hợp cụ thể này, các liên kết là đơn hướng.
Điều này cũng đúng đối với phương pháp thay thế với has_many
và một mô hình cho bảng nối.
Điều này được thấy rõ nhất khi chỉ cần truy cập các liên kết từ irb và xem SQL mà Rails tạo ra trong tệp nhật ký. Bạn sẽ tìm thấy một cái gì đó như sau:
SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )
Để thực hiện liên kết hai chiều, chúng ta phải tìm cách tạo Rails OR
với các điều kiện trên vớipost_a_id
và post_b_id
đảo ngược, vì vậy nó sẽ nhìn theo cả hai hướng.
Thật không may, cách duy nhất để làm điều này mà tôi biết là khá hacky. Bạn sẽ phải tự xác định SQL của bạn sử dụng tùy chọn để has_and_belongs_to_many
như :finder_sql
, :delete_sql
vv Nó không đẹp. (Tôi cũng sẵn sàng nhận các đề xuất ở đây. Có ai không?)
Để trả lời câu hỏi do Shteef đặt ra:
Mối quan hệ người theo dõi-người theo dõi giữa Người dùng là một ví dụ điển hình về mối liên kết lặp lại theo hai hướng. Một tài khoản có thể có nhiều:
Đây là cách mã cho user.rb có thể trông như thế nào :
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
Đây là cách mã cho 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
Những điều quan trọng nhất cần lưu ý có lẽ là các điều khoản :follower_follows
và :followee_follows
trong user.rb. Để sử dụng một loạt liên kết (không lặp lại) làm ví dụ, một Nhóm có thể có nhiều: players
thông qua :contracts
. Điều này không khác gì đối với một Người chơi , người cũng có thể :teams
trải qua nhiều lần :contracts
(trong suốt sự nghiệp của Người chơi đó). Nhưng trong trường hợp này, khi chỉ có một mô hình được đặt tên tồn tại (tức là Người dùng ), việc đặt tên cho mối quan hệ qua: giống hệt nhau (ví dụ: through: :follow
hoặc, như đã được thực hiện ở trên trong ví dụ bài viết, through: :post_connections
) sẽ dẫn đến xung đột đặt tên cho các trường hợp sử dụng khác nhau của ( hoặc điểm truy cập vào) bảng tham gia. :follower_follows
và:followee_follows
được tạo ra để tránh va chạm đặt tên như vậy. Bây giờ, một Người dùng có thể có nhiều :followers
thông qua :follower_follows
và nhiều :followees
thông qua :followee_follows
.
Để xác định Người dùng : người theo dõi (khi @user.followees
gọi đến cơ sở dữ liệu), Rails bây giờ có thể xem xét từng phiên bản của class_name: “Follow” trong đó Người dùng đó là người theo dõi (tức là foreign_key: :follower_id
) thông qua: such Người dùng : followee_follows. Để xác định Người dùng : người theo dõi (khi @user.followers
gọi đến cơ sở dữ liệu), Rails giờ có thể xem xét từng trường hợp của class_name: “Theo dõi” trong đó Người dùng đó là người theo dõi (tức là foreign_key: :followee_id
) thông qua: Người dùng đó: follower_follows.
Nếu ai đó đến đây để cố gắng tìm hiểu cách tạo mối quan hệ bạn bè trong Rails, thì tôi sẽ giới thiệu với họ về thứ mà cuối cùng tôi quyết định sử dụng, đó là sao chép những gì 'Community Engine' đã làm.
Bạn có thể tham khảo:
https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb
và
https://github.com/bborn/communityengine/blob/master/app/models/user.rb
để biết thêm thông tin.
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"
Lấy cảm hứng từ @ Stéphan Kochen, điều này có thể hoạt động cho các hiệp hội hai chiều
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
thì post.posts
&& post.reversed_posts
nên cả hai đều hoạt động, ít nhất là hiệu quả với tôi.
Đối với hai hướng belongs_to_and_has_many
, hãy tham khảo câu trả lời tuyệt vời đã được đăng, sau đó tạo một liên kết khác với tên khác, các khóa ngoài đảo ngược và đảm bảo rằng bạn đã class_name
đặt để trỏ trở lại đúng mô hình. Chúc mừng.
Nếu bất kỳ ai có vấn đề về việc nhận được câu trả lời tuyệt vời để làm việc, chẳng hạn như:
(Đối tượng không hỗ trợ #inspect)
=>
hoặc là
NoMethodError: phương thức không xác định `` split '' cho: Nhiệm vụ: Biểu tượng
Sau đó, giải pháp là thay thế :PostConnection
bằng "PostConnection"
, thay thế tên lớp của bạn tất nhiên.
:foreign_key
trênhas_many :through
là không cần thiết và tôi đã thêm giải thích về cách sử dụng:dependent
tham số rất tiện dụng chohas_many
.