Hubungan banyak-ke-banyak dengan model rel yang sama?

107

Bagaimana saya bisa membuat hubungan banyak-ke-banyak dengan model yang sama di rel?

Misalnya, setiap postingan terhubung ke banyak postingan.

Pemenang
sumber

Jawaban:

276

Ada beberapa jenis hubungan banyak-ke-banyak; Anda harus bertanya pada diri sendiri pertanyaan-pertanyaan berikut:

  • Apakah saya ingin menyimpan informasi tambahan dengan asosiasi? (Bidang tambahan di tabel gabungan.)
  • Apakah asosiasi perlu secara implisit dua arah? (Jika postingan A terhubung dengan postingan B, maka postingan B juga terhubung dengan postingan A.)

Itu menyisakan empat kemungkinan berbeda. Saya akan berjalan di bawah ini.

Untuk referensi: dokumentasi Rails tentang subjek . Ada bagian yang disebut "Banyak ke banyak", dan tentu saja dokumentasi tentang metode kelas itu sendiri.

Skenario paling sederhana, uni-directional, tanpa bidang tambahan

Ini adalah kode yang paling ringkas.

Saya akan mulai dengan skema dasar ini untuk posting Anda:

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

Untuk hubungan banyak ke banyak, Anda memerlukan tabel gabungan. Berikut skema untuk itu:

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

Secara default, Rails akan menyebut tabel ini kombinasi dari nama dua tabel yang kita gabungkan. Tapi ternyata seperti posts_postsdalam situasi ini, jadi saya memutuskan untuk mengambil post_connectionsalih.

Sangat penting di sini adalah :id => false, untuk menghilangkan idkolom default . Rails menginginkan kolom itu di mana-mana kecuali pada tabel gabungan untuk has_and_belongs_to_many. Ini akan mengeluh keras.

Terakhir, perhatikan bahwa nama kolom juga tidak standar (tidak post_id), untuk mencegah konflik.

Sekarang dalam model Anda, Anda hanya perlu memberi tahu Rails tentang beberapa hal non-standar ini. Ini akan terlihat sebagai berikut:

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

Dan itu seharusnya berhasil! Berikut adalah contoh sesi irb yang dijalankan 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!">]

Anda akan menemukan bahwa menetapkan ke postspengaitan akan membuat catatan di post_connectionstabel yang sesuai.

Beberapa hal yang perlu diperhatikan:

  • Anda dapat melihat pada sesi irb di atas bahwa asosiasinya adalah uni-directional, karena setelah itu a.posts = [b, c], keluaran dari b.poststidak menyertakan kiriman pertama.
  • Hal lain yang mungkin Anda perhatikan adalah tidak adanya model PostConnection. Anda biasanya tidak menggunakan model untuk has_and_belongs_to_manyasosiasi. Karenanya, Anda tidak dapat mengakses bidang tambahan apa pun.

Uni-directional, dengan bidang tambahan

Sekarang ... Anda memiliki pengguna biasa yang hari ini membuat posting di situs Anda tentang betapa enaknya belut. Orang asing ini datang ke situs Anda, mendaftar, dan menulis posting omelan tentang ketidakmampuan pengguna biasa. Bagaimanapun, belut adalah spesies yang terancam punah!

Jadi Anda ingin memperjelas dalam database Anda bahwa posting B adalah kata-kata kasar pada posting A. Untuk melakukan itu, Anda ingin menambahkan categoryfield ke asosiasi.

Apa yang kita butuhkan adalah tidak lagi has_and_belongs_to_many, tetapi kombinasi has_many, belongs_to, has_many ..., :through => ...dan model tambahan untuk bergabung meja. Model ekstra inilah yang memberi kita kekuatan untuk menambahkan informasi tambahan ke asosiasi itu sendiri.

Berikut skema lain, sangat mirip dengan di atas:

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

Perhatikan bagaimana, dalam situasi ini, post_connections memang memiliki idkolom. (Tidak ada :id => false parameter.) Ini diperlukan, karena akan ada model ActiveRecord biasa untuk mengakses tabel.

Saya akan mulai dengan PostConnectionmodelnya, karena sangat sederhana:

class PostConnection < ActiveRecord::Base
  belongs_to :post_a, :class_name => :Post
  belongs_to :post_b, :class_name => :Post
end

