Antworten:
Es gibt verschiedene Arten von Viele-zu-Viele-Beziehungen. Sie müssen sich folgende Fragen stellen:
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.
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:
a.posts = [b, c]der Ausgabe vonb.posts nicht den ersten Beitrag enthält.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.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:
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.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
=> []
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?)
Um die Frage von Shteef zu beantworten:
Die Follower-Followee-Beziehung zwischen Benutzern ist ein gutes Beispiel für eine bidirektionale Schleifenassoziation. Ein Benutzer kann viele haben:
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 .
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"
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.
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.
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.
:foreign_keyon thehas_many :throughnicht erforderlich, und ich habe eine Erklärung hinzugefügt, wie der sehr praktische:dependentParameter für verwendet wirdhas_many.