Raylarda aynı modelle çoktan çoğa ilişki?


107

Raylarda aynı modelle nasıl çoka çok ilişki kurabilirim?

Örneğin, her gönderi birçok gönderiye bağlıdır.

Yanıtlar:


276

Çoktan çoğa pek çok ilişki türü vardır; kendinize şu soruları sormalısınız:

  • Dernekle ek bilgi depolamak istiyor muyum? (Birleştirme tablosundaki ek alanlar.)
  • Derneklerin dolaylı olarak çift yönlü olması gerekiyor mu? (Eğer A postası B postasına bağlıysa, B postası da A postasına bağlıdır.)

Bu, dört farklı olasılık bırakır. Aşağıda bunların üzerinden yürüyeceğim.

Referans için: konuyla ilgili Rails belgeleri . "Çoktan çoğa" adlı bir bölüm ve elbette sınıf yöntemlerinin kendileriyle ilgili belgeler var.

En basit senaryo, tek yönlü, ek alan yok

Bu, kod açısından en kompakt olanıdır.

Gönderileriniz için bu temel şema ile başlayacağım:

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

Çoktan çoğa herhangi bir ilişki için bir birleştirme tablosuna ihtiyacınız vardır. İşte bunun için şema:

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

Varsayılan olarak, Rails bu tabloya katıldığımız iki tablonun adlarının bir kombinasyonu diyecektir. Ama bu posts_posts, bu durumda olduğu gibi ortaya çıkacaktı , ben de post_connectionsonun yerine almaya karar verdim .

Burada çok önemli olan :id => false, varsayılan idsütunun çıkarılmasıdır. Rails, bu sütunu birleştirme tabloları dışında her yerde istiyor has_and_belongs_to_many. Yüksek sesle şikayet edecek.

Son olarak, post_idçakışmayı önlemek için sütun adlarının da standart olmadığına (değil ) dikkat edin.

Şimdi modelinizde, Rails'e bu standart olmayan birkaç şeyi anlatmanız yeterlidir. Aşağıdaki gibi görünecek:

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

Ve bu basitçe çalışmalı! İşte üzerinden çalıştırılan bir irb oturumu örneği 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İlişkilendirmeye atamanın , post_connectionstabloda uygun şekilde kayıtlar oluşturacağını göreceksiniz .

Dikkat edilmesi gereken bazı noktalar:

  • Yukarıdaki irb oturumunda, ilişkilendirmenin tek yönlü olduğunu görebilirsiniz, çünkü sonrasındaki a.posts = [b, c]çıktı b.postsilk gönderiyi içermez.
  • Fark etmiş olabileceğiniz başka bir şey de modelin olmadığıdır PostConnection. Normalde bir has_and_belongs_to_manyilişkilendirme için modeller kullanmazsınız . Bu nedenle, herhangi bir ek alana erişmeniz mümkün olmayacaktır.

Ek alanlarla tek yönlü

Doğru, şimdi ... Bugün sitenizde yılan balıklarının ne kadar lezzetli olduğu hakkında bir gönderi yapan düzenli bir kullanıcınız var. Bu tamamen yabancı, sitenize gelir, kaydolur ve normal kullanıcının beceriksizliği hakkında bir azarlama yazısı yazar. Sonuçta yılanbalıkları nesli tükenmekte olan bir türdür!

Bu nedenle, veritabanınızda B gönderisinin A gönderisine yönelik bir azarlama sözü olduğunu açıkça belirtmek istersiniz. Bunu yapmak categoryiçin, ilişkiye bir alan eklemek istersiniz .

İhtiyacımız artık olup has_and_belongs_to_many, ancak bir arada has_many, belongs_to, has_many ..., :through => ...ve ekstra modeli masaya katılmak. Bu ekstra model, bize derneğin kendisine ek bilgi ekleme gücü veren şeydir.

İşte yukarıdakine çok benzeyen başka bir şema:

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

Uyarı bu durumda, nasıl, post_connections yok bir var idsütunu. ( Parametre yoktur :id => false .) Bu gereklidir, çünkü tabloya erişim için normal bir ActiveRecord modeli olacaktır.

Modelle başlayacağım PostConnectionçünkü çok basit:

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

