Comment créer une relation many-to-many avec le même modèle dans rails ?
Par exemple, chaque message est relié à de nombreux messages.
Comment créer une relation many-to-many avec le même modèle dans rails ?
Par exemple, chaque message est relié à de nombreux messages.
Il existe plusieurs types de relations many-to-many ; vous devez vous poser les questions suivantes :
Cela laisse quatre possibilités différentes. Je vais les passer en revue ci-dessous.
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 la classe elle-même.
Il s'agit du code le plus compact.
Je vais commencer par ce schéma de base pour vos articles :
create_table "posts", :force => true do |t|
t.string "name", :null => false
end
Pour toute relation many-to-many, 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 donnerait posts_posts
dans cette situation, j'ai donc décidé de prendre post_connections
à la place.
Très important ici est :id => false
pour omettre l'option par défaut id
colonne. Rails veut cette colonne partout sauf sur les tables de jonction pour has_and_belongs_to_many
. Il se plaindra bruyamment.
Enfin, remarquez que les noms des colonnes ne sont pas standard non plus (non post_id
), afin de prévenir les conflits.
Maintenant, dans votre modèle, vous devez simplement indiquer à Rails ces quelques éléments non standard. Cela ressemblera à ce qui suit :
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 d'exécution d'une session irb 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 à la posts
créera des enregistrements dans la base de données post_connections
le cas échéant.
Quelques points à noter :
a.posts = [b, c]
la sortie de b.posts
ne comprend pas le premier message.PostConnection
. Normalement, vous n'utilisez pas de modèles pour un has_and_belongs_to_many
association. Pour cette raison, vous ne pourrez pas accéder à des champs supplémentaires.Bon, maintenant... Vous avez un utilisateur régulier qui a posté aujourd'hui sur votre site un message expliquant que les anguilles sont délicieuses. Ce parfait inconnu se rend sur votre site, s'inscrit et rédige un article virulent sur l'ineptie de l'utilisateur régulier. Après tout, les anguilles sont une espèce en voie de disparition !
Vous souhaitez donc indiquer clairement dans votre base de données que l'article B est une critique virulente de l'article A. Pour ce faire, vous voulez ajouter une balise category
à l'association.
Ce dont nous avons besoin n'est plus un 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 jonction. C'est ce modèle supplémentaire qui nous permet d'ajouter des informations supplémentaires à l'association elle-même.
Voici un autre schéma, très similaire au précédent :
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, post_connections
hace ont un id
colonne. (Il y a pas de :id => false
paramètre). Ceci est nécessaire, car il y aura un modèle ActiveRecord régulier pour accéder à la table.
Je vais commencer par le PostConnection
parce que 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 de post_a
o post_b
que nous avons affaire à un poste 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 le premier has_many
l'association, nous disons au modèle de rejoindre 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 qui sont connectés à celui-ci, par le biais de notre première association. post_connections
suivi par le post_b
association de PostConnection
.
Il y a juste une dernière chose manquant, et c'est que nous devons dire à Rails qu'une PostConnection
dépend des postes auxquels il appartient. Si l'un ou les deux post_a_id
y post_b_id
étaient NULL
alors ce lien ne nous dirait pas grand-chose, n'est-ce pas ? Voici comment nous faisons cela 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 différentes ici :
has_many :post_connections
a un supplément :dependent
paramètre. Avec la valeur :destroy
nous indiquons à Rails qu'une fois que ce message aura disparu, il pourra détruire ces objets. Une autre valeur que vous pouvez utiliser ici est :delete_all
ce qui est plus rapide, mais n'appellera pas les hooks de destruction si vous en utilisez.has_many
association pour le inverser les connexions aussi, celles qui nous ont liés à travers post_b_id
. De cette façon, Rails peut aussi les détruire proprement. Notez que nous devons spécifier :class_name
ici, parce que le nom de la classe du modèle ne peut plus être déduit à partir de :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 créer une PostConnection et en finir :
>> 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 aussi manipuler le post_connections
y reverse_post_connections
les associations ; il se reflétera clairement dans les 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
=> []
En normal has_and_belongs_to_many
l'association est définie dans ambos modèles impliqués. Et l'association est bidirectionnelle.
Mais il n'y a qu'un seul modèle de poste 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 jonction.
On peut le constater en accédant simplement aux associations à partir de 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, il faudrait trouver un moyen de faire en sorte que Rails OR
les conditions ci-dessus avec post_a_id
y post_b_id
inversé, de sorte qu'il regarde dans les deux sens.
Malheureusement, la seule façon de le faire que je connaisse est plutôt compliquée. Vous devrez spécifier manuellement votre SQL en utilisant des options de has_and_belongs_to_many
comme :finder_sql
, :delete_sql
etc. Ce n'est pas joli. (Je suis ouvert aux suggestions ici aussi. Quelqu'un ?)
Merci pour vos commentaires ! :) J'ai fait quelques modifications supplémentaires. Plus précisément, le :foreign_key
sur le has_many :through
n'est pas nécessaire, et j'ai ajouté une explication sur la façon d'utiliser la très pratique :dependent
pour has_many
.
@Shtééf même l'affectation en masse (update_attributes) ne fonctionnera pas dans le cas d'associations bidirectionnelles ex : postA.update_attributes({:post_b_ids =>[2,3,4]}) une idée ou une solution de contournement ?
Pour répondre à la question posée par Shteef :
La relation suiveur-suivi chez Utilisateurs est un bon exemple d'une association en boucle bidirectionnelle. A Utilisateur peut en avoir plusieurs :
Voici comment le code pour utilisateur.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 éléments les plus importants à noter sont probablement les termes suivants :follower_follows
y :followee_follows
dans user.rb. Pour prendre l'exemple d'une association courante (non bouclée), une association de type Équipe peut avoir de nombreux : players
par le biais de :contracts
. Il en va de même pour un Joueur qui peuvent avoir plusieurs :teams
par le biais de :contracts
également (au cours d'une telle Joueur ). Mais dans ce cas, où il n'existe qu'un seul modèle nommé (c'est-à-dire une Utilisateur ), en nommant la relation through : de manière identique (par ex. through: :follow
ou, comme cela a été fait ci-dessus dans l'exemple des postes, through: :post_connections
) entraînerait une collision de noms pour différents cas d'utilisation (ou points d'accès) de la table de jonction. :follower_follows
y :followee_follows
ont été créés pour éviter une telle collision de noms. Maintenant, un Utilisateur peut avoir de nombreux :followers
par le biais de :follower_follows
et beaucoup :followees
par le biais de :followee_follows
.
Pour déterminer un Utilisateur s :followees (sur un @user.followees
à la base de données), Rails peut maintenant regarder chaque instance de class_name : "Follow" où cet utilisateur est le suiveur (c'est-à-dire foreign_key: :follower_id
) par : tel Utilisateur de :followee_follows. Pour déterminer un Utilisateur s :followers (sur un @user.followers
à la base de données), Rails peut maintenant examiner chaque instance de class_name : "Follow" où un tel appel est effectué. Utilisateur est la personne à suivre (c'est à dire foreign_key: :followee_id
) par : tel Utilisateur de :follower_follows.
Si quelqu'un venait ici pour essayer de trouver comment créer des relations d'amitié dans Rails, je le renverrais à ce que j'ai finalement décidé d'utiliser, à savoir copier ce qu'a fait 'Community Engine'.
Vous pouvez vous y 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"
Si quelqu'un a eu des problèmes pour faire fonctionner l'excellente réponse, par exemple :
(L'objet ne supporte pas #inspect)
\=>
ou
NoMethodError : méthode `split' non définie pour :Mission:Symbol
La solution consiste alors à remplacer :PostConnection
con "PostConnection"
en substituant votre nom de classe bien sûr.
Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.