On The Rails Again

Les associations Many-to-Many avancées

Posté par Nima le 17 octobre 2011

Pré-requis :
Ce que nous allons voir :
  • stocker des informations relatives à une relation Many-to-Many ;
  • utiliser et manipuler les associations.

Présentation du problème

Avec les relations Many-to-Many nous avons déjà réussi à résoudre un certain nombre de problèmes pour ce qui était des relations entre deux objets. Allons encore plus loin. Supposons que vous vouliez stocker des informations supplémentaires et relatives à votre relation. Des propriétés propres à la relation et non aux objets liés.

Un exemple concret

Illustrons le problème. Supposons que nous avons deux modèles : Doctor et Patient.

Un docteur a plusieurs patients et un patient peut avoir plusieurs docteurs. Pour gérer les rendez-vous, nous avons besoin de savoir la date du prochain rendez-vous entre le docteur D et le patient P.

Dans la théorie

Il nous faut une table de jointure entre ces modèles avec la référence du docteur, la référence du patient et des champs qui décrivent le rendez-vous. Cette approche est très similaire à une relation Many-to-Many classique à la différence que cette table ne stocke pas uniquement des références.

Voici tout ce qui diffère d'une relation Many-to-Many classique :

  • Le stockage en base de données est différent, la table de jointure doit avoir une colonne clé primaire. (contrairement à la relation Many-to-Many classique où l'on ne veut justement pas de clé primaire).
  • Il faut créer un modèle pour cette relation. Donc un modèle correspondant à la table en base de donnée.
  • Il n'y a pas de convention pour le nom de la table. Nous ne sommes pas obligé d'appeler la table DoctorPatients. Il est même conseillé de donner un nom plus adéquat pour ces tables. Nous l'appellerons Appointments.

Dans la 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 avancées

Préparer la base de données

Créons rapidement les modèles Doctor et Patient.

$ rails generate scaffold Doctor  name:string speciality:string
$ rails generate scaffold Patient name:string

Comme je vous l'ai dit, il est nécessaire de créer un modèle correspondant à la table de jointure :

$ rails generate model Appointment

On ne crée que le modèle car aucune vue ou contrôleur n'a besoin d'y être associé. De plus, le fait de passer par la commande rails permet d'avoir le modèle et la migration générés en une fois. Il faut maintenant éditer la migration générée pour qu'elle corresponde à ce que l'on veut :

# db/migrate/x_create_appointments.rb
class CreateAppointments < ActiveRecord::Migration
  def self.up
    create_table :appointments do |t|
      t.references :doctor, :patient
      t.datetime   :appointment_date
      t.timestamps
    end
    add_index :appointments, [:doctor_id, :patient_id]
  end
  def self.down
    drop_table :appointments
  end
end

Comme dans la relation Many-to-Many classique, on spécifie les références aux modèles Doctor et Patient . Puis on rajoute les informations nécéssaires à la description de la relation entre les deux objets. On peut également rajouter les indexes pour doctor_id et patient_id qui sont les clés qui serviront à la base de données lors de la recherche dans la table.

On notera que cette table DOIT avoir une clé primaire, c'est pour ça que l'on n'a pas mis :id => false après le create_table.

Adapter les modèles

Maintenant que la table de jointure est crée, il faut renseigner dans les modèles la façon dont ils sont liés entre eux.

# app/models/doctor.rb
Class Doctor < ActiveRecord::Base
  has_many :appointments
  has_many :patients,    :through => :appointments
end
# app/models/patient.rb
Class Patient < ActiveRecord::Base
  has_many :appointments
  has_many :doctors,     :through => :appointments
end
# app/models/appointment.rb
Class Appointment < ActiveRecord::Base
  belongs_to :doctor
  belongs_to :patient
end

Comme vous le voyez, on n'utilise plus has_and_belong_to_many. Maintenant, les docteurs ont plusieurs patients et vice-versa à travers la table Appointment. Dans cette table seront stockées les informations qui lient les docteurs aux patients. C'est pour cela que l'on utilise le mot clé :through. On indique à Rails que s'il veut trouver les patients liés à un docteur il devra passer par la table Appointments pour les trouver.

Comment utiliser ces relations dans les contrôleurs

Manipuler les associations (ici des «appointments»)

Plusieurs moyens s'offre à vous pour créer un Appointment qui lie un Doctor et un Patient. En voici un exemple :

# @patient et @doctor contiennent une instance d'un docteur et d'un patient
Appointment.create(:doctor => @doctor, :patient => @patient, :appointment_date => Date.tomorrow)

Vous pouvez également créer une association sans spécifier tous les champs, juste en ajoutant un nouveau patient à la liste des docteurs ou vice-versa :

@doctor.patients << @patient
# Ou bien 
@patient.doctors << @doctor

Cela créera automatiquement un Appointment avec les bonnes valeurs pour doctor_id et patient_id, mais tous les autres attributs (ici, la date) seront fixés à null.

Si vous voulez réaffecter toute la liste des patients ou des docteurs vous pouvez faire comme suit :

@doctor.patients = @new_array_of_patients

Ici plusieurs choses sont à noter :

  • des appointments sont créés pour lier les nouveaux patients au docteur ;
  • si des patients de la nouvelle liste étaient déjà présents, leurs appointments ne seront pas supprimés ;
  • les appointments de tous les patients qui ne sont pas dans la nouvelle liste seront supprimés.

Supprimer une association

Comme je l'ai dit précédemment, en réaffectant de nouveaux patients à un docteur, ses anciennes associations seront automatiquement supprimées. Si vous voulez supprimer une association en particulier, vous pouvez simplement faire comme suit :

# On supprime tous les appointments qui existent entre @doctor et @patient
@doctor.patients.delete(@patient)
# Ou bien
@patient.doctors.delete(@doctor)
# Ou encore, pour supprimer les appointments qui ont une date antérieur à aujourd'hui
@patient.appointments.where("appointment_date < ?", Date.today).delete

Les helpers Rails

Rails crée également un tas de helpers qui peuvent être très sympa et que vous trouverez dans l'API Rails. Voici un un avant goût :

@doctor.patient_ids
@doctor.patients.delete_all
@doctor.patients.size
# Etc.

Également utile pour créer des «raccourcis»

Vous pouvez vous servir des has_many :through pour vous faire des raccourcis. Je m'explique. Supposez qu'un document à plusieurs sections et que les sections ont plusieurs paragraphes comme le montre le schéma suivant :

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

Il peut arriver que vous ayez besoin d'accéder à tous les paragraphes d'un document. Pour faire cela, vous pouvez par exemple définir vos relations comme suit :

# app/models/document.rb
class Document < ActiveRecord::Base
  has_many :sections
  has_many :paragraphs, :through => :sections
end
# app/models/section.rb
class Section < ActiveRecord::Base
  belongs_to :document
  has_many :paragraphs
end
# app/models/paragraph.rb
class Paragraph < ActiveRecord::Base
  belongs_to :section
end

Et donc pour accéder à tous les paragraphes d'un document il suffit de faire :

@document.paragraphs

Voilà, vous devriez avoir toutes les clés pour mettre en place vos relations. Si vous avez des questions ou des améliorations à proposer n'hésitez pas à laisser un commentaire !

Références