ORM Patterns
- Type : Architectural
- Difficulté : 7/10
- Définition succincte : Les ORM (Object-Relational Mapping) Patterns sont des patterns architecturaux qui permettent de mapper des objets du domaine avec des tables relationnelles d'une base de données. Deux des implémentations courantes sont le Data Mapper Pattern (utilisé par Doctrine) et le Active Record Pattern (utilisé par Eloquent dans Laravel). Ces deux patterns gèrent la persistance des objets dans une base de données, mais de manières fondamentalement différentes.
Objectif des ORM Patterns
Les ORM Patterns visent à simplifier la persistance des objets dans une base de données relationnelle en faisant correspondre des classes et des objets en code avec des tables et des colonnes en base de données. Cela permet de manipuler des données sous forme d'objets dans le code, tout en délégant les opérations CRUD (Create, Read, Update, Delete) au framework sous-jacent.
Data Mapper Pattern (Doctrine)
Le Data Mapper Pattern, tel qu'il est utilisé par Doctrine, est un design pattern qui permet de découpler complètement la logique métier d'une application de la logique de persistance (gestion des bases de données). Avec ce pattern, les objets métier (appelés entités dans Doctrine) sont complètement ignorants de la manière dont ils sont stockés, récupérés ou supprimés dans une base de données. C'est le rôle de l'Entity Manager (ou Mapper) de gérer cette persistance.
Doctrine est l'exemple par excellence d'une implémentation de ce pattern dans le monde PHP.
Structure du Data Mapper Pattern avec Doctrine
Entités (Domain Model) : Ce sont les objets métier qui ne contiennent aucune logique de persistance, juste la logique métier. Ils sont représentés par des classes PHP pures, mais ils sont mappés à des tables de base de données via des annotations, XML, ou YAML.
Entity Manager (Data Mapper) : Doctrine fournit un Entity Manager qui est responsable de la gestion des entités. Il est chargé de la persistance, de la récupération et de la suppression des entités dans la base de données.
Unit of Work : Doctrine utilise un modèle appelé Unit of Work, qui surveille toutes les modifications faites aux entités et applique ces changements à la base de données en une seule transaction, une fois que l'opération est validée (
flush()).
Exemple concret avec Doctrine
Prenons l'exemple d'une application de gestion d'utilisateurs qui utilise Doctrine pour persister des entités User dans une base de données.
1. Créer une entité User
Une entité dans Doctrine représente un objet métier (comme un utilisateur), mais elle ne contient aucune méthode liée à la persistance. Doctrine s'occupe de mapper cette entité à une table de la base de données.
1.1. Définition de l'entité User avec des annotations
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="users")
*/
class User
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/** @ORM\Column(type="string", length=100) */
private $name;
/** @ORM\Column(type="string", length=150, unique=true) */
private $email;
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
}
Dans cet exemple, la classe User est une entité Doctrine avec trois propriétés : id, name, et email. Elle n'a aucune méthode save(), update(), ou delete() comme dans le modèle Active Record (Eloquent). C'est Doctrine qui s'occupe de la persistance via l'Entity Manager.
2. Utilisation de l'Entity Manager pour gérer la persistance dans une commande
L'Entity Manager de Doctrine est le composant central qui gère la persistance des entités. C'est lui qui va enregistrer, mettre à jour et supprimer les entités dans la base de données.
2.1. Créer un utilisateur via une commande
Tu peux utiliser une commande CLI pour créer un nouvel utilisateur et le persister dans la base de données.
// src/Command/CreateUserCommand.php
namespace App\Command;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CreateUserCommand extends Command
{
protected static $defaultName = 'app:create-user';
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct();
$this->entityManager = $entityManager;
}
protected function configure()
{
$this
->setDescription('Creates a new user.')
->addArgument('name', InputArgument::REQUIRED, 'The name of the user.')
->addArgument('email', InputArgument::REQUIRED, 'The email of the user.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$user = new User();
$user->setName($input->getArgument('name'));
$user->setEmail($input->getArgument('email'));
$this->entityManager->persist($user);
$this->entityManager->flush();
$output->writeln('User created with ID ' . $user->getId());
return Command::SUCCESS;
}
}
Explication du code :
- Création de l'entité : création d'un nouvel objet
User(un simple objet métier). - Persistance de l'entité : L'Entity Manager est utilisé pour persister (enregistrer) cet objet. L'appel à
persist()prépare l'objet à être sauvegardé dans la base de données. - Validation (flush) : L'appel à
flush()applique les changements dans la base de données.
Doctrine utilise ici le pattern Unit of Work pour gérer les changements. Il garde une trace des entités créées, modifiées ou supprimées, et applique toutes ces opérations en une seule fois lors de l'appel à flush().
2.2. Mettre à jour un utilisateur via une commande
Pour mettre à jour un utilisateur existant, il suffit de récupérer l'entité, de modifier ses propriétés, puis d'appeler flush() pour appliquer les changements.
// src/Command/UpdateUserCommand.php
namespace App\Command;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UpdateUserCommand extends Command
{
protected static $defaultName = 'app:update-user';
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct();
$this->entityManager = $entityManager;
}
protected function configure()
{
$this
->setDescription('Updates a user.')
->addArgument('id', InputArgument::REQUIRED, 'The ID of the user.')
->addArgument('name', InputArgument::REQUIRED, 'The new name of the user.')
->addArgument('email', InputArgument::REQUIRED, 'The new email of the user.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$user = $this->entityManager->getRepository(User::class)->find($input->getArgument('id'));
if (!$user) {
$output->writeln('User not found.');
return Command::FAILURE;
}
$user->setName($input->getArgument('name'));
$user->setEmail($input->getArgument('email'));
$this->entityManager->flush();
$output->writeln('User updated successfully.');
return Command::SUCCESS;
}
}
2.3. Supprimer un utilisateur via une commande
Pour supprimer une entité, utilise la méthode remove() de l'Entity Manager, suivie d'un flush().
// src/Command/DeleteUserCommand.php
namespace App\Command;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DeleteUserCommand extends Command
{
protected static $defaultName = 'app:delete-user';
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct();
$this->entityManager = $entityManager;
}
protected function configure()
{
$this
->setDescription('Deletes a user.')
->addArgument('id', InputArgument::REQUIRED, 'The ID of the user.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$user = $this->entityManager->getRepository(User::class)->find($input->getArgument('id'));
if (!$user) {
$output->writeln('User not found.');
return Command::FAILURE;
}
$this->entityManager->remove($user);
$this->entityManager->flush();
$output->writeln('User deleted successfully.');
return Command::SUCCESS;
}
}
Avantages du Data Mapper Pattern avec Doctrine
- Découplage complet : Les entités
User,Order, etc., ne sont pas responsables de la persistance. Elles ne savent pas comment elles sont stockées ou récupérées, ce qui les rend plus propres et plus faciles à tester. - Flexibilité : Doctrine permet de mapper des entités complexes à des schémas de base de données, y compris les relations complexes.
- Testabilité : Les entités peuvent être testées de manière indépendante sans avoir à se soucier des interactions avec la base de données, facilitant ainsi les tests unitaires.
- Unit of Work : Doctrine utilise le pattern Unit of Work pour gérer les modifications apportées aux entités.
Inconvénients du Data Mapper Pattern avec Doctrine
- Complexité : Doctrine peut être plus complexe à appréhender, notamment en ce qui concerne la gestion des relations et la configuration des entités.
- Performances : Pour de petites requêtes simples, l'Active Record (comme Eloquent) peut être plus performant en raison d'une surcharge moindre.
- Configuration nécessaire : Doctrine nécessite plus de configuration et de mapping (annotations, XML, YAML), contrairement à Eloquent qui suit des conventions très simples.
Active Record Pattern (Eloquent)
Le Active Record Pattern est le design pattern utilisé par Eloquent, l'ORM intégré de Laravel. Contrairement au Data Mapper Pattern, dans le Active Record Pattern, les modèles d'Eloquent sont responsables à la fois de la logique métier et de la logique de persistance. Chaque modèle sait comment se sauvegarder, se mettre à jour et se supprimer dans la base de données.
Structure de l'Active Record Pattern
- Modèle : Les modèles dans Eloquent représentent des tables de la base de données, et chaque instance d'un modèle représente un enregistrement dans cette table.
- Persistance intégrée : Les modèles contiennent des méthodes comme
save(),update(),delete()pour gérer la persistance. - Logique métier et persistance combinées : Les modèles dans Eloquent contiennent à la fois la logique métier et la persistance.
Exemple concret avec Eloquent (Active Record Pattern)
1. Créer un modèle Eloquent User
Dans Eloquent, un modèle représente à la fois la logique métier et la logique de persistance pour une table de base de données.
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $fillable = ['name', 'email'];
}
2. Création, mise à jour et suppression d'utilisateurs via des commandes CLI
2.1. Créer un utilisateur via une commande
// app/Console/Commands/CreateUserCommand.php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class CreateUserCommand extends Command
{
protected $signature = 'user:create {name} {email}';
protected $description = 'Create a new user';
public function handle()
{
$user = User::create([
'name' => $this->argument('name'),
'email' => $this->argument('email'),
]);
$this->info('User created with ID ' . $user->id);
}
}
2.2. Mettre à jour un utilisateur via une commande
// app/Console/Commands/UpdateUserCommand.php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class UpdateUserCommand extends Command
{
protected $signature = 'user:update {id} {name} {email}';
protected $description = 'Update an existing user';
public function handle()
{
$user = User::find($this->argument('id'));
if (!$user) {
$this->error('User not found.');
return;
}
$user->update([
'name' => $this->argument('name'),
'email' => $this->argument('email'),
]);
$this->info('User updated successfully.');
}
}
2.3. Supprimer un utilisateur via une commande
// app/Console/Commands/DeleteUserCommand.php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class DeleteUserCommand extends Command
{
protected $signature = 'user:delete {id}';
protected $description = 'Delete an existing user';
public function handle()
{
$user = User::find($this->argument('id'));
if (!$user) {
$this->error('User not found.');
return;
}
$user->delete();
$this->info('User deleted successfully.');
}
}
Avantages du Active Record Pattern avec Eloquent
- Simplicité et rapidité : Les modèles Eloquent gèrent directement les opérations CRUD, facilitant ainsi le développement rapide.
- Intégration avec Laravel : Eloquent est profondément intégré à Laravel, ce qui permet une manipulation fluide de la base de données.
- Moins de code : Eloquent permet de réaliser beaucoup d'opérations avec très peu de code.
Inconvénients du Active Record Pattern avec Eloquent
- Couplage des responsabilités : Le modèle contient à la fois la logique métier et la persistance, ce qui complique la séparation des responsabilités.
- Difficile à tester indépendamment : Les objets Eloquent sont plus difficiles à tester sans une base de données connectée.
Comparaison entre Active Record et Data Mapper
| Caractéristique | Eloquent (Active Record) | Doctrine (Data Mapper) |
|---|---|---|
| Pattern | Active Record | Data Mapper |
| Responsabilité de la persistance | Les modèles contiennent à la fois la logique métier et la persistance | La persistance est gérée par un gestionnaire séparé (Entity Manager) |
| Couplage | Fort couplage entre la logique métier et la base de données | Découplage total entre la logique métier et la persistance |
| Flexibilité | Conventions strictes et faciles à suivre | Très flexible, gère des schémas complexes |
| Testabilité | Plus difficile à tester sans base de données | Facile à tester indépendamment de la base de données |
Conclusion
Le Active Record Pattern, utilisé par Eloquent, est idéal pour des projets simples à moyens où l'accent est mis sur la rapidité de développement. En revanche, le Data Mapper Pattern (Doctrine) est mieux adapté à des architectures complexes nécessitant un découplage stricte entre la logique métier et la persistance. Le choix dépend des besoins spécifiques de ton projet en termes de performance, de flexibilité et de maintenabilité.