Viele-zu-viele-Beziehung mit demselben Modell in Schienen?

107

Wie kann ich mit Schienen eine Viele-zu-Viele-Beziehung mit demselben Modell herstellen?

Beispielsweise ist jeder Beitrag mit vielen Beiträgen verbunden.

Sieger
quelle

Antworten:

276

Es gibt verschiedene Arten von Viele-zu-Viele-Beziehungen. Sie müssen sich folgende Fragen stellen:

  • Möchte ich zusätzliche Informationen bei der Vereinigung speichern? (Zusätzliche Felder in der Join-Tabelle.)
  • Müssen die Assoziationen implizit bidirektional sein? (Wenn Pfosten A mit Pfosten B verbunden ist, ist Pfosten B auch mit Pfosten A verbunden.)

Das lässt vier verschiedene Möglichkeiten. Ich werde unten darauf eingehen.

Als Referenz: die Rails-Dokumentation zu diesem Thema . Es gibt einen Abschnitt namens "Viele-zu-Viele" und natürlich die Dokumentation zu den Klassenmethoden selbst.

Einfachstes Szenario, unidirektional, keine zusätzlichen Felder

Dies ist der kompakteste Code.

Ich beginne mit diesem Grundschema für Ihre Beiträge:

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

Für jede Viele-zu-Viele-Beziehung benötigen Sie eine Join-Tabelle. Hier ist das Schema dafür:

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

Standardmäßig nennt Rails diese Tabelle eine Kombination der Namen der beiden Tabellen, denen wir beitreten. Aber das würde sich wie posts_postsin dieser Situation herausstellen , also entschied ich mich post_connectionsstattdessen zu nehmen .

Hier ist es sehr wichtig :id => false, die Standardspalte wegzulassen id. Rails möchte diese Spalte überall außer auf Join-Tabellen fürhas_and_belongs_to_many . Es wird sich laut beschweren.

Beachten Sie schließlich, dass die Spaltennamen ebenfalls nicht dem Standard entsprechen (nicht post_id), um Konflikte zu vermeiden.

Jetzt müssen Sie in Ihrem Modell Rails lediglich über diese paar nicht standardmäßigen Dinge informieren. Es wird wie folgt aussehen:

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

Und das sollte einfach funktionieren! Hier ist ein Beispiel für eine irb-Sitzung 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!">]

Wenn Sie der Zuordnung zuweisen, postswerden entsprechend Datensätze in der post_connectionsTabelle erstellt.

Einige Dinge zu beachten:

  • Sie können in der obigen irb-Sitzung sehen, dass die Zuordnung unidirektional ist, da nach a.posts = [b, c]der Ausgabe vonb.posts nicht den ersten Beitrag enthält.
  • Eine andere Sache, die Sie vielleicht bemerkt haben, ist, dass es kein Modell gibt PostConnection. Normalerweise verwenden Sie keine Modelle für eine has_and_belongs_to_manyZuordnung. Aus diesem Grund können Sie nicht auf zusätzliche Felder zugreifen.

Unidirektional, mit zusätzlichen Feldern

Richtig, jetzt ... Sie haben einen regulären Benutzer, der heute auf Ihrer Website einen Beitrag darüber verfasst hat, wie lecker Aale sind. Dieser völlig Fremde kommt auf Ihre Website, meldet sich an und schreibt einen schimpfenden Beitrag über die Unfähigkeit des regulären Benutzers. Aale sind schließlich eine vom Aussterben bedrohte Art!

Sie möchten also in Ihrer Datenbank klarstellen, dass Post B eine Schelte auf Post A ist. Dazu möchten Sie categoryder Zuordnung ein Feld hinzufügen .

Was wir brauchen , ist nicht mehr ein has_and_belongs_to_many, sondern eine Kombination von has_many, belongs_to, has_many ..., :through => ...und ein zusätzliches Modell für die Join - Tabelle. Dieses zusätzliche Modell gibt uns die Möglichkeit, dem Verband selbst zusätzliche Informationen hinzuzufügen.

Hier ist ein weiteres Schema, das dem obigen sehr ähnlich ist:

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

Beachten Sie, wie in dieser Situation post_connections hat eine haben idSpalte. (Es gibt keinen :id => false Parameter.) Dies ist erforderlich, da es ein reguläres ActiveRecord-Modell für den Zugriff auf die Tabelle gibt.