Burada olan tek şey :class_name, gerekli olan, çünkü Rails bir Gönderiden post_aveya post_bburada bir Gönderiyle uğraştığımız sonucunu çıkaramaz . Açıkça söylemeliyiz.

Şimdi Postmodel:

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

İlk ile has_manydernek, biz katılmak için modelini anlatmak post_connectionsüzerine posts.id = post_connections.post_a_id.

İkinci dernek ile, diğer mesajları ulaşabilmesi Rayları anlatıyorsun, bu birine bağlı olanlar, bizim ilk dernek aracılığıyla post_connections, ardından post_bderneği PostConnection.

Orada sadece var bir şey daha eksik ve bu biz bu Rayları anlatmak gerektiğidir PostConnectionait olduğu mesajların bağlıdır. Birinin ya da her ikisinin ise post_a_idve post_b_idvardı NULL, sonra bağlantı çok bize, ederdi olmaz? Bunu Postmodelimizde şu şekilde yapıyoruz :

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

Sözdizimindeki küçük değişikliğin yanı sıra, burada iki gerçek şey farklıdır:

  • has_many :post_connectionsEkstra sahiptir :dependentparametre. Değer ile :destroy, Rails'e bu gönderi ortadan kalktığında devam edip bu nesneleri yok edebileceğini söylüyoruz. Burada kullanabileceğiniz alternatif bir değer :delete_all, daha hızlı olan, ancak bunları kullanıyorsanız herhangi bir yok etme kancası çağırmayacaktır.
  • Bizi bağlayan ters bağlantılar has_manyiçin de bir dernek ekledik . Bu şekilde, Rails bunları da düzgün bir şekilde yok edebilir. Burada belirtmemiz gerektiğini unutmayın , çünkü modelin sınıf adı artık buradan çıkarılamaz .post_b_id:class_name:reverse_post_connections

Bunu yerine getirerek, size başka bir IRB seansı getiriyorum 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

İlişkilendirmeyi oluşturmak ve ardından kategoriyi ayrı ayrı ayarlamak yerine, sadece bir Bağlantı Sonrası oluşturabilir ve bununla işinizi bitirebilirsiniz:

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

Ayrıca post_connectionsve reverse_post_connectionsilişkilerini de manipüle edebiliriz ; postsderneğe düzgün bir şekilde yansıyacaktır :

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

Çift yönlü döngülü ilişkilendirmeler

Normal has_and_belongs_to_manyilişkilendirmelerde, ilişkilendirme ilgili her iki modelde de tanımlanır . Ve dernek iki yönlüdür.

Ancak bu durumda sadece bir Post modeli var. Ve ilişkilendirme yalnızca bir kez belirtilir. İşte tam da bu yüzden bu özel durumda, ilişkilendirmeler tek yönlüdür.

Aynısı ile alternatif yöntem has_manyve birleştirme tablosu modeli için de geçerlidir.

Bu en iyi, irb'den ilişkilendirmelere erişildiğinde ve Rails'in günlük dosyasında oluşturduğu SQL'e bakıldığında görülür. Aşağıdakine benzer bir şey bulacaksınız:

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

İlişkilendirmeyi iki yönlü yapmak için, Rails'i ORyukarıdaki koşullara sahip post_a_idve post_b_idtersine çevirmenin bir yolunu bulmalıyız , böylece her iki yönde de görünecektir.

Maalesef, bunu yapmanın bildiğim tek yolu biraz hilekârlık. Manuel SQL için seçenekleri kullanarak belirtmek gerekir has_and_belongs_to_manygibi :finder_sql, :delete_sqlGüzel değil vb. (Ben de burada önerilere açığım. Kimse?)


Güzel yorumlar için teşekkürler! :) Bazı ek düzenlemeler yaptım. Özellikle, :foreign_keyon the has_many :throughgerekli değildir ve çok kullanışlı :dependentparametrenin nasıl kullanılacağına dair bir açıklama ekledim has_many.
Stéphan Kochen

@ Shtééf bile toplu atama (update_attributes), iki yönlü ilişkilendirmelerde çalışmaz, örneğin: postA.update_attributes ({: post_b_ids => [2,3,4]}) herhangi bir fikir veya geçici çözüm?
Lohith MV

Çok güzel cevap dostum 5. kez {koyar "+1"}
Rahul

