Rapporto molti-a-molti con lo stesso modello nei binari?


107

Come posso creare una relazione molti-a-molti con lo stesso modello in rails?

Ad esempio, ogni post è collegato a molti post.

Risposte:


276

Esistono diversi tipi di relazioni molti a molti; devi porsi le seguenti domande:

  • Voglio memorizzare informazioni aggiuntive presso l'associazione? (Campi aggiuntivi nella tabella di join.)
  • Le associazioni devono essere implicitamente bidirezionali? (Se il post A è collegato al post B, anche il post B è collegato al post A.)

Ciò lascia quattro diverse possibilità. Camminerò su questi di seguito.

Per riferimento: la documentazione di Rails sull'argomento . C'è una sezione chiamata "Many-to-many" e, naturalmente, la documentazione sui metodi di classe stessi.

Scenario più semplice, unidirezionale, nessun campo aggiuntivo

Questo è il codice più compatto.

Inizierò con questo schema di base per i tuoi post:

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

Per qualsiasi relazione molti a molti, è necessaria una tabella di partecipazione. Ecco lo schema per questo:

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

Per impostazione predefinita, Rails chiamerà questa tabella una combinazione dei nomi delle due tabelle che stiamo unendo. Ma sarebbe risultato come posts_postsin questa situazione, quindi ho deciso di prendere post_connectionsinvece.

Molto importante qui è :id => falseomettere la idcolonna predefinita . Rails vuole quella colonna ovunque tranne che nelle tabelle di join per has_and_belongs_to_many. Si lamenterà ad alta voce.

Infine, nota che anche i nomi delle colonne non sono standard (non post_id), per evitare conflitti.

Ora nel tuo modello, devi semplicemente dire a Rails di queste due cose non standard. Apparirà come segue:

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

E dovrebbe semplicemente funzionare! Ecco un esempio di sessione irb eseguita 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!">]

Scoprirai che l'assegnazione postsall'associazione creerà record nella post_connectionstabella come appropriato.

Alcune cose da notare:

  • Puoi vedere nella sessione irb sopra che l'associazione è unidirezionale, perché dopo a.posts = [b, c], l'output di b.postsnon include il primo post.
  • Un'altra cosa che potresti aver notato è che non esiste un modello PostConnection. Normalmente non usi i modelli per has_and_belongs_to_manyun'associazione. Per questo motivo non potrai accedere a nessun campo aggiuntivo.

Unidirezionale, con campi aggiuntivi

Bene, ora ... Hai un utente normale che oggi ha pubblicato un post sul tuo sito su come le anguille sono deliziose. Questo perfetto sconosciuto arriva sul tuo sito, si iscrive e scrive un post di rimprovero sull'inettitudine dell'utente normale. Dopo tutto, le anguille sono una specie in via di estinzione!

Quindi vorresti chiarire nel tuo database che il post B è uno sproloquio di rimprovero sul post A. Per farlo, vuoi aggiungere un categorycampo all'associazione.

Ciò di cui abbiamo bisogno non è più una è has_and_belongs_to_many, ma una combinazione di has_many, belongs_to, has_many ..., :through => ...e un modello in più per il join tavolo. Questo modello extra è ciò che ci dà il potere di aggiungere ulteriori informazioni all'associazione stessa.

Ecco un altro schema, molto simile al precedente:

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

Notate come, in questa situazione, post_connections ha avere una idcolonna. (Non c'è nessun :id => false parametro.) Questa operazione è necessaria, perché ci sarà un modello regolare ActiveRecord per l'accesso al tavolo.

Inizierò con il PostConnectionmodello, perché è semplicissimo:

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

L'unica cosa che sta succedendo qui è :class_name, che è necessaria, perché Rails non può dedurre da post_ao post_bche abbiamo a che fare con un Post qui. Dobbiamo dirlo esplicitamente.

Ora il Postmodello:

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

Con la prima has_manyassociazione, diciamo il modello di aderire post_connectionson posts.id = post_connections.post_a_id.

Con la seconda associazione diciamo a Rails che possiamo raggiungere gli altri post, quelli collegati a questo, tramite la nostra prima associazione post_connections, seguita post_bdall'associazione di PostConnection.

C'è solo un'altra cosa che manca, ed è che dobbiamo dire a Rails che a PostConnectiondipende dai post a cui appartiene. Se uno o entrambi post_a_ide post_b_idfossero NULL, allora quella connessione non ci direbbe molto, vero? Ecco come lo facciamo nel nostro Postmodello:

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

Oltre al leggero cambiamento di sintassi, qui due cose reali sono diverse:

  • Il has_many :post_connectionsha un extra :dependentparametro. Con il valore :destroy, diciamo a Rails che, una volta che questo post scompare, può andare avanti e distruggere questi oggetti. Un valore alternativo che puoi usare qui è :delete_all, che è più veloce, ma non chiamerà alcun hook di distruzione se li stai usando.
  • Abbiamo aggiunto anche has_manyun'associazione per le connessioni inverse , quelle che ci hanno collegato post_b_id. In questo modo, Rails può distruggere anche quelli. Nota che dobbiamo specificare :class_namequi, perché il nome della classe del modello non può più essere dedotto da :reverse_post_connections.

Con questo in atto, ti porto un'altra sessione irb attraverso 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

Invece di creare l'associazione e quindi impostare la categoria separatamente, puoi anche creare semplicemente una PostConnection e farla finita:

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

E possiamo anche manipolare le associazioni post_connectionse reverse_post_connections; si rifletterà chiaramente nell'associazione posts:

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

Associazioni loop bidirezionali

Nelle has_and_belongs_to_manyassociazioni normali , l'associazione è definita in entrambi i modelli coinvolti. E l'associazione è bidirezionale.

Ma in questo caso c'è solo un modello Post. E l'associazione viene specificata solo una volta. Proprio per questo, in questo caso specifico, le associazioni sono unidirezionali.

Lo stesso vale per il metodo alternativo con has_manye un modello per la tabella di join.

Ciò si vede meglio quando si accede semplicemente alle associazioni da irb e si osserva l'SQL che Rails genera nel file di log. Troverai qualcosa di simile al seguente:

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

Per rendere l'associazione bidirezionale, dovremmo trovare un modo per rendere Rails ORle condizioni di cui sopra con post_a_ide post_b_idinvertite, in modo che guardi in entrambe le direzioni.

Sfortunatamente, l'unico modo per farlo che conosco è piuttosto hacky. Dovrete specificare manualmente SQL utilizzando le opzioni di has_and_belongs_to_manycome :finder_sql, :delete_sqlecc Non è bello. (Anche qui sono aperto a suggerimenti. Qualcuno?)


Grazie per i bei commenti! :) Ho apportato ulteriori modifiche. In particolare, l' :foreign_keysulla has_many :throughnon è necessario, e ho aggiunto una spiegazione su come utilizzare il molto utile :dependentparametro per has_many.
Stéphan Kochen

