Mối quan hệ nhiều-nhiều với cùng một mô hình trong đường ray?


107

Làm cách nào để tạo mối quan hệ nhiều-nhiều với cùng một mô hình trong đường ray?

Ví dụ, mỗi bài đăng được kết nối với nhiều bài đăng.

Câu trả lời:


276

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:

  • Tôi có muốn lưu trữ thông tin bổ sung với hiệp hội không? (Các trường bổ sung trong bảng tham gia.)
  • Các hiệp hội có cần được ngầm định hai chiều không? (Nếu bài A nối với bài B thì bài B cũng được nối với bài A.)

Đ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.

Kịch bản đơn giản nhất, đơn hướng, không có trường bổ sung

Đâ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_poststrong tình huống này, vì vậy tôi quyết định post_connectionsthay thế.

Điều rất quan trọng ở đây là :id => falsebỏ qua idcộ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 postsliên kết sẽ tạo các bản ghi trong post_connectionsbảng nếu thích hợp.

Một số điều cần lưu ý:

  • Bạn có thể thấy trong phiên irb ở trên rằng liên kết là đơn hướng, vì sau đó a.posts = [b, c], đầu ra của b.postskhông bao gồm bài đăng đầu tiên.
  • Một điều khác bạn có thể nhận thấy là không có mô hình PostConnection. Bạn thường không sử dụng mô hình cho một has_and_belongs_to_manyhiệ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.

Đơn hướng, với các trường bổ sung

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 categorytrườ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 idcột. (Không :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 PostConnectionmô 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_ahay post_brằ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à Postmô 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_manyhiệp hội đầu tiên , chúng tôi yêu cầu người mẫu tham gia post_connectionstiế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_bliê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 PostConnectionphụ 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_idpost_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 Postmô 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_connectionsmột :dependenttham 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.
  • Chúng tôi cũng đã thêm một has_manyliê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_connectionsreverse_post_connectionsliên kết; nó sẽ phản ánh gọn gàng trong postsliê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
=> []

Liên kết lặp lại hai hướng

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 ORvới các điều kiện trên vớipost_a_idpost_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_manynhư :finder_sql, :delete_sqlvv Nó không đẹp. (Tôi cũng sẵn sàng nhận các đề xuất ở đây. Có ai không?)


Cảm ơn các ý kiến ​​đẹp! :) Tôi đã thực hiện một số chỉnh sửa thêm. Cụ thể, :foreign_keytrên has_many :throughlà không cần thiết và tôi đã thêm giải thích về cách sử dụng :dependenttham số rất tiện dụng cho has_many.
Stéphan Kochen

@ Shtééf chỉ định hàng loạt thậm chí (update_attributes) sẽ không hoạt động trong trường hợp liên kết hai chiều, ví dụ: postA.update_attributes ({: post_b_ids => [2,3,4]}) bất kỳ ý tưởng hoặc cách giải quyết nào?
Lohith MV

Người bạn đời có câu trả lời rất hay. 5. lần {đặt "+1"}
Rahul

@ Shtééf Tôi đã học được rất nhiều từ câu trả lời này, cảm ơn bạn! Tôi cố gắng để hỏi và trả lời câu hỏi bi-directional hiệp hội của mình tại đây: stackoverflow.com/questions/25493368/...
jbmilgrom

17

Để trả lời câu hỏi do Shteef đặt ra:

Liên kết lặp lại hai hướng

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:

  • người theo dõi với tư cách là người theo dõi
  • người theo dõi với tư cách là người theo dõi.

Đâ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:followee_followstrong 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: playersthô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ể :teamstrả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: :followhoặ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: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 :followersthông qua :follower_followsvà nhiều :followeesthông qua :followee_follows.

Để xác định Người dùng : người theo dõi (khi @user.followeesgọ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.followersgọ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.


1
Chính xác những gì tôi cần! Cảm ơn! (Tôi cũng khuyên bạn nên liệt kê các lần di chuyển cơ sở dữ liệu; Tôi đã phải thu thập thông tin đó từ câu trả lời được chấp nhận)
Adam Denoon

6

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

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"

2

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_postsnên cả hai đều hoạt động, ít nhất là hiệu quả với tôi.


1

Đố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.


2
Bạn có thể cho một ví dụ trong bài viết của bạn? Tôi đã thử nhiều cách như bạn đề xuất, nhưng dường như không hiệu quả.
achabacha322

0

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ế :PostConnectionbằng "PostConnection", thay thế tên lớp của bạn tất nhiên.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.