Ich werde mit dem PostConnectionModell beginnen, weil es ganz einfach ist:

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

Das einzige, was hier vor sich geht :class_name, ist , was notwendig ist, weil Rails nicht daraus schließen kann post_aoder post_bdass es sich hier um einen Beitrag handelt. Wir müssen es explizit sagen.

Nun das PostModell:

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

Mit dem ersten has_manyVerein, sagen wir , das Modell zu verbinden post_connectionsauf posts.id = post_connections.post_a_id.

Mit der zweiten Vereinigung teilen wir Rails mit, dass wir die anderen Stellen, die mit dieser verbunden sind, über unsere erste Vereinigung erreichen können post_connections, gefolgt von der post_bVereinigung von PostConnection.

Es fehlt nur noch eine Sache , und das ist, dass wir Rails mitteilen müssen, dass a PostConnectionvon den Posts abhängt, zu denen es gehört. Wenn eine oder beide post_a_idund post_b_idwaren NULL, so dass die Verbindung uns nicht sagen würde viel, oder ? So machen wir das in unserem PostModell:

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

Neben der geringfügigen Änderung der Syntax unterscheiden sich hier zwei reale Dinge:

  • Das has_many :post_connectionshat einen zusätzlichen :dependentParameter. Mit dem Wert :destroyteilen wir Rails mit, dass dieser Beitrag, sobald er verschwindet, diese Objekte zerstören kann. Ein alternativer Wert, den Sie hier verwenden können, ist der :delete_all, der schneller ist, aber keine Zerstörungs-Hooks aufruft, wenn Sie diese verwenden.
  • Wir haben auch eine has_manyZuordnung für die umgekehrten Verbindungen hinzugefügt, die uns verbunden haben post_b_id. Auf diese Weise können Rails auch diese sauber zerstören. Beachten Sie, dass wir hier angeben müssen :class_name, da der Klassenname des Modells nicht mehr abgeleitet werden kann :reverse_post_connections.

Nachdem dies geschehen ist, bringe ich Ihnen eine weitere irb-Sitzung durch 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

Anstatt die Zuordnung zu erstellen und die Kategorie dann separat festzulegen, können Sie auch einfach eine PostConnection erstellen und damit fertig sein:

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

Und wir können auch die post_connectionsund reverse_post_connectionsAssoziationen manipulieren ; es wird sich ordentlich in der postsVereinigung widerspiegeln :

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

Bidirektionale Schleifenassoziationen

In normalen has_and_belongs_to_manyAssoziationen ist die Assoziation in beiden beteiligten Modellen definiert . Und der Verein ist bidirektional.

In diesem Fall gibt es jedoch nur ein Post-Modell. Und die Zuordnung wird nur einmal angegeben. Genau deshalb sind Assoziationen in diesem speziellen Fall unidirektional.

Gleiches gilt für die alternative Methode mit has_manyund ein Modell für die Join-Tabelle.

Dies lässt sich am besten erkennen, wenn Sie einfach über irb auf die Zuordnungen zugreifen und sich die SQL ansehen, die Rails in der Protokolldatei generiert. Sie finden ungefähr Folgendes:

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

Um die Assoziation bidirektional zu machen, müssten wir einen Weg finden, um Rails ORdie oben genannten Bedingungen mit post_a_idund post_b_idumgekehrt zu machen, damit es in beide Richtungen schaut.

Leider ist der einzige Weg, den ich kenne, ziemlich hackig. Sie werden von Hand, um Ihre SQL verwenden Optionen festlegen has_and_belongs_to_many, wie :finder_sql, :delete_sqletc. Es ist ziemlich nicht. (Ich bin auch hier offen für Vorschläge. Jemand?)

Shtééf
quelle
Danke für die netten Kommentare! :) Ich habe einige weitere Änderungen vorgenommen. Insbesondere ist das :foreign_keyon the has_many :throughnicht erforderlich, und ich habe eine Erklärung hinzugefügt, wie der sehr praktische :dependentParameter für verwendet wird has_many.
Stéphan Kochen
@ Shtééf Auch die Massenzuweisung (update_attributes) funktioniert bei bidirektionalen Assoziationen nicht. Beispiel: postA.update_attributes ({: post_b_ids => [2,3,4]}) Ideen oder Problemumgehungen?
Lohith MV
Sehr nette Antwort Kumpel 5.times {setzt "+1"}
Rahul
@ Shtééf Ich habe viel aus dieser Antwort gelernt, danke! Ich habe versucht, Ihre bidirektionale Assoziationsfrage hier zu stellen und zu beantworten: stackoverflow.com/questions/25493368/…
jbmilgrom
17

