Relation plusieurs à plusieurs avec le même modèle dans les rails?

Comment puis-je établir une relation plusieurs-à-plusieurs avec le même modèle dans les rails?

Par exemple, chaque article est connecté à de nombreux messages.

Il existe plusieurs types de relations plusieurs-à-plusieurs; vous devez vous poser les questions suivantes:

  • Est-ce que je veux stocker des informations supplémentaires avec l’association? (Champs supplémentaires dans la table de jointure.)
  • Les associations doivent-elles être implicitement bidirectionnelles? (Si le poste A est connecté au poste B, le poste B est également connecté au poste A.)

Cela laisse quatre possibilités différentes. Je vais passer par-dessus.

Pour référence: la documentation Rails sur le sujet . Il y a une section intitulée «Many-to-many», et bien sûr la documentation sur les méthodes de classe elles-mêmes.

Scénario le plus simple, unidirectionnel, pas de champs supplémentaires

C’est le code le plus compact.

Je vais commencer avec ce schéma de base pour vos messages:

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

Pour toute relation plusieurs-à-plusieurs, vous avez besoin d’une table de jointure. Voici le schéma pour cela:

 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 

Par défaut, Rails appelle cette table une combinaison des noms des deux tables que nous rejoignons. Mais cela deviendrait des posts_posts dans cette situation, alors j’ai décidé de prendre post_connections place.

Très important ici :id => false , pour omettre la colonne id par défaut. Rails veut cette colonne partout sauf sur les tables de has_and_belongs_to_many pour has_and_belongs_to_many . Il va se plaindre fort.

Enfin, notez que les noms de colonne sont également non standard (pas post_id ), pour éviter les conflits.

Maintenant, dans votre modèle, vous devez simplement informer Rails de ces deux choses non standard. Il ressemblera à ceci:

 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 

Et cela devrait simplement fonctionner! Voici un exemple de session irb exécutée par script/console :

 >> a = Post.create :name => 'First post!' => # >> b = Post.create :name => 'Second post?' => # >> c = Post.create :name => 'Definitely the third post.' => # >> a.posts = [b, c] => [#, #] >> b.posts => [] >> b.posts = [a] => [#] 

Vous constaterez que l’affectation à l’association des posts créera des enregistrements dans la table post_connections , le cas échéant.

Quelques points à noter:

  • Vous pouvez voir dans la session irb ci-dessus que l’association est unidirectionnelle, car après a.posts = [b, c] , la sortie de b.posts n’inclut pas le premier message.
  • Une autre chose que vous avez peut-être remarquée, c’est qu’il n’y a pas de modèle PostConnection . Vous n’utilisez normalement pas de modèles pour une association has_and_belongs_to_many . Pour cette raison, vous ne pourrez accéder à aucun champ supplémentaire.

Uni-directionnel, avec des champs supplémentaires

Bon, maintenant … Vous avez un utilisateur régulier qui a publié aujourd’hui sur votre site un article sur la façon dont les anguilles sont délicieuses. Cet inconnu arrive sur votre site, s’inscrit et écrit un article critique sur l’inaptitude de l’utilisateur habituel. Après tout, les anguilles sont une espèce en voie de disparition!

Donc, vous souhaitez préciser dans votre firebase database que le post B est une expression répréhensible sur le post A. Pour ce faire, vous souhaitez append un champ de category à l’association.

Ce dont nous avons besoin n’est plus has_and_belongs_to_many , mais une combinaison de has_many , belongs_to , has_many ..., :through => ... et un modèle supplémentaire pour la table de jointure. Ce modèle supplémentaire est ce qui nous donne le pouvoir d’append des informations supplémentaires à l’association elle-même.

Voici un autre schéma, très similaire à ce qui précède:

 create_table "posts", :force => true do |t| t.ssortingng "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.ssortingng "category" end 

Remarquez que, dans cette situation, post_connections possède une colonne id . (Il n’y a pas :id => false paramètre :id => false .) Ceci est nécessaire, car il y aura un modèle ActiveRecord standard pour accéder à la table.

Je vais commencer par le modèle PostConnection , car il est simple:

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

La seule chose qui se passe ici est :class_name , ce qui est nécessaire, car Rails ne peut pas déduire de post_a ou post_b que nous avons affaire à un article ici. Nous devons le dire explicitement.

Maintenant, le modèle Post :

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

Avec la première association has_many , nous indiquons au modèle de joindre post_connections sur posts.id = post_connections.post_a_id .

Avec la deuxième association, nous disons à Rails que nous pouvons atteindre les autres postes, ceux connectés à celui-ci, via notre première association post_connections , suivie de l’association PostConnection de PostConnection .

Il ne manque plus qu’une chose , à savoir que nous devons dire à Rails qu’une PostConnection dépend des messages PostConnection elle appartient. Si l’un ou les deux de post_a_id et post_b_id étaient NULL , alors cette connexion ne nous dirait pas grand-chose, n’est-ce pas? Voici comment nous le faisons dans notre modèle Post :

 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 

Outre le léger changement de syntaxe, deux choses réelles sont différentes ici:

  • Le has_many :post_connections a un paramètre supplémentaire :dependent . Avec la valeur :destroy , on dit à Rails qu’une fois que ce post disparaît, il peut aller de l’avant et détruire ces objects. Une autre valeur que vous pouvez utiliser ici est :delete_all , qui est plus rapide, mais n’appellera aucun hook de destruction si vous l’utilisez.
  • Nous avons également ajouté une association has_many pour les connexions inverses , celles qui nous ont liées via post_b_id . De cette façon, les rails peuvent détruire ceux-ci. Notez que nous devons spécifier :class_name ici, car le nom de classe du modèle ne peut plus être déduit de :reverse_post_connections .

