Réponses:
Il existe plusieurs types de relations plusieurs à plusieurs; vous devez vous poser les questions suivantes:
Cela laisse quatre possibilités différentes. Je vais les parcourir ci-dessous.
Pour référence: la documentation Rails sur le sujet . Il y a une section appelée «plusieurs-à-plusieurs», et bien sûr la documentation sur les méthodes de classe elles-mêmes.
C'est le code le plus compact.
Je vais commencer par ce schéma de base pour vos messages:
create_table "posts", :force => true do |t|
t.string "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 appellera cette table une combinaison des noms des deux tables que nous joignons. Mais cela se passerait comme posts_postsdans cette situation, alors j'ai décidé de prendre à la post_connectionsplace.
Il est très important :id => falsed'omettre la idcolonne par défaut . Rails veut cette colonne partout sauf sur les tables de jointure pour has_and_belongs_to_many. Il se plaindra bruyamment.
Enfin, notez que les noms de colonnes ne sont pas non plus standard (non post_id) pour éviter les conflits.
Maintenant, dans votre modèle, vous devez simplement informer Rails de ces deux éléments 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 tout simplement fonctionner! Voici un exemple de session irb exécutée 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!">]
Vous constaterez que l'affectation à l' postsassociation créera des enregistrements dans la post_connectionstable selon le cas.
Quelques points à noter:
a.posts = [b, c], la sortie de b.postsn'inclut pas le premier message.PostConnection. Vous n'utilisez normalement pas de modèles pour une has_and_belongs_to_manyassociation. Pour cette raison, vous ne pourrez accéder à aucun champ supplémentaire.Bon, maintenant ... Vous avez un utilisateur régulier qui a publié aujourd'hui un message sur votre site sur la façon dont les anguilles sont délicieuses. Cet étranger total vient sur votre site, s'inscrit et écrit un message de réprimande sur l'ineptie de l'utilisateur régulier. Après tout, les anguilles sont une espèce en voie de disparition!
Donc, vous voudriez préciser dans votre base de données que le post B est un coup de gueule sur le post A. Pour ce faire, vous souhaitez ajouter un categorychamp à l'association.
Ce que 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'ajouter des informations supplémentaires à l'association elle-même.
Voici un autre schéma, très similaire à celui ci-dessus:
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
Remarquez comment, dans cette situation, n'ont une colonne. (Il n'y a pas de paramètre.) Ceci est obligatoire, car il y aura un modèle ActiveRecord régulier pour accéder à la table.post_connections id :id => false
Je vais commencer par le PostConnectionmodèle, car c'est très 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 post_aou post_bque nous avons affaire à un message ici. Nous devons le dire explicitement.
Maintenant le Postmodèle:
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 has_manyassociation, nous disons au modèle de se joindre post_connectionsà posts.id = post_connections.post_a_id.
Avec la seconde association, nous disons à Rails que nous pouvons accéder aux autres postes, ceux liés à celui-ci, via notre première association post_connections, suivie de l' post_bassociation de PostConnection.
Il manque juste une dernière chose , c'est que nous devons dire à Rails que a PostConnectiondépend des postes auxquels il appartient. Si l'un ou les deux de post_a_idet l' post_b_idétaient NULL, alors cette connexion ne nous en dirait pas beaucoup, n'est-ce pas? Voici comment nous procédons dans notre Postmodèle:
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 ici différentes:
has_many :post_connectionsa un :dependentparamètre supplémentaire . Avec la valeur :destroy, nous disons à Rails qu'une fois que ce poste disparaît, il peut continuer et détruire ces objets. Une autre valeur que vous pouvez utiliser ici est :delete_all, qui est plus rapide, mais n'appellera aucun hook de destruction si vous les utilisez.has_manyassociation pour les connexions inverses , celles qui nous ont liés post_b_id. De cette façon, les rails peuvent également les détruire parfaitement. Notez que nous devons spécifier :class_nameici, car le nom de classe du modèle ne peut plus être déduit :reverse_post_connections.Avec ceci en place, je vous apporte une autre session irb à travers 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
Au lieu de créer l'association puis de définir la catégorie séparément, vous pouvez également simplement créer une PostConnection et en finir avec:
>> 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!">]
Et nous pouvons également manipuler les associations post_connectionset reverse_post_connections; il se reflétera parfaitement dans l' postsassociation:
>> 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
=> []
Dans les has_and_belongs_to_manyassociations normales , 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 seule 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_manyet un modèle pour la table de jointure.
Ceci est mieux vu en accédant simplement aux associations depuis irb, et en regardant le SQL que Rails génère dans le fichier journal. Vous trouverez quelque chose comme ce qui suit:
SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )
Pour rendre l'association bidirectionnelle, nous devrons trouver un moyen de rendre Rails ORles conditions ci-dessus avec post_a_idet post_b_idinversées, de sorte qu'il regarde dans les deux sens.
Malheureusement, le seul moyen de le faire que je connaisse est plutôt piraté. Vous devez spécifier manuellement votre SQL en utilisant les options à has_and_belongs_to_manyce que :finder_sql, :delete_sqletc. Ce n'est pas assez. (Je suis également ouvert aux suggestions ici. Quelqu'un?)
Pour répondre à la question posée par Shteef:
La relation suiveur-suivi entre les utilisateurs est un bon exemple d'association bidirectionnelle en boucle. Un utilisateur peut en avoir plusieurs:
Voici à quoi pourrait ressembler le code de 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
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_followset :followee_followsdans user.rb. Pour utiliser une association run of the mill (sans boucle) comme exemple, une équipe peut avoir plusieurs: playersthrough :contracts. Ce n'est pas différent pour un joueur , qui peut avoir beaucoup à :teamstravers :contractsaussi bien (au cours de ce joueur carrière). Mais dans ce cas, où un seul modèle nommé existe (c'est-à-dire un utilisateur ), nommer la relation through: de manière identique (par exemple through: :follow, ou, comme cela a été fait ci-dessus dans l'exemple des articles, through: :post_connections) entraînerait une collision de noms pour différents cas d'utilisation de ( ou points d'accès dans) la table de jointure. :follower_followset:followee_followsont été créés pour éviter une telle collision de noms. Désormais, un utilisateur peut en avoir plusieurs à :followerstravers :follower_followset plusieurs à :followeestravers :followee_follows.
Pour déterminer les suivis d' un utilisateur (lors d'un @user.followeesappel à la base de données), Rails peut désormais examiner chaque instance de nom_classe: «Follow» où cet utilisateur est le suiveur (c'est-à-dire foreign_key: :follower_id) via: cet utilisateur : followee_follows. Pour déterminer les suiveurs d' un utilisateur (lors d'un @user.followersappel à la base de données), Rails peut désormais examiner chaque instance de nom_classe: «Follow» où cet utilisateur est le suivant (c'est-à-dire foreign_key: :followee_id) à travers: un tel utilisateur : follower_follows.
Si quelqu'un venait ici pour essayer de découvrir comment créer des relations amicales dans Rails, je lui ferais référence à ce que j'ai finalement décidé d'utiliser, à savoir copier ce que 'Community Engine' 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"
Inspiré par @ Stéphan Kochen, cela pourrait fonctionner pour des 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_postsdevrait les deux travaux, au moins a fonctionné pour moi.
Pour bidirectionnel 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 vous avez class_namedéfini pour pointer vers le bon modèle. À votre santé.
Si quelqu'un avait des problèmes pour obtenir l'excellente réponse, comme:
(L'objet ne prend pas en charge #inspect)
=>
ou
NoMethodError: méthode non définie `split 'pour: Mission: Symbole
Ensuite, la solution est de remplacer :PostConnectionpar "PostConnection", en remplaçant bien sûr votre nom de classe.
:foreign_keysur lehas_many :throughn'est pas nécessaire, et j'ai ajouté une explication sur la façon d'utiliser le:dependentparamètre très pratique pourhas_many.