Um die Frage von Shteef zu beantworten:

Bidirektionale Schleifenassoziationen

Die Follower-Followee-Beziehung zwischen Benutzern ist ein gutes Beispiel für eine bidirektionale Schleifenassoziation. Ein Benutzer kann viele haben:

  • Anhänger in seiner Eigenschaft als Gefolgsmann
  • Anhänger in seiner Eigenschaft als Anhänger.

So könnte der Code für user.rb aussehen:

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

So lautet der Code für 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

Die wichtigsten Dinge, die zu beachten sind, sind wahrscheinlich die Begriffe :follower_followsund :followee_followsin user.rb. Um eine Run-of-the-Mill-Zuordnung (ohne Schleife) als Beispiel zu verwenden, kann ein Team viele: playersDurchgänge haben :contracts. Dies ist nicht anders für einen Spieler , der möglicherweise auch viele :teamsdurch hat :contracts(im Laufe der Karriere eines solchen Spielers ). In diesem Fall, in dem nur ein benanntes Modell existiert (dh ein Benutzer ), würde die identische Benennung der through: -Beziehung (z. B. through: :followoder, wie oben im Beitragsbeispiel beschrieben through: :post_connections) zu einer Namenskollision für verschiedene Anwendungsfälle von (führen) oder Zugriffspunkte in die Join-Tabelle. wurden erstellt, um eine solche Namenskollision zu vermeiden. Nun, a:follower_followsund:followee_follows Benutzer viele :followersdurch :follower_followsund viele :followeesdurch haben :followee_follows.

Um die Follower eines Benutzers zu ermitteln (bei einem @user.followeesAufruf der Datenbank), kann Rails nun jede Instanz von class_name: "Follow" anzeigen, wobei dieser Benutzer der Follower ist (dh foreign_key: :follower_id) durch: die folgenden Benutzer : followee_follows. Um die Follower eines Benutzers zu ermitteln (bei einem @user.followersAufruf der Datenbank), kann Rails nun jede Instanz von class_name: "Follow" anzeigen, wobei dieser Benutzer der Follower ist (dh foreign_key: :followee_id) durch : Follower_follows dieses Benutzers .

jbmilgrom
quelle
1
Genau das, was ich brauchte! Vielen Dank! (Ich empfehle auch die Datenbankmigrationen aufzulisten; ich musste diese Informationen aus der akzeptierten Antwort
entnehmen
6

Wenn jemand hierher kam, um herauszufinden, wie man Freundschaftsbeziehungen in Rails erstellt, würde ich ihn auf das verweisen, was ich letztendlich verwendet habe, nämlich zu kopieren, was 'Community Engine' getan hat.

Sie können sich beziehen auf:

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

und

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

für mehr Informationen.

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
quelle
2

Inspiriert von @ Stéphan Kochen könnte dies für bidirektionale Assoziationen funktionieren

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

dann sollte post.posts&& post.reversed_postsbeides funktionieren, zumindest für mich gearbeitet.

Alba Hoo
quelle
1

Informationen zur bidirektionalen Ausrichtung belongs_to_and_has_manyfinden Sie in der bereits veröffentlichten Antwort. Erstellen Sie dann eine weitere Zuordnung mit einem anderen Namen. Die Fremdschlüssel sind umgekehrt, und stellen Sie sicher, dass Sie class_nameauf das richtige Modell zurückweisen. Prost.

Zhenya Slabkovski
quelle
2
Könnten Sie ein Beispiel in Ihrem Beitrag zeigen? Ich habe mehrere Möglichkeiten ausprobiert, wie Sie vorgeschlagen haben, kann es aber nicht nageln.
Achabacha322
0

Wenn jemand Probleme hatte, die ausgezeichnete Antwort auf die Arbeit zu bekommen, wie zum Beispiel:

(Objekt unterstützt #inspect nicht)
=>

oder

NoMethodError: undefinierte Methode `split 'für: Mission: Symbol

Dann ist die Lösung zu ersetzen , :PostConnectionmit "PostConnection", ersetzen Sie Ihre Klassennamen natürlich.

user2303277
quelle