@ Shtééf bu cevaptan çok şey öğrendim, teşekkür ederim! İki yönlü ilişkilendirme sorunuzu burada sormaya ve yanıtlamaya çalıştım: stackoverflow.com/questions/25493368/…
jbmilgrom

17

Shteef'in sorduğu soruyu cevaplamak için:

Çift yönlü döngülü ilişkilendirmeler

Kullanıcılar arasındaki takipçi-takipçi ilişkisi , İki yönlü döngülü ilişkiye iyi bir örnektir. Bir Kullanıcı çok sayıda şunlara sahip olabilir:

  • takipçi sıfatıyla takipçileri
  • takipçi sıfatıyla takipçidir.

User.rb kodu şu şekilde görünebilir:

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 kodu şu şekilde :

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

Dikkat edilmesi gereken en önemli şeyler muhtemelen terimlerdir :follower_follows ve :followee_followsuser.rb'dir. Örnek olarak değirmen çalıştırma (döngüsüz) ilişkisini kullanmak için, bir Takımda çok sayıda: playersbaştan sona olabilir :contracts. Bu durum farklı değil Player birçok olabilir, :teamsthrough :contractssıra (örneğin boyunca Oyuncu 'ın kariyerinde). Ancak bu durumda, yalnızca bir adlandırılmış modelin (yani bir Kullanıcı ) olduğu durumlarda, through: ilişkisini aynı şekilde adlandırmak (ör through: :follow. Veya, yukarıda yazılanlar örneğinde olduğu gibi through: :post_connections) farklı kullanım durumları için bir adlandırma çakışmasına neden olur ( veya birleştirme tablosuna erişim noktaları. :follower_followsve:followee_followsböyle bir adlandırma çakışmasını önlemek için oluşturuldu. Şimdi, kullanıcı birçok olabilir :followersthrough :follower_followsve birçok :followeesthrough :followee_follows.

Bir Kullanıcının : takipçilerini belirlemek için ( @user.followeesveri tabanına yapılan bir çağrı üzerine ), Rails artık her bir class_name: "Follow" örneğine bakabilir, burada söz konusu Kullanıcının takipçi (yani foreign_key: :follower_id) aracılığıyla: söz konusu Kullanıcının : followee_follows olduğu. Belirlemek için Kullanıcının : takipçilerini ( @user.followersveri tabanına yapılan bir çağrı üzerine ), Rails artık her bir class_name: "Follow" örneğine bakabilir, burada söz konusu Kullanıcının takipçisi (yani foreign_key: :followee_id) aracılığıyla: söz konusu Kullanıcının : follower_follows.


1
Tam olarak ihtiyacım olan şey! Teşekkürler! (Veritabanı geçişlerini de listelemenizi öneririm; bu bilgiyi kabul edilen cevaptan toplamak zorunda kaldım)
Adam Denoon

6

Eğer birisi buraya Rails'de arkadaş ilişkileri kurmayı öğrenmek için geldiyse, o zaman onlara nihayet kullanmaya karar verdiğim şeye başvururdum, bu da 'Community Engine'in yaptığını kopyalamaktır.

Şunlara başvurabilirsiniz:

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

ve

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

daha fazla bilgi için.

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

@ Stéphan Kochen'den esinlenerek, bu iki yönlü dernekler için işe yarayabilir

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

sonra post.posts&& post.reversed_postsher ikisi de çalışmalı, en azından benim için çalışmalı.


1

Çift yönlü için belongs_to_and_has_many, önceden gönderilmiş olan harika yanıta bakın ve ardından farklı bir adla başka bir ilişkilendirme oluşturun, yabancı anahtarlar tersine çevrilir ve class_namedoğru modeli göstermeye ayarladığınızdan emin olun . Şerefe.


2
Gönderinizde bir örnek gösterebilir misiniz? Senin önerdiğin gibi birçok yol denedim, ama başaramıyorum.
achabacha322

0

Çalışmak için mükemmel cevabı almakta sorun yaşayan biri varsa, örneğin:

(Nesne #inspect'i desteklemiyor)
=>

veya

NoMethodError: tanımsız yöntem `` bölme '': Görev: Sembol

Sonra çözüm değiştirmektir :PostConnectionile "PostConnection"elbette sınıfadı yerine,.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.