Doctrine ORM – Héritage une classe par table sur plusieurs niveaux

La méthode de traduction une classe par table du modèle relationnel au modèle objet – ou inversement – est une stratégie qui se caractérise par le fait que dans une hiérarchie, chaque classe – abstraite ou concrète – issue d’un modèle objet est associée à une table d’un modèle relationnel correspondant. Cette association se réalise par le biais de la configuration d’un ORM : le mapping.

Pour faire simple, dans un modèle hiérarchique, pour chaque classe objet il existe une table correspondante (dans le modèle relationnel associé). Ces classes sont nommées entités.

Exemple d'héritage une classe par table (Class Table Inheritance) - © Martin Fowler
Exemple d’héritage une classe par table (Class Table Inheritance) – © Martin Fowler

Pour avoir de plus amples informations sur cette méthode de traduction d’héritage, je vous invite à faire un tour sur le blog de Martin Fowler : http://martinfowler.com/eaaCatalog/classTableInheritance.html

Dans cet article, il s’agit de décrire la méthode permettant de mettre en application ce type d’héritage pour une hiérarchie de un ou plusieurs niveaux. Dans ce cadre, il sera décrit la méthode pour traduire un modèle conceptuel de données (MCD) vers son modèle physique de données (MPD).

Dans l’exemple qui suit, les technologies utilisées sont Doctrine pour l’ORM et MySQL pour la base de données relationnelle, tandis que le langage décrivant les entités sera PHP. Cependant, ce principe reste applicable ou dérivable pour d’autres technologies.

Le logiciel utilisé dans l’exemple pour la création du modèle physique de donnée se nomme MySQL Workbench : http://www.mysql.fr/products/workbench/

Traduction du modèle conceptuel (UML)

Notre cas d’étude est basé sur le modèle conceptuel de données ci-dessous.

Exemple-two-level-hierarchy
Modèle conceptuel de données (MCD)

Les classes grisées sont des classes abstraites tandis que les classes sur fond blanc sont des classes concrètes.

Création du modèle physique de données

Les tables

Pour chaque classe, abstraite ou concrète, il est nécessaire de créer la table correspondante :

Modèle physique de données (MPD) sans les relations
Modèle physique de données (MPD) sans les relations

Les relations identifiées

Une relation identifiée correspond à une relation entre deux tables, où la clé étrangère décrivant cette relation fait partie de la clé primaire de la table la contenant.

Relation identifiée entre la table Animal et Elephant
Relation identifiée entre la table Animal et Elephant

Ci-dessus, la relation identifiée est celle entre la table Elephant et la table Animal. Cette relation doit son existence à la clé étrangère et primaire id de la table Elephant. Cette clé étrangère pointe sur le champ id de la table Animal.

Cela reviens à dire que pour tout enregistrement e dans la table Elephant correspond un enregistrement e’ dans la table Animal tel que e.id = e’.id. On peut aussi dire qu’un enregistrement dans la table Elephant ne peut pas exister sans un enregistrement associé dans la table Animal.

Un seul niveau pour l’optimisation des requêtes

Le modèle relationnel contrairement au modèle objet ne conserve qu’un seul niveau de « hiérarchisation ». C’est à dire que toutes les tables correspondantes aux sous-classes d’une hiérarchie ont une relation identifiée vers la table correspondante à la classe parente de cette même hiérarchie.

Cette structure permet d’optimiser les requêtes produites par l’ORM.

total

Ci-dessus, on note que les tables Cobra et Iguane qui dérivent de Reptile sont directement jointes à la table « parente » Animal. Tandis que dans le modèle objet les classes Cobra et Iguane dérivent de la classe Reptile, qui elle même, dérive de la classe Animal.

Le discriminant

Le discriminant va permettre de déterminer le « type », d’un enregistrement via une valeur. Cette valeur peut être décrite dans différents type (VARCHAR, INTEGER, etc) on utilise cependant communément le type chaîne VARCHAR. Chacune des valeurs représentant le « type » d’un enregistrement correspond à un type du modèle objet. Par exemple on utilisera la valeur « cobra » pour le type Cobra, la valeur « elephant » pour le type Elephant, etc. La correspondance entre la valeur et le type de classe doit être précisée dans la configuration de l’ORM (nous verrons comment faire par la suite dans ORM Mapping du discriminant).

Le champ représentant le discriminant doit être défini dans la table « parente » : celle correspondante à la classe située au plus haut niveau de la hiérarchie. Dans notre cas, il s’agit de la table Animal.

Table Animal avec le champs discriminant dtype
Table Animal avec le champs discriminant dtype

Exemple d’enregistrement :

