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_posts
in dieser Situation herausstellen , also entschied ich mich post_connections
stattdessen 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, posts
werden entsprechend Datensätze in der post_connections
Tabelle 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_many
Zuordnung. 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 category
der 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 id
Spalte. (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 PostConnection
Modell 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_a
oder post_b
dass es sich hier um einen Beitrag handelt. Wir müssen es explizit sagen.
Nun das Post
Modell:
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_many
Verein, sagen wir , das Modell zu verbinden post_connections
auf 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_b
Vereinigung von PostConnection
.
Es fehlt nur noch eine Sache , und das ist, dass wir Rails mitteilen müssen, dass a PostConnection
von den Posts abhängt, zu denen es gehört. Wenn eine oder beide post_a_id
und post_b_id
waren NULL
, so dass die Verbindung uns nicht sagen würde viel, oder ? So machen wir das in unserem Post
Modell:
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_connections
hat einen zusätzlichen :dependent
Parameter. Mit dem Wert :destroy
teilen 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_many
Zuordnung 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_connections
und reverse_post_connections
Assoziationen manipulieren ; es wird sich ordentlich in der posts
Vereinigung 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_many
Assoziationen 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_many
und 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 OR
die oben genannten Bedingungen mit post_a_id
und post_b_id
umgekehrt 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_sql
etc. 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_follows
und :followee_follows
in user.rb. Um eine Run-of-the-Mill-Zuordnung (ohne Schleife) als Beispiel zu verwenden, kann ein Team viele: players
Durchgänge haben :contracts
. Dies ist nicht anders für einen Spieler , der möglicherweise auch viele :teams
durch 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: :follow
oder, 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_follows
und:followee_follows
Benutzer viele :followers
durch :follower_follows
und viele :followees
durch haben :followee_follows
.
Um die Follower eines Benutzers zu ermitteln (bei einem @user.followees
Aufruf 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.followers
Aufruf 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_posts
beides funktionieren, zumindest für mich gearbeitet.
Informationen zur bidirektionalen Ausrichtung belongs_to_and_has_many
finden 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_name
auf 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 , :PostConnection
mit "PostConnection"
, ersetzen Sie Ihre Klassennamen natürlich.
:foreign_key
on thehas_many :through
nicht erforderlich, und ich habe eine Erklärung hinzugefügt, wie der sehr praktische:dependent
Parameter für verwendet wirdhas_many
.