Comment puis-je créer une relation plusieurs-à-plusieurs avec le même modèle dans les rails?
Par exemple, chaque publication est liée à de nombreuses publications.
la source
Comment puis-je créer une relation plusieurs-à-plusieurs avec le même modèle dans les rails?
Par exemple, chaque publication est liée à de nombreuses publications.
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_posts
dans cette situation, alors j'ai décidé de prendre à la post_connections
place.
Il est très important :id => false
d'omettre la id
colonne 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' posts
association créera des enregistrements dans la post_connections
table selon le cas.
Quelques points à noter:
a.posts = [b, c]
, la sortie de b.posts
n'inclut pas le premier message.PostConnection
. Vous n'utilisez normalement pas de modèles pour une has_and_belongs_to_many
association. 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 category
champ à 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 PostConnection
modè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_a
ou post_b
que nous avons affaire à un message ici. Nous devons le dire explicitement.
Maintenant le Post
modè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_many
association, 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_b
association de PostConnection
.
Il manque juste une dernière chose , c'est que nous devons dire à Rails que a PostConnection
dépend des postes auxquels il appartient. Si l'un ou les deux de post_a_id
et 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 Post
modè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_connections
a un :dependent
paramè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_many
association 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_name
ici, 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_connections
et reverse_post_connections
; il se reflétera parfaitement dans l' posts
association:
>> 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_many
associations 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_many
et 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 OR
les conditions ci-dessus avec post_a_id
et post_b_id
inversé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_many
ce que :finder_sql
, :delete_sql
etc. Ce n'est pas assez. (Je suis également ouvert aux suggestions ici. Quelqu'un?)
:foreign_key
sur lehas_many :through
n'est pas nécessaire, et j'ai ajouté une explication sur la façon d'utiliser le:dependent
paramètre très pratique pourhas_many
.Pour répondre à la question posée par Shteef:
Associations en boucle bidirectionnelles
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 :
Voici comment le code pour follow.rb :
Les choses les plus importantes à noter sont probablement les termes
:follower_follows
et:followee_follows
dans user.rb. Pour utiliser une association run of the mill (sans boucle) comme exemple, une équipe peut avoir plusieurs:players
through:contracts
. Ce n'est pas différent pour un joueur , qui peut avoir beaucoup à:teams
travers:contracts
aussi 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 exemplethrough: :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_follows
et:followee_follows
ont été créés pour éviter une telle collision de noms. Désormais, un utilisateur peut en avoir plusieurs à:followers
travers:follower_follows
et plusieurs à:followees
travers:followee_follows
.Pour déterminer les suivis d' un utilisateur (lors d'un
@user.followees
appel à la base de données), Rails peut désormais examiner chaque instance de nom_classe: «Follow» où cet utilisateur est le suiveur (c'est-à-direforeign_key: :follower_id
) via: cet utilisateur : followee_follows. Pour déterminer les suiveurs d' un utilisateur (lors d'un@user.followers
appel à la base de données), Rails peut désormais examiner chaque instance de nom_classe: «Follow» où cet utilisateur est le suivant (c'est-à-direforeign_key: :followee_id
) à travers: un tel utilisateur : follower_follows.la source
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
..
la source
Inspiré par @ Stéphan Kochen, cela pourrait fonctionner pour des associations bidirectionnelles
alors
post.posts
&&post.reversed_posts
devrait les deux travaux, au moins a fonctionné pour moi.la source
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 avezclass_name
défini pour pointer vers le bon modèle. À votre santé.la source
Si quelqu'un avait des problèmes pour obtenir l'excellente réponse, comme:
ou
Ensuite, la solution est de remplacer
:PostConnection
par"PostConnection"
, en remplaçant bien sûr votre nom de classe.la source