Satu-satunya hal yang terjadi di sini adalah :class_name, yang diperlukan, karena Rails tidak dapat menyimpulkan dari post_aatau post_bbahwa kita berurusan dengan Post di sini. Kami harus menceritakannya secara eksplisit.

Sekarang Postmodelnya:

class Post < ActiveRecord::Base
  has_many :post_connections, :foreign_key => :post_a_id
  has_many :posts, :through => :post_connections, :source => :post_b
end

Dengan yang pertama has_manyasosiasi, kami memberitahu model untuk bergabung post_connectionsdi posts.id = post_connections.post_a_id.

Dengan pengaitan kedua, kami memberi tahu Rails bahwa kami dapat menjangkau pos lain, yang terhubung ke pos ini, melalui pengaitan pertama kami post_connections, diikuti oleh post_bpengaitan PostConnection.

Hanya ada satu hal lagi yang hilang, dan itu adalah kita perlu memberi tahu Rails bahwa a PostConnectionbergantung pada pos yang dimilikinya. Jika salah satu atau kedua post_a_iddan post_b_iditu NULL, maka koneksi tidak akan memberitahu kita banyak, kan? Inilah cara kami melakukannya dalam Postmodel kami :

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

Selain sedikit perubahan sintaks, ada dua hal nyata yang berbeda di sini:

  • The has_many :post_connectionsmemiliki tambahan :dependentparameter. Dengan nilai tersebut :destroy, kami memberi tahu Rails bahwa, setelah postingan ini menghilang, ia dapat melanjutkan dan menghancurkan objek ini. Nilai alternatif yang dapat Anda gunakan di sini adalah :delete_all, yang lebih cepat, tetapi tidak akan memanggil kait penghancur jika Anda menggunakannya.
  • Kami telah menambahkan has_manyasosiasi untuk koneksi balik juga, yang telah menghubungkan kami post_b_id. Dengan cara ini, Rails juga dapat menghancurkannya dengan rapi. Perhatikan bahwa kita harus menentukan di :class_namesini, karena nama kelas model tidak lagi dapat disimpulkan :reverse_post_connections.

Dengan ini, saya membawakan Anda sesi irb lainnya melalui 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

Daripada membuat pengaitan dan kemudian menyetel kategori secara terpisah, Anda juga dapat membuat PostConnection dan selesai dengannya:

>> 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!">]

Dan kami juga dapat memanipulasi asosiasi post_connectionsdan reverse_post_connections; itu akan tercermin dengan rapi dalam postsasosiasi:

>> 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
=> []

Asosiasi melingkar dua arah

Dalam has_and_belongs_to_manyasosiasi normal , asosiasi ditentukan dalam kedua model yang terlibat. Dan pengaitannya dua arah.

Tapi hanya ada satu model Post dalam kasus ini. Dan pengaitannya hanya ditentukan sekali. Itulah mengapa dalam kasus khusus ini, pengaitan bersifat searah.

Hal yang sama berlaku untuk metode alternatif dengan has_manydan model untuk tabel gabungan.

Ini paling baik dilihat ketika hanya mengakses asosiasi dari irb, dan melihat SQL yang dihasilkan Rails di file log. Anda akan menemukan sesuatu seperti berikut ini:

SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )

Untuk membuat pengaitan dua arah, kita harus menemukan cara untuk membuat Rails ORdengan kondisi di atas post_a_iddan post_b_iddibalik, sehingga Rails akan terlihat di kedua arah.

Sayangnya, satu-satunya cara untuk melakukan ini yang saya tahu agak hacky. Anda harus menentukan secara manual SQL Anda menggunakan pilihan untuk has_and_belongs_to_manyseperti :finder_sql, :delete_sql, dll Ini tidak cantik. (Saya juga terbuka untuk saran di sini. Siapapun?)

Shtéf
sumber
Terima kasih atas komentar bagusnya! :) Saya telah melakukan beberapa pengeditan lebih lanjut. Secara khusus, :foreign_keypada has_many :throughtidak diperlukan, dan saya menambahkan penjelasan tentang cara menggunakan :dependentparameter yang sangat berguna untuk has_many.
Stéphan Kochen
@ Shtééf bahkan penugasan massal (update_attributes) tidak akan berfungsi dalam kasus asosiasi dua arah misalnya: postA.update_attributes ({: post_b_ids => [2,3,4]}) ada ide atau solusi?
Lohith MV
Jawaban yang sangat bagus kawan 5.kali {menempatkan "+1"}
Rahul
@ Shtééf Saya belajar banyak dari jawaban ini, terima kasih! Saya mencoba untuk bertanya dan menjawab pertanyaan asosiasi dua arah Anda di sini: stackoverflow.com/questions/25493368/…
jbmilgrom
17