Avec ceci en place, je vous apporte une autre session irb par script/console :

 >> a = Post.create :name => 'Eels are delicious!' => # >> b = Post.create :name => 'You insensitive cloth!' => # >> b.posts = [a] => [#] >> b.post_connections => [#] >> connection = b.post_connections[0] => # >> connection.category = "scolding" => "scolding" >> connection.save! => true 

Au lieu de créer l’association et de définir la catégorie séparément, vous pouvez également créer une PostConnection et la terminer:

 >> b.posts = [] => [] >> PostConnection.create( ?> :post_a => b, :post_b => a, ?> :category => "scolding" >> ) => # >> b.posts(true) # 'true' means force a reload => [#] 

Et nous pouvons également manipuler les associations post_connections et reverse_post_connections ; cela reflétera parfaitement dans l’association des posts :

 >> a.reverse_post_connections => # >> a.reverse_post_connections = [] => [] >> b.posts(true) # 'true' means force a reload => [] 

Associations en boucle bidirectionnelles

Dans les associations has_and_belongs_to_many , l’association est définie dans les deux modèles impliqués. Et l’association est bidirectionnelle.

Mais il n’y a qu’un seul modèle Post dans ce cas. Et l’association n’est spécifiée qu’une fois. C’est exactement pourquoi dans ce cas précis, les associations sont unidirectionnelles.

Il en va de même pour la méthode alternative avec has_many et un modèle pour la table de jointure.

Cela se voit mieux lorsque vous accédez simplement aux associations à partir de irb et que vous examinez le SQL généré par Rails dans le fichier journal. Vous trouverez quelque chose comme ceci:

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

Pour que l’association soit bidirectionnelle, il faudrait trouver un moyen de rendre Rails OR les conditions ci-dessus avec post_a_id et post_b_id inversés, pour que cela apparaisse dans les deux sens.

Malheureusement, la seule façon de le faire que je connaisse est plutôt pirate. Vous devrez spécifier manuellement votre SQL en utilisant les options de has_and_belongs_to_many telles que :finder_sql :delete_sql , etc. Ce n’est pas joli. (Je suis ouvert aux suggestions ici aussi. Quelqu’un?)

Pour répondre à la question posée par Shteef:

Associations en boucle bidirectionnelles

La relation suiveur-suiveur entre utilisateurs est un bon exemple d’association en boucle bidirectionnelle. Un utilisateur peut en avoir plusieurs:

  • suiveurs en sa qualité de suiveurs
  • suiveurs en sa qualité de suiveurs.

Voici comment le code pour user.rb pourrait ressembler:

 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 

Voici comment le code pour 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 

Les choses les plus importantes à noter sont probablement les termes :follower_follows et :followee_follows dans user.rb. Pour utiliser une association à la chaîne (non bouclée) comme exemple, une équipe peut avoir plusieurs players via :contracts . Ce n'est pas différent pour un joueur , qui peut avoir plusieurs :teams travers :contracts aussi (au cours de la carrière de ce joueur ). Mais dans ce cas, où il n’existe qu’un seul modèle nommé (c’est-à-dire un utilisateur ), nommer à travers la relation through: (par exemple à through: :follow , ou, comme dans les posts, à through: :post_connections ) nommer la collision pour différents cas d'utilisation de (ou points d'access dans) la table de jointure. :follower_follows et :followee_follows ont été créés pour éviter une telle collision de noms. Maintenant, un utilisateur peut en avoir plusieurs :followers via :follower_follows et beaucoup :followees travers :followee_follows .

Pour déterminer un utilisateur : suiveurs (sur un appel @user.followees à la firebase database), Rails peut maintenant regarder chaque instance de class_name: "Follow" où cet utilisateur est le suiveur (ie foreign_key: :follower_id ) via: tel utilisateur : followee_follows. Pour déterminer un utilisateur : suiveurs (sur un appel à @user.followers à la firebase database), Rails peut maintenant regarder chaque instance de class_name: "Follow" où cet utilisateur est le suiveur (ie foreign_key: :followee_id ) via: un tel utilisateur : follower_follows.

Si quelqu’un venait ici pour essayer de créer des relations d’amitié dans Rails, je les renverrais à ce que j’ai finalement décidé d’utiliser, à savoir copier ce que «Moteur de communauté» a fait.

Vous pouvez vous référer à:

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

et

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

pour plus d’informations.

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" 

Pour la belongs_to_and_has_many bidirectionnelle belongs_to_and_has_many , reportez-vous à la bonne réponse déjà publiée, puis créez une autre association avec un nom différent, les clés étrangères inversées et assurez-vous que class_name défini pour pointer vers le modèle correct. À votre santé.

Si quelqu’un avait des problèmes pour obtenir l’excellente réponse au travail, par exemple:

(L’object ne supporte pas #inspect)
=>

ou

NoMethodError: méthode indéfinie `split ‘pour: Mission: Symbol

La solution consiste alors à remplacer :PostConnection par "PostConnection" , en remplaçant bien sûr votre nom de classe.

Inspiré par @ Stéphan Kochen, cela pourrait fonctionner pour les associations bidirectionnelles

 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 

alors post.posts && post.reversed_posts devraient fonctionner, au moins pour moi.