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 :

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.