Description
Bonjour, voici l’épisode 7, sur les associations dans Rails.
Il fait suite à l’épisode 6, où je vous recommande de toujours
considérer la cardinalité de ce dont vous parlez : 0, 1 ou N.
En pratique c’est aussi ce que vous trouverez dans un diagramme
UML ou dans vos bases de données : associations 1 à 0 ou 1, 1 à 1,
1 à N et N à N. Plus qu’un épisode de métaphore, c’est un épisode
qui sert de “pont” entre trois domaines : UML, SQL relationnel,
et ActiveRecord qui est l’ORM de Ruby on Rails.
Là encore, il y a une introduction à Rails et ActiveRecord
dans l’épisode 3 sur les requêtes N+1,
que je vous recommande si vous êtes perdus.
Le cas de l’association
L’ORM, c’est la correspondance entre des objets Ruby et des
données dans une base de données.
On fait une correspondance entre une table et une classe,
un champ dans une table et une propriété de l’objet,
et un enregistrements de la table avec une instance d’objet.
L’enjeu de la modélisation objet est de représenter un
modèle pratique et utilisable de la réalité (à défaut
d’être exact), et une des méthodes les plus connues (je ne
dis pas les seules ni que la norme précise est respectée)
est le diagramme de classes de la norme UML.
Quant aux bases de données relationnelles, elles sont
souvent représentées par quelque chose approchant les
diagrammes entité-relations, les MCD de Merise ou plus
simplement… les schémas de bases de données.
Dans Ruby on Rails également, on a la possibilité de
décrire des associations entre objets.
Note sur l’anglais
Je parlerai de code en anglais avec un énorme accent français,
parce que ça fait une différence entre le code et le commentaire ;
pour retrouver les mêmes mots dans mes exemples que dans la
documentation ou sur StackOverflow, et parce que le code Ruby et Rails
est presque aussi facile à lire que des phrases anglaises.
On peut débattre de “faut-il coder en français ou en anglais”,
mais mon but n’est pas de vous forcer à faire quoi que ce soit :
dans votre équipe, dans votre projet, trouvez votre propre équilibre.
Has Many
L’association 1-N, que Rails nomme Has Many.
Par exemple, un panier a beaucoup d’articles :
Cart Has Many Articles.
À l’inverse, un article ne peut être que dans un seul panier.
C’est l’association Belongs To : Articles Belongs To Cart.
En base de données, ActiveRecord s’attend à trouver la colonne
cart_id dans la table articles, et un id dans la table carts.
Vous pouvez sortir de ces conventions, mais lisez bien la doc
d’ActiveRecord pour apprendre à le faire proprement.
ActiveRecord vous donnera plein de méthodes utiles :
cart.articles pour avoir un tableau des articles du panier,
cart.articles= pour enregistrer ces articles dans le panier,
cart.article_ids pour les identifiants.
De l’autre côté, article.cart, article.cart=, article.cart_id.
Note sur l’enregistrement
Attention, il est important de noter que tout ce que vous faites ici
c’est travailler sur des objets Ruby. Rien n’est enregistré.
Il y a trois étapes :
aller chercher les données en base et en faire des objets
avec un where, find, ou une méthode d’accès comme @cart.goods
travailler sur lesdits objets
avec tous les traitements Ruby et Rails que vous voulez
enregistrer tout cela dans la base de données
si vous avez fait des changements, il faut les sauvegarder
Si vous modifiez des objets Ruby, tant que vous n’avez pas sauvé
vos objets Ruby en base, seul le bout de code qui est allé les
chercher (et les bouts de code auxquels il les a donnés) sont au
courant des modifications. Si quelqu’un d’autre va chercher les
infos en base, il ne voit pas les changements.
HABTM
Il y a aussi l’association N-N, que Rails nomme
Has And Belongs To Many, HABTM pour faire court.
Par exemple, une personne peut posséder plusieurs logements.
Vous pouvez n’être propriétaire de rien du tout ([]),
d’un seul ([:home]), et certains peuvent en avoir énormément
([:home, :beach_house, :ski_house, :fishing_house]).
Mais si votre client souhaite pouvoir noter qu’une maison a
plusieurs propriétaires, et non un seul, 1-N (Has Many) ne suffit pas.
Il faut passer à N-N (HABTM).
La table Owner ne contient pas d’ID de House
(sur un formulaire papier, on écrirait maison 1, maison 2, maison 3….
impossible de savoir combien on devrait en prévoir, et que d’espace
gaspillé !), pas plus que la table House ne contient d’ID de Owner.
Mais il y aura en BDD une table dite de jointure, parce qu’elle
fait le lien entre les données des deux tables.
Rails l’appellera Houses_Owners, et cette table contiendra une clé
house_id et une clé owner_id contenant les identifiants
respectifs de House et Owner.
Il y aura ensuite autant d’enregistrement que de
“liens de possession” entre une maison et un propriétaire.
Les méthodes utiles seront les mêmes, owner.houses, owner.houses=, owner.house_ids et réciproquement house.owners, house.owners= et
house.owner_ids.
Has-Many Through
Mais on sait seulement qu’il y a un lien ! On n’a pas d’information
sur la nature ou les conditions de ce lien.
Si vous voulez que cette association porte des informations, l’idée
de la jointure est bonne. Poussons-la plus loin.
Faites un objet qui relie les deux et dans lequel vous rangez les
informations de ladite relation, puis des Has-Many vers cet objet.
Par exemple pour une location de voiture Person Has Many Rentals
et Car Has Many Rentals. Ainsi chaque Rental sait retrouver
la bonne voiture, son locataire, et tout ce que vous voudriez
ajouter, comme : les dates et heures de début et fin de location,
le prix appliqué, des infos variées etc.
Rails proposera un dernier confort dans ce cas, les Has Many Through,
ici Person Has Many Cars Through Rentals, qui veut dire
“on peut retrouver toutes les voitures d’un client,
à condition d’aller les chercher à travers son historique de locations”,
et vous pourrez alors avoir les deux méthodes utiles
person.cars et person.car_ids à disposition.
Je ne pourrais pas vous donner de person.cars= car il faudrait
alors renseigner tous les objets Rental entre deux.
Has-One
Enfin, je vous mets en garde contre les associations 1-1,
qui sont toujours techniquement possibles, mais qui sont pour moi
assez rares.
En tout cas, c’est signe qu’il y a une question à poser :
on veut dire “un chien a un maître”, Dog Has One Master…
mais en réalité Dog Belongs To Master,
car un maître peut avoir plusieurs chiens (là encore, 0, 1 ou N chiens)
mais un chien ne peut pas avoir plusieurs maîtres.
Enfin, c’est possible dans la vraie vie et mais si c’était le cas,
vous n’auriez pas tenté d’écrire “un chien A UN maître”.
D’autre part parce que si les informations sont si “couplées” et
doivent aller la main dans la main, pourquoi ne pas tout mettre dans
la même table ? Si un client peut avoir plusieurs mails, que certaines
boîtes mail peuvent être partagées… on ne se pose aucune question,
on laisse dans la même table email et nom du client.
Mais quand vous avez un souci avec un Has One, je pense qu’en général
il y aura une évolution un jour pour le transformer en Has Many :
Customer Has One Address deviendra probablement Customer Has Many Addresses
PhoneNumber Has One Customer plutôt Customer Has Many PhoneNumbers
Si vous êtes toujours coincés, voici mon astuce, demandez-vous :
“puis-je avoir l’un sans l’autre ?”
Une adresse sans maison a du sens (terrain vide),
une adresse avec plusieurs maisons a du sens (appartements),
une maison sans adresse n’a pas de sens (on l’a bien construite quelque part),
une maison avec plusieurs adresses est possible mais rare (coins de rues).
Ainsi, on part sur Address Has One House, ce qui était probablement
l’inverse de ce que vous vous attendiez à écrire dans un raisonnement
classique. En tout cas, j’avais pris l’exemple au hasard et
la conclusion m’a surpris moi-même.