php-redis-om : un object mapper pour Redis
Publié le 23 juillet 2024
Redis est un système de base de données NoSQL en mémoire ultra performant et polyvalent, très apprécié pour sa rapidité et sa simplicité d’utilisation. Principalement utilisé comme cache, queuing de messages ou base de données clé-valeur, Redis offre pourtant une flexibilité inégalée pour une multitude d’autres cas d’usage et s’accompagne de nombreux outils qui permettent de l’utiliser de manière beaucoup plus avancée, notamment pour la création d’index et le stockage d’objets complexes en son sein.
Dans l’écosystème PHP, les devs sont habitués à utiliser des ORM (Object-Relational Mappers) comme Doctrine ou Eloquent pour mapper des objets avec des tables relationnelles et des bases de données SQL traditionnelles. En revanche actuellement, il n’existe pas d’équivalent pour Redis écrit en PHP (à l’inverse de Java, Node.js ou Python, qui disposent chacun de librairie de ce genre), obligeant soit les développeurs à écrire du code spécifique pour persister et requêter des objets dans Redis, ou imposant le plus souvent de l’utiliser comme un simple serveur de stockage d’objets sérialisés en JSON. C'est dans ce contexte qu’a démarré le développement de la bibliothèque php-redis-om, une nouvelle librairie qui permet de persister, requêter et transposer des objets directement dans Redis, en exploitant pleinement ses capacités via les formats Hash et JSON.
Persister facilement vos données avec l’ObjectManager #
php-redis-om est donc une bibliothèque PHP qui facilite l'interaction avec Redis en proposant un object mapper largement inspiré de Doctrine. Les devs familiarisés avec l’ORM traditionnel de Symfony (ou son ODM pour MongoDB) retrouveront des méthodes et classes similaires, rendant ainsi la prise en main rapide et intuitive. La classe RedisObjectManager fournie par la bibliothèque implémente notamment les méthodes définies dans l’interface ObjectManager de Doctrine, permettant ainsi de retrouver les mêmes fonctionnalités et d’utiliser php-redis-om de manière quasi-interchangeable avec Doctrine.
Première étape, installer la librairie via Composer :
composer require clementtalleu/php-redis-om
Ensuite, utiliser les attributs PHP fournis par la bibliothèque pour mapper vos classes :
<?php
use Talleu\RedisOm\Om\Mapping as RedisOm;
#[RedisOm\Entity]
class User
{
#[RedisOm\Id]
#[RedisOm\Property]
public int $id;
#[RedisOm\Property(index:true)]
public string $name;
#[RedisOm\Property]
public \DateTimeImmutable $createdAt;
}
- L’attribut Entity permet d’indiquer au bundle que vous souhaitez que cette classe soit considérée comme une entité mappée par l’ObjectManager.
- L’attribut Id est utilisé pour spécifier la propriété utilisée comme identifiant (il en faut un pour chaque objet qu’on va vouloir persister).
- L’attribut Property permet de spécifier quelle propriété sera persistée dans Redis et de lui indiquer un certain nombre d’infos supplémentaires (nom, type, propriété indexée etc.).
Enfin, persister vos données dans votre serveur Redis par un simple persist/flush :
$objectManager = new RedisObjectManager();
$objectManager->persist($user);
$objectManager->flush();
Et voilà 🥳, votre objet est persisté dans votre serveur Redis !
Les formats JSON et HASH dans Redis #
Redis supporte plusieurs types de structures de données, mais les deux formats les plus pertinents pour l'object mapping sont les modèles JSON et Hash.
Le format JSON n’est plus à présenter, il vous permet de stocker des objets complexes sous la forme de chaînes JSON. Idéal pour les cas où vous avez des structures imbriquées et souhaitez conserver la flexibilité de JSON. Cependant, si votre serveur Redis est tout à fait capable de stocker une string JSON comme valeur, il ne supporte pas nativement l’indexation, il n’est donc pas possible de requêter les données stockées.
Pour l’utiliser vous aurez besoin d’installer le module RedisJSON à votre architecture Redis.
Le format HASH vous est peut-être étranger, il s’agit d’un format natif de Redis : Hash vous permet “d’aplatir vos objets”. Il est très performant pour des lectures et écritures partielles car il permet de stocker individuellement chaque champ ou sous-propriété de votre objet sous la forme clé-valeur. C'est particulièrement utile pour des données souvent modifiées ou partiellement lues.
Par exemple, voici comment sera représenté un objet Product dans le datastore Redis :
$product = new Product();
$product->name = 'Super t-shirt';
$product->color = "bleu"
$product->category = new Category(id: 4, name: 't-shirts');
// Au moment de le persister
> HSET product1 name "Super t-shirt" color "bleue"
> HSET product1 category.id 4
> HSET product1 category.name "t-shirts"
// Au moment de récupérer l'objet intégralement
> HGETALL product1
1) "name"
2) "Super t-shirt"
3) "color"
4) "bleue"
5) "category.id"
6) "4"
7) "category.name"
8) "t-shirts"
// Récupérer uniquement un champ
> HGET product1 name
"Super t-shirt"
Les deux formats sont supportés par php-redis-om, et disposent des mêmes méthodes de persistance et de récupération des objets. Les différences de performances sont négligeables, lors d’un benchmark le format JSON est sensiblement plus performant lors d’un nombre importants de persist/flush successifs (+ de 1000).
Data Mapper, Repository pattern et Active Record #
Plusieurs design patterns sont couramment utilisés concernant l'accès aux données : Repository, Data Mapper et Active Record.
Le Data Mapper est un design pattern qui sépare l'interface de l'application et la couche de persistance des données. Il va agir comme un intermédiaire entre nos objets PHP et la base de données Redis, en gérant la conversion des données et en garantissant que les objets n'ont pas besoin de connaître les détails de la persistance. Ici en utilisant des classes de repository pour gérer ces opérations. C'est le modèle choisi notamment par Doctrine et repris ici dans php-redis-om car il offre une grande flexibilité et maintenabilité. En centralisant la logique d'accès aux données, il devient plus facile de tester et de modifier le code sans affecter la logique métier.
L'Active Record quant à lui, intègre la logique d'accès aux données directement dans les objets métiers (comme c’est le cas dans Eloquent, l’ORM par défaut de Laravel). Bien que cela puisse simplifier le code pour des applications simples, il peut rendre la maintenance plus difficile dans des architectures plus complexes. Cette option a été exclue.
Requêter et trier vos données grâce à l’ObjectRepository #
(Pour pouvoir créer des index et ensuite réaliser vos premières requêtes, votre serveur Redis doit s’être précédemment vu ajouter le module Redisearch, par défaut ces modules sont disponibles dans la redis-stack complète et dans le Docker fourni par la bibliothèque, pour l’ajouter en production, je vous renvoie à cette documentation).
Une fois vos objets enregistrés dans le datastore Redis, vous chercherez naturellement à les récupérer. Pour ce faire, php-redis-om propose d'instancier un repository pour chacune de vos classes et de disposer d’une suite de méthodes pour filtrer et requêter vos ressources.
use Talleu\RedisOm\Om\RedisObjectManager;
// Récupérez votre repository
$userRepository = $objectManager->getRepository(User::class);
// Bénéficiez des différentes méthodes fournies
$userRepository->findAll();
$userRepository->findOneBy(['name' => 'John Doe']);
$userRepository->findBy(['name' => 'John'], ['age' => 'ASC'], 5);
Les utilisateurs de Doctrine sont ici comme à la maison, ces méthodes portent encore une fois les mêmes noms avec les mêmes arguments. Je vous renvoie à la documentation pour une liste plus exhaustive des fonctions disponibles.
Les repositories permettent également d'instancier un QueryBuilder afin d’écrire des requêtes plus complexes telles que vous les feriez directement en ligne de commande dans redis-cli.
Performances #
Les performances sont un aspect crucial pour toute bibliothèque de gestion de données. Plusieurs benchmarks ont été effectués afin d’évaluer les performances de php-redis-om par rapport à Doctrine et à une base de données relationnelles PostgreSQL.
Sans trop de surprise, les résultats montrent que Redis offre de bonnes performances pour des opérations de lecture et d'écriture, surtout lorsque le nombre des opérations augmente.
Avec le format HASH :
![Performances avec HASH avec la librairie php-redis-om" class="wp-image-9579" style="aspect-ratio:3/2;object-fit:cover](/wp-content/uploads/2024/07/performances-hash-1024x642.webp)
Avec le format JSON :
![Performances avec JSON avec la librairie php-redis-om" class="wp-image-9580](/wp-content/uploads/2024/07/performances-JSON-1024x608.webp)
La consommation de mémoire est également plus faible mais les différences sont bien moins notables. Les chiffres détaillés des tests effectués avec phpbench.
Utilisation de différents clients Redis #
Bien que php-redis-om utilise par défaut l’extension PHP ext-redis, plus rapide que la bibliothèque Predis, il est possible d'utiliser d'autres clients Redis, en implémentant dans votre classe l’interface fournie : RedisClientInterface. Cette flexibilité permet de choisir le client qui répond le mieux à vos besoins et à contraintes spécifiques.
À terme, les deux implémentations devraient être disponibles directement dans la librairie.
État actuel de la librairie php-redis-om #
Une intégration Docker est fournie pour vos développements, elle dispose des modules nécessaires (RedisJson et RediSearch) et profite des hautes performances fournies par FrankenPHP.
php-redis-om est actuellement en version 0.2.*, ce qui signifie qu'elle est utilisable mais pour le moment encore en phase de développement. Initialement développée pour un besoin ponctuel, la librairie est ouverte aux retours et aux contributions de la communauté. Vous pouvez signaler des issues, proposer des améliorations ou contribuer au code sur le dépôt GitHub.