@ Shtééf anche l'assegnazione di massa (update_attributes) non funzionerà in caso di associazioni bidirezionali, ad esempio: postA.update_attributes ({: post_b_ids => [2,3,4]}) qualche idea o soluzione?
Lohith MV

Molto bella risposta amico 5 volte {mette "+1"}
Rahul

@ Shtééf ho imparato molto da questa risposta, grazie! Ho provato a chiedere e rispondere alla tua domanda associazione bidirezionale qui: stackoverflow.com/questions/25493368/...
jbmilgrom

17

Per rispondere alla domanda posta da Shteef:

Associazioni loop bidirezionali

La relazione follower-followee tra gli utenti è un buon esempio di associazione a ciclo bidirezionale. Un utente può avere molti:

  • seguaci nella sua qualità di seguaci
  • followees nella sua qualità di follower.

Ecco come potrebbe apparire il codice per 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

Ecco come il codice per 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

Le cose più importanti da notare sono probabilmente i termini :follower_followse :followee_followsin user.rb. Per utilizzare un'associazione run of the mill (non loop) come esempio, un team può avere molti: playersthrough :contracts. Questo non è diverso per un giocatore , che può avere molti :teamsattraverso :contractsoltre (nel corso di tale Player carriera s'). Ma in questo caso, dove esiste un solo modello denominato (ovvero un utente ), denominare la relazione through: in modo identico (ad es through: :follow. O, come è stato fatto sopra nell'esempio dei post through: :post_connections) comporterebbe una collisione di denominazione per diversi casi d'uso di ( o punti di accesso nella) tabella di join. :follower_followse:followee_followssono stati creati per evitare una tale collisione di denominazione. Ora, un utente può avere molti :followersattraverso :follower_followse molti :followeesattraverso :followee_follows.

Per determinare un utente : followees (su una @user.followeeschiamata al database), Rails può ora guardare ogni istanza di class_name: "Follow" dove tale utente è il follower (cioè foreign_key: :follower_id) attraverso: such User : followee_follows. Per determinare un utente : followers (su una @user.followerschiamata al database), Rails può ora esaminare ogni istanza di class_name: "Follow" dove tale utente è il followee (cioè foreign_key: :followee_id) tramite: such User : follower_follows.


1
Esattamente quello di cui avevo bisogno! Grazie! (Consiglio anche di elencare le migrazioni del database; ho dovuto raccogliere queste informazioni dalla risposta accettata)
Adam Denoon

6

Se qualcuno venisse qui per cercare di scoprire come creare relazioni di amicizia in Rails, allora lo farei riferimento a ciò che ho finalmente deciso di usare, ovvero copiare ciò che ha fatto "Community Engine".

Puoi fare riferimento a:

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

e

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

per maggiori informazioni.

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

Ispirato da @ Stéphan Kochen, questo potrebbe funzionare per associazioni bidirezionali

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

allora post.posts&& post.reversed_postsdovrebbero funzionare entrambi, almeno per me.


1

Per il bidirezionale belongs_to_and_has_many, fai riferimento alla grande risposta già pubblicata, quindi crea un'altra associazione con un nome diverso, le chiavi esterne invertite e assicurati di aver class_nameimpostato per puntare al modello corretto. Saluti.


2
Potresti mostrare un esempio nel tuo post? Ho provato diversi modi come hai suggerito, ma non riesco a inchiodarlo.
achabacha322

0

Se qualcuno ha avuto problemi a far funzionare la risposta eccellente, ad esempio:

(L'oggetto non supporta #inspect)
=>

o

NoMethodError: metodo non definito `split 'per: Mission: Symbol

Quindi la soluzione è sostituire :PostConnectioncon "PostConnection", sostituendo ovviamente il nome della classe.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.