Un enregistrement e dans la table Animal dont le discriminant (valeur du champ dtype) est elephant permettra de déterminer qu’il devrait exister un enregistrement e’ dans la table Elephant associé à e tel que e.id = e’.id.

Exemple d’enregistrement de l’entité Elephant dans la base de données

La suppression en cascade

Les relations entre les tables doivent être paramétrées de manière à autoriser la suppression en cascade. C’est ce qui permettra au moteur de base de données de supprimer automatiquement les enregistrements de la table enfant pointant sur un enregistrement de la table « parente » en cours de suppression. Par exemple si on supprime l’animal d’identifiant 1 de type elephant, la ligne d’identifiant 1 dans la table Elephant sera aussi supprimé automatiquement. Ceci permet de conserver une intégrité dans les données.

Option de suppression en cascade dans MySQL Workbench

Configuration de l’ORM (Mapping)

Description de la jointure

Pour que l’ORM puisse comprendre la méthode d’héritage appliquée, il faut la décrire. Les trois méthodes les plus connues sont une classe par table (Class per Table Inheritance), une seule table (Single table Inheritance) et une table par classe concrète (Concrete Table Inheritance). Elles ne sont pas toutes supportées par l’ensemble des ORM. Par exemple Doctrine ne supporte que les deux premières.

Exemple (Doctrine) :

Dans notre cas Class per Table est définie par la valeur JOINED dans Doctrine.

<entity name="Animal" table="Animal" inheritance-type="JOINED">

Mapping du discriminant

C’est dans la configuration de l’ORM que l’on décrit l’ensemble des valeurs des discriminants et à quelles sous-classes ils correspondent.

Exemple (Doctrine) :

<discriminator-column name="dtype" type="string" />
<discriminator-map>
    <discriminator-mapping value="elephant" class="Elephant" />
    <discriminator-mapping value="cobra" class="Cobra" />
    <discriminator-mapping value="iguane" class="Iguane" />
</discriminator-map>

Ci-dessus, le mapping du discriminant est défini dans la configuration de l’entité parente.

Implémentation du modéle objet

Héritage des classes (entités)

Les classes du modèle objet doivent définir leurs héritages normalement.

Exemple en-tête des classes (PHP) :

abstract class Animal
class Elephant extends Animal

abstract class Reptile extends Animal
class Cobra extends Reptile
class Iguane extends Reptile

3 thoughts on “Doctrine ORM – Héritage une classe par table sur plusieurs niveaux”

    1. Salut Keverg,

      En effet le lien d’héritage entre reptile et iguane (comme celui entre reptile et cobra) que l’on voit bien en évidence sur le schéma conceptuel (au début de l’article) n’existe pas au niveau de la base de données. Toutes les classes dérivées, quel que soit le niveau de profondeur, sont directement liées à la table correspondante au plus haut niveau de la hiérarchie d’héritage (ici Animal).

      En fait doctrine (et la plupart des ORM) retrouvent ce lien d’héritage grâce à l’implémentation PHP grâce aux extends + le mapping. Ainsi dès lors où l’on va créer un nouvel objet Iguane,doctrine va automatiquement retrouver ces liens et insérer une ligne dans la table Animal, Reptile et Iguane.

      Pour la récupération, si tu as implémenté les classes « Repository » correspondantes aux entités, tu pourras effectuer un type de requête du genre : $em->getRepository(‘Reptile’)->findAll();
      Le résultat fourni par l’ORM correspondra à l’ensemble des Reptiles, c’est à dire dans notre cas les résultats des tables Cobra + Iguane mappés automatiquement sur les classes qui vont bien.

      Pour terminer, si tout les niveaux de profondeurs d’héritage ne doivent pas êtres intégrés au niveau de la base, c’est principalement pour optimiser les requêtes générés par l’ORM.

      Dans le cas présent, il n’y a besoin que d’une seule jointure SQL, alors que si il avait fallu lier la table Iguane à la table Reptile et la table Reptile à celle Animal on aurait déjà deux jointures.

      Tu comprendras donc que si tu as une hiérarchie avec une grande profondeur, le nombre de jointures augmentera relativement à cette profondeur, ce qui augmentera par conséquent le nombre de « plans d’exécution » de la requête au niveau de la base de données, ce qui se ressentira indéniablement sur les performances.

      J’espère avoir été clair et que ma réponse apporte un éclairage sur tes interrogations. Peut-être devrait-je mettre le code source en téléchargement ?

      Si tu as besoin de compléments d’informations, ou de poser d’autres questions n’hésite pas !
      A bientôt et merci pour ta lecture !

Répondre à _ Annuler la réponse.

%d blogueurs aiment cette page :