Untuk menjawab pertanyaan yang diajukan oleh Shteef:

Asosiasi melingkar dua arah

Hubungan pengikut-pengikut di antara Pengguna adalah contoh yang baik dari asosiasi loop dua arah. Seorang Pengguna dapat memiliki banyak:

  • pengikut dalam kapasitasnya sebagai berikut
  • pengikut dalam kapasitasnya sebagai pengikut.

Berikut tampilan kode untuk 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

Begini cara kode untuk 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

Hal terpenting yang perlu diperhatikan mungkin adalah istilah :follower_followsdan :followee_followsdi user.rb. Untuk menggunakan asosiasi run of the mill (non-looped) sebagai contoh, sebuah Tim mungkin memiliki banyak: playersmelalui :contracts. Ini tidak berbeda untuk seorang Pemain , yang mungkin juga telah :teamsmelalui banyak hal :contracts(selama karir Pemain tersebut ). Tapi dalam kasus ini, di mana hanya ada satu model bernama (yaitu Pengguna ), penamaan melalui: hubungan identik (misalnya through: :follow, atau, seperti yang dilakukan di atas dalam contoh posting, through: :post_connections) akan mengakibatkan benturan penamaan untuk kasus penggunaan yang berbeda dari ( atau titik akses ke) tabel gabungan. :follower_followsdan:followee_followsdiciptakan untuk menghindari tabrakan penamaan seperti itu. Sekarang, Pengguna dapat memiliki banyak :followersmelalui :follower_followsdan banyak :followeesmelalui :followee_follows.

Untuk menentukan User 's: followees (setelah @user.followeespanggilan ke database), Rails sekarang dapat melihat setiap instance class_name: "Follow" dimana User tersebut adalah follower (yaitu foreign_key: :follower_id) melalui: User tersebut : followee_follows. Untuk menentukan Pengguna : pengikut (setelah @user.followerspanggilan ke database), Rails sekarang dapat melihat setiap contoh class_name: "Ikuti" di mana Pengguna tersebut adalah yang mengikuti (yaitu foreign_key: :followee_id) melalui: User seperti : follower_follows.

jbmilgrom.dll
sumber
1
Persis yang saya butuhkan! Terima kasih! (Saya sarankan juga mendaftar migrasi database; Saya harus mengumpulkan info itu dari jawaban yang diterima)
Adam Denoon
6

Jika ada yang datang ke sini untuk mencoba mencari tahu cara membuat hubungan pertemanan di Rails, saya akan merujuk mereka ke apa yang akhirnya saya putuskan untuk digunakan, yaitu meniru apa yang dilakukan 'Community Engine'.

Anda bisa merujuk ke:

https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb

dan

https://github.com/bborn/communityengine/blob/master/app/models/user.rb

untuk informasi lebih lanjut.

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"
hrdwdmrbl
sumber
2

Terinspirasi oleh @ Stéphan Kochen, ini bisa bekerja untuk asosiasi dua arah

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

lalu post.posts&& post.reversed_postsharus keduanya berfungsi, setidaknya berhasil untuk saya.

Alba Hoo
sumber
1

Untuk dua arah belongs_to_and_has_many, lihat jawaban bagus yang sudah diposting, dan kemudian buat asosiasi lain dengan nama berbeda, kunci asing dibalik dan pastikan bahwa Anda telah class_namemengatur untuk menunjuk kembali ke model yang benar. Bersulang.

Zhenya Slabkovski
sumber
2
Bisakah Anda menunjukkan contoh di posting Anda? Saya telah mencoba berbagai cara seperti yang Anda sarankan, tetapi tampaknya tidak berhasil.
achabacha322
0

Jika ada yang kesulitan mendapatkan jawaban terbaik untuk bekerja, seperti:

(Objek tidak mendukung #inspect)
=>

atau

NoMethodError: metode tak terdefinisi `split 'untuk: Mission: Symbol

Maka solusinya adalah mengganti :PostConnectiondengan "PostConnection", mengganti nama kelas Anda tentunya.

pengguna2303277
sumber