On The Rails Again

Les associations Many-to-Many

Posté par Nima le 11 octobre 2011

Pré-requis :
  • avoir des bases en SQL (table, clé primaire, clé étrangère ;
  • avoir vu les associations One-to-Many.
Ce que nous allons voir :
  • comprendre une relation Many-to-Many ;
  • créer une relation Many-to-Many avec Rails.

Présentation du problème

Lorsque l'on conçoit une application on tombe souvent sur un problème simple : un objet A peut avoir plusieurs objets B et un objet B peut avoir plusieurs objets A... Bon, tout ceci n'est pas très clair, mais quand on l'illustre par un exemple ça le devient.

Prenons un exemple très courant : Elèves - Cours. Un élève peut avoir plusieurs cours et appartenir à plusieurs cours. Ou encore avec un autre exemple : Articles - Tags. Un article peut avoir et peut appartenir à plusieurs tags. Effectivement un tag n'est pas unique, ce n'est pas parce qu'un article a le tag Ruby qu'aucun autre article ne pourra avoir ce même tag.

En théorie

Cette association ressemble à une double association One-to-Many, mais le problème est : « Où mettre la/les clé(s) étrangère(s) pour connaître les associations entre les objets ? »

Pour ça, il faut créer ce que l'on appelle une table de jointure (ou d'association) pour stocker les couples de clés étrangères appartenant aux deux modèles concernés. Il est important de noter que cette table d'association ne sera constituée uniquement de deux colonnes qui contiendront des clés étrangères. En d'autres termes, cette table ne doit pas contenir de clé primaire.

En pratique

Avant de continuer, voilà une représentation de la relation des modèles entre eux.

Représentation schématique de la relation Many to Many

Création des modèles

Prenons l'exemple des Articles et des Tags. Nous avons donc deux modèles, le modèle Article et le modèle Tag et nous voulons pouvoir associer plusieurs tags à un article mais également plusieurs articles au même tag. Commençons par créer les deux modèles à l'aide des scaffolds :

$ rails generate scaffold Article title:string content:text
$ rails generate scaffold Tag name:string

Création de la migration

Bon, comme vous vous en doutez, la table de jointure ne vas pas se créer toute seule. Pour la créer nous allons passer par une migration (hors de question de la créer à la « main » !)

Les migrations sont un moyen pratique de modifier la base de données de façon structurée et organisée. Elles décrivent des actions à effectuer comme créer une table, ajouter/supprimer une colonne à une table, etc. Elles permettent également de garder une trace de toutes les modifications effectuées sur la base de données.

Convention de nommage

Mais avant de faire ça, comme le principe de Rails est « Convention Over Configuration », il faut bien comprendre comment nommer cette table de jointure. Voici les conventions à respecter pour le nom de cette table :

  • nom_modèle_1 + _ + nom_modèle_2 ;
  • Les deux noms de tables doivent être au pluriel ;
  • Les noms des tables sont ordonnés alphabétiquement. Donc Articles vient avant Tags.

Dans notre cas, le nom sera donc : articles_tags. Article en premier car il est alphabétiquement avant Tag, et les deux noms au pluriel. Si le nom d'un de vos modèles est en plusieurs mots, il suffit de remplacer les espaces par des « _ ».
Exemple : Le nom d'une table de jointure pour un modèle BlogPost et un modèle Category serait blog_posts_categories.

Pour les anglophobes : faites bien attention au pluriel anglais pour les mots qui se finissent en « y ». « One category but two categories ! »

Le fichier migration

Maintenant que nous savons comment nommer cette table et que l'on sait qu'elle ne doit pas avoir de clé primaire nous pouvons créer la migration. Tâchez de donner un nom explicite à celle-ci (celui du fichier qui apparaitra dans le dossier db/migrations), cela pourrait vous servir à l'avenir si vous avez besoin de revenir en arrière.

$ rails generate migration CreateArticlesTagsJoinTable

Il faut maintenant éditer cette migration pour y ajouter l'instruction de création de la table avec le bon nom (comme nous avons vu ci-dessus).

# db/migrations/20111009221942_create_articles_tags_join_table
class CreateArticlesTagsJoinTable < ActiveRecord::Migration
  def self.up
    create_table :articles_tags, :id => false do |t|
      t.references :article, :tag # Pour créer les clés etrangères
    end
    add_index :articles_tags, [:article_id, :tag_id] # Optionnel
  end
  def self.down
    drop_table :articles_tags
  end
end

Comme on le voit, il faut absolument préciser :id => false pour qu'aucune clé primaire ne soit créée. L'instruction add_index est optionnelle mais permet d'accélérer les recherches en base de données.

Ensuite, pour exécuter la migration il vous suffit d'éxécuter la commande suivante :

rake db:migrate

Adaptation des modèles

Il ne reste plus qu'à spécifier aux modèles les associations et le tour est joué ! Traduction : dire aux Articles qu'ils ont et appartiennent (has_and_belongs) à plusieurs (to_many) Tags. Et vice-versa !

# app/models/article.rb
class Article < ActiveRecord::Base
  has_and_belongs_to_many :tags
end
# app/models/tag.rb
class Tag < ActiveRecord::Base
  has_and_belongs_to_many :articles
end

Utilisation dans les vues

Vous pouvez être amené à vous demander comment gérer ces dépendances dans un formulaire. Par exemple, lors de la création d'un article je voudrais pouvoir choisir tous les tags qui lui seront associés. Voilà une méthode pour faire ça :

# app/views/articles/_form.html.erb
<%= form_for @article do |f| %>
  <label>Title: <%= f.text_field :title %> </label>
  <% Tag.all.each do |tag| %>
    <label><%= tag.name %>
      <%= check_box_tag "article[tag_ids][]", tag.id, @article.tags.include?(tag) %>
    </label>
  <% end %>
  <%= f.submit %>
<% end %>

Ce qui se passe ici c'est que lors de la création d'un article on va chercher tous les tags existant en base de données (Tag.all) et on affiche une checkbox pour chacun de ces tags. Ce formulaire servant également pour l'édition, on regarde si l'article n'a pas déjà ce tag (@article.tags.include?(tag)) et si oui, la checkbox sera cochée.

De plus, il n'y a rien a changer du côté du contrôleur, l'affectation des tags à l'article se fera automatiquement grâce au nom de l'input qui est article[tag_ids][]. En effet, dans la methode create du contrôleur de l'article on retrouve :

# app/controllers/articles_controller.rb
def create
  @article = Article.new(params[:article])
  ...
end

L'article qui va être mis en base de donnée est créé à partir des paramètres qui sont stockés dans le tableau article lors de l'envoi du formulaire.

Utilisation dans les contrôleurs

Une fois toute la partie base de données et modèle mis en place, Rails permet d'accéder aux objets entre eux très simplement. Par exemple, pour accéder à tous les tags d'un article on peut faire :

@article.tags # Renvoie un tableau contenant tous les tags de @article

Pour ce qui est de l'affectation il y a deux possibilités :

@article.tags << Tag.first # On ajoute au tags déjà existant
@article.tags = Tag.first  # On remplace les tags déjà existant

Si vous utilisez l'opérateur =, il faut bien garder en tête que tous les tags que possédait l'article seront remplacés par la nouvelle affectation. Au contraire, si vous utilisez l'opérateur <<, le tag sera ajouté aux tags déjà existant.

Références

Voilà pour les associations Many-to-Many. Je posterai un article sur les associations Many-to-Many avancées (has_many :through) dans les jours à venir. Si vous avez des questions ou des améliorations à proposer n'hésitez pas à laisser un commentaire !