Jawaban:
Ada beberapa jenis hubungan banyak-ke-banyak; Anda harus bertanya pada diri sendiri pertanyaan-pertanyaan berikut:
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.
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_posts
dalam situasi ini, jadi saya memutuskan untuk mengambil post_connections
alih.
Sangat penting di sini adalah :id => false
, untuk menghilangkan id
kolom 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 posts
pengaitan akan membuat catatan di post_connections
tabel yang sesuai.
Beberapa hal yang perlu diperhatikan:
a.posts = [b, c]
, keluaran dari b.posts
tidak menyertakan kiriman pertama.PostConnection
. Anda biasanya tidak menggunakan model untuk has_and_belongs_to_many
asosiasi. Karenanya, Anda tidak dapat mengakses bidang tambahan apa pun.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 category
field 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 id
kolom. (Tidak ada :id => false
parameter.) Ini diperlukan, karena akan ada model ActiveRecord biasa untuk mengakses tabel.
Saya akan mulai dengan PostConnection
modelnya, 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_a
atau post_b
bahwa kita berurusan dengan Post di sini. Kami harus menceritakannya secara eksplisit.
Sekarang Post
modelnya:
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_many
asosiasi, kami memberitahu model untuk bergabung post_connections
di 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_b
pengaitan PostConnection
.
Hanya ada satu hal lagi yang hilang, dan itu adalah kita perlu memberi tahu Rails bahwa a PostConnection
bergantung pada pos yang dimilikinya. Jika salah satu atau kedua post_a_id
dan post_b_id
itu NULL
, maka koneksi tidak akan memberitahu kita banyak, kan? Inilah cara kami melakukannya dalam Post
model 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:
has_many :post_connections
memiliki tambahan :dependent
parameter. 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.has_many
asosiasi 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_name
sini, 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_connections
dan reverse_post_connections
; itu akan tercermin dengan rapi dalam posts
asosiasi:
>> 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
=> []
Dalam has_and_belongs_to_many
asosiasi 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_many
dan 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 OR
dengan kondisi di atas post_a_id
dan post_b_id
dibalik, 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_many
seperti :finder_sql
, :delete_sql
, dll Ini tidak cantik. (Saya juga terbuka untuk saran di sini. Siapapun?)
Untuk menjawab pertanyaan yang diajukan oleh Shteef:
Hubungan pengikut-pengikut di antara Pengguna adalah contoh yang baik dari asosiasi loop dua arah. Seorang Pengguna dapat memiliki banyak:
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_follows
dan :followee_follows
di user.rb. Untuk menggunakan asosiasi run of the mill (non-looped) sebagai contoh, sebuah Tim mungkin memiliki banyak: players
melalui :contracts
. Ini tidak berbeda untuk seorang Pemain , yang mungkin juga telah :teams
melalui 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_follows
dan:followee_follows
diciptakan untuk menghindari tabrakan penamaan seperti itu. Sekarang, Pengguna dapat memiliki banyak :followers
melalui :follower_follows
dan banyak :followees
melalui :followee_follows
.
Untuk menentukan User 's: followees (setelah @user.followees
panggilan 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.followers
panggilan 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.
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"
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_posts
harus keduanya berfungsi, setidaknya berhasil untuk saya.
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_name
mengatur untuk menunjuk kembali ke model yang benar. Bersulang.
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 :PostConnection
dengan "PostConnection"
, mengganti nama kelas Anda tentunya.
:foreign_key
padahas_many :through
tidak diperlukan, dan saya menambahkan penjelasan tentang cara menggunakan:dependent
parameter yang sangat berguna untukhas_many
.