Skywalker13

Diary of an ex-GeeXboX developer…

Les SmartId ~ 🇫🇷

Posted at — Dec 13, 2024

Il y a une bonne dizaine d’annĂ©es, un de mes amis et collègue, Samuel, a imaginĂ© une reprĂ©sentation pour les identifiants d’entitĂ©s et de services, qui sont simples, et particulièrement pratiques. Je vais vous les prĂ©senter ici mais tout d’abord, parlons un peu de modĂ©lisation.

La bonne vieille modélisation de grand-papa

Il existe de nombreuses thĂ©ories mais souvent elles se ressemblent toutes. On a Merise de nos amis français, on a l’UML ou encore les diagrammes ER, etc. Dans le fond, c’est toujours la mĂŞme idĂ©e. On crĂ©e des relations entre des entitĂ©s et on se pose la question suivante :

Est-ce que j’ai une relation 1-1 ou n-n (1-n, n-1, 0-n, etc.) ?

En français :

Est-ce que mon entitĂ© doit avoir un lien avec une seule entitĂ© Ă©trangère ou Ă  une collection d’entitĂ©s ?

Selon les outils, les langages, etc., il y a différentes façons de le faire mais contentons-nous de quelques diagrammes très simples qui permettent de résumer très rapidement de quoi on parle.


1-1

Une clef étrangère permet de référencer B depuis A.


n-n

Une table d’allocation permet de relier les A avec les B.


J’utilise le terme “table d’allocation” qui vient de Merise. Mais on peut aussi parler de table de relations, de liens, de correspondances, ou que sais-je. Ce n’est pas très important ici. Je ne sais pas pour vous, mais c’est ce genre de mĂ©thode que j’ai appris, et je les ai appliquĂ© un certain temps avec gĂ©nĂ©ralement des bases de donnĂ©es SQL.

Cette manière de faire a ses mĂ©rites comme pouvoir poser des contraintes d’intĂ©gritĂ© rĂ©fĂ©rentielles par exemple. Mais… pour autant que tout soit gĂ©rĂ© dans la mĂŞme base de donnĂ©es.

Et pourtant, comme c’est pĂ©nible de gĂ©rer des migrations de modèles (schĂ©mas). Personne ne fait le modèle parfaitement adaptĂ© du premier coup. Vaut mieux limiter autant que possible les modifications.

Le NoSQL

J’aime beaucoup cette manière de faire. Au lieu d’avoir un modèle rigide, on a un modèle beaucoup plus simple au niveau du schĂ©ma de la base de donnĂ©es. Grosso modo, on veut simplement retrouver des documents en fonction d’identifiants. Les modèles de ces documents ne sont pas forcĂ©ment connus par le moteur de la base de donnĂ©es. Les migrations se font au niveau des documents et pas au niveau du schĂ©ma de la base.

Au premier abord, on pourrait penser que c’est un pas en arrière par rapport Ă  la manière plus traditionnelle… et pourtant.

Rester intègre

Imaginons un document A qui rĂ©fĂ©rence un document B. On a alors une relation de type 1-1. Mais au lieu d’avoir des tables SQL, on a simplement deux documents dans une table unique. Le document A contient un champ (une clef Ă©trangère) avec l’id du document B.

Si j’efface le document B, je casse la relation et le moteur de la base de donnĂ©es ne peut pas le voir. Dans ce scĂ©nario, il y a un problème majeur. Celui-ci peut sembler surprenant pour certaines personnes mais le problème, c’est tout simplement le fait d’effacer un document. Il ne faut pas les effacer. Ce n’est pas plus compliquĂ© que ça. Il n’est donc pas possible de casser une rĂ©fĂ©rence. Facile, non ?

Hein ? Ne jamais rien effacer ? HĂ© mec, c’est n’importe quoi…

Du calme, nous ne sommes plus en 1990. L’espace de stockage n’est pas un problème, ni les performances d’ailleurs. Il faut repenser l’effacement. Dans notre univers, on doit suivre la flèche du temps toujours dans le mĂŞme sens. Ce qui veut dire qu’effacer un document revient Ă  ne plus le prĂ©senter car finalement, dans le passĂ©, il a existĂ©. Ainsi on le fait simplement en lui donnant un statut. Par exemple, “trashed”. En lui donnant ce statut, on dit qu’on arrĂŞte sa flèche du temps. De cette manière on pourrait mĂŞme le ressusciter un jour.

Le temps

Quand on m’a appris la modĂ©lisation de base de donnĂ©es, on ne m’a jamais rien appris sur le temps. Et pourtant, c’est fondamental.

Comment gĂ©rer le temps avec une modĂ©lisation classique Ă  la Merise, UML, etc. ? Mmm… dur dur… Je ne dis pas que c’est impossible mais vous allez transpirer. Mais en NoSQL, c’est d’une très grande simplicitĂ© et voici pourquoi.

Chaque entitĂ© est un document (par exemple un JSON) avec un identifiant. Ce document, vous le stockez dans une table du type clef/valeur. Très important, la clef ne doit pas avoir de contrainte unique. Chaque mutation de l’entitĂ© doit insĂ©rer une nouvelle entrĂ©e dans cette table. Il est important que l’ordre d’insertion soit garanti. Le temps ? C’est simplement l’axe Y (les lignes). Effacer un document revient alors Ă  insĂ©rer Ă  la suite ce document avec le statut “trashed”.

Effacer, c’est crĂ©er une nouvel Ă©tat du document.

Ce que je vous prĂ©sente ici, c’est une version simplifiĂ©e de ce qu’on a mis en place avec Samuel. Chaque insertion est immuable, on peut uniquement aller de l’avant.

La suppression

Oui, il existe une situation oĂą on peut supprimer d’anciennes entrĂ©es (d’anciens documents). Ce sont des situations très diffĂ©rentes car on va parler ici de maintenance et de garbage collector. Étant donnĂ© qu’on ne fait qu’insĂ©rer du neuf, on peut supprimer de l’ancien sans impacter les utilisateurs. Le scĂ©nario est du genre de ne pas garder toutes les mutations qui existent quand elles sont plus anciennes de 2 mois (par exemple). On peut imaginer garder que le dernier Ă©tat. Et selon les types d’entitĂ©s, on pourrait choisir des conditions diffĂ©rentes. On peut mĂŞme tout garder pour toujours. Tout dĂ©pend des documents concernĂ©s, de l’activitĂ© par type de document, etc. Mais attention, on garde toujours au moins le dernier mĂŞme si son statut est “trashed”. Ă€ moins que votre garbage collector soit capable de garantir qu’il n’y a plus aucun document qui y fait rĂ©fĂ©rence.

Et les SmartId dans tout ça ?

J’avais besoin de passer par une introduction pour que les SmartId prennent du sens. Nous avons donc une base de donnĂ©es qui contient des documents dans une table sous la forme clef/valeur. Un document de mĂŞme ID peut apparaĂ®tre plusieurs fois (axe temporel). Pour faire des rĂ©fĂ©rences entre les documents, on peut :

C’est bien mais parfois c’est ennuyeux. On doit modifier le modèle du document A pour qu’il puisse stocker une rĂ©fĂ©rence au document B. Ce n’est pas toujours souhaitable et ça demande potentiellement de la migration.

Qu’est-ce que le SmartId ?

Pour avoir un SmartId minimal, il faut 2 choses. Il faut un type et il faut un texte ou numĂ©ro unique par type. Imaginons que le document est un message. Nous avons le type “message”. Nous pouvons utiliser un UUID pour la partie unique. Par exemple :

Mais on peut aussi utiliser ce qui nous arrange :

Utiliser un UUID donne une garantie d’unicitĂ©. Utiliser un mot comme “main” permet de diffĂ©rencier une des entitĂ©s. Ce qui veut dire que vous pouvez mĂ©langer diffĂ©rentes sortes d’identifiants tant que vous garantissez qu’il ne peut pas y avoir de conflit. En ayant “main” et des UUID, il est garanti qu’il n’y aura jamais un UUID qui sera par hasard sous la forme de “main”.

Pourquoi le type dans l’id ?

En ayant le type dans l’id, il est possible de stocker diffĂ©rents types de documents dans la mĂŞme base de donnĂ©es et sans conflits. Nous pourrions avoir message@main ainsi que user@main dans la mĂŞme base. On pourrait Ă©viter le type en ayant une base “message” et une base “user”. Ou pas, parce que les SmartId sont bien plus que ça. Chez nous, on fait les deux. Ce que je veux dire, c’est qu’on a des bases de donnĂ©es par domaine et plusieurs types par domaine. Ce que j’ai prĂ©sentĂ© avant, ce sont les SmartId minimaux : type@unique. Mais les SmartId sont composables et voici comment et pourquoi.

type1@type2@unique

Mais qu’est-ce que c’est que ça ?

journal@workitem@unique

Exemple 1

Voici un exemple très courant dans notre système. Nous crĂ©ons un service “journal” qui gère une sorte de journal pour le service “workitem”.

Les SmartId ne sont pas utilisĂ©s que pour les entitĂ©s mais aussi pour les services qui manipulent des entitĂ©s. workitem@unique est un service pour modifier des entitĂ©s. Ce workitem va crĂ©er un service “journal”. Le service “journal” hĂ©rite de l’identifiant du workitem. Ce journal ne peut exister que pour cette instance du workitem. Ici vous pouvez alors deviner une relation forte (uniquement avec l’ID) entre le service journal et le service workitem.

Exemple 2

Un autre exemple avec les entitĂ©s. Imaginons que nous avons un type d’entitĂ© “message” qui reprĂ©sente un message gĂ©nĂ©rique dans le système. Il est possible que ce message existe aussi sous la forme d’un email, mais pas toujours. En modĂ©lisant, on pourrait se dire : « suffit de mettre une rĂ©fĂ©rence du message dans l’entitĂ© email ». On crĂ©e une relation avec le modèle du document. Mais parfois ce n’est pas appropriĂ© de modifier le modèle. Simplement qu’on ne souhaite pas polluer le modèle et encore moins le migrer. Pour cela, on peut compter sur les SmartId. Quand un email est crĂ©Ă© Ă  partir d’un message, on peut alors faire :

Les SmartId nous permettent de créer une dépendance 1-1 sans toucher aux modèles.

Exemple 3

Nous avons utilisé ce principe pour les tags sur les entités. Ici, je vais vous donner un exemple en production. Pour moi (et pas que moi), il est exclu de toucher aux modèles des entités pour les polluer avec des tags.

Mais on nous a demandĂ© d’intĂ©grer les tags bien après que nos modèles soient rĂ©alisĂ©s. Voici comment les SmartId ont rĂ©solu la problĂ©matique. Nous avons des entitĂ©s de type “tag”. Par exemple :

Un tag a de la couleur, un nom, une description, etc.

Nous avons des entitĂ©s de type “entityTags”. Par exemple :

Le contact Ă  comme SmartId contact@unique. Ici on crĂ©er simplement un id de type “entityTags” qui utilise l’id du contact comme identifiant unique. C’est exactement comme avec l’exemple du message et de l’email prĂ©sentĂ© prĂ©cĂ©demment.

“entityTags” contient la liste des liens sur les tags associĂ©s Ă  l’entitĂ© contact@unique. Cette entitĂ© “entityTags” sert de table de relations.

Cette entitĂ© “entityTags” n’est pas indispensable. Mais elle est pratique pour faciliter l’Ă©dition des tags de l’entitĂ© avec notre framework UI.

“entityTags” ne contient pas une collection de tag@ mais une collection de tagLink@ (oui encore un type). Un “tagLink” se prĂ©sente ainsi :

Ce “tagLink” peut contenir des donnĂ©es utilisateur qui sont par exemple introduites au moment de l’application du tag sur le contact. C’est optionnel et très pratique.

Il serait possible de se passer des “entityTags” car les relations entre l’entitĂ© et les tags peuvent ĂŞtre exprimĂ©es uniquement avec les “tagLink”. Le “tagLink” contient tout ce qui est nĂ©cessaire dans son SmartId. On y trouve l’entitĂ© sur laquelle le “tagLink” est liĂ© contact@unique, on y trouve Ă©galement le tag associĂ© tag@supplier. Avec une requĂŞte dans la base de donnĂ©e il serait facile de retrouver tous les tags associĂ©s Ă  une entitĂ© en particulier uniquement en se basant sur les ID. Chez nous on utilise l’“entityTags” car notre framework UI sait bien travailler avec des collections qui existent rĂ©ellement dans une entitĂ©. Avec une requĂŞte sur une liste d’ID, la UI nĂ©cessiterait quelques adaptations supplĂ©mentaires.

Avec ce système basĂ© sur les SmartId, il est alors possible d’assigner des tags Ă  n’importe quelle entitĂ© du système sans toucher au moindre modèle et tout en pouvant assigner des donnĂ©es avec chaque tag posĂ©. Finalement le schĂ©ma se construit avec les SmartId car l’entitĂ© “contact” n’a jamais de rĂ©fĂ©rence sur entityTags@contact@unique dans son modèle.

Les liens avec l’extĂ©rieur

Je souhaite encore vous prĂ©senter un cas d’utilisation très pratique des SmartId. Nous avons des systèmes oĂą nous devons importer rĂ©gulièrement des donnĂ©es externes en plus de gĂ©rer nos propres entitĂ©s. Imaginons des clients (par exemple). Le système crĂ©e des clients sous la forme customer@uuid. Le système externe crĂ©e des clients avec des nombres auto-incrĂ©mentĂ©s. Client 1001, 1002, 1005, 1011, … Un système traditionnel… Quand nous importons depuis ce système, nous pouvons utiliser les SmartId ainsi (par exemple) :

Nous n’avons pas de risque de conflit avec nos propres ID et il est très facile d’effectuer une mise Ă  jour. L’importateur des externes va simplement chercher l’entitĂ© customer@XXXX-extern existante (ou la crĂ©er au besoin). Sans les SmartId, tout devient plus complexe car certainement qu’on devrait conserver l’id extern dans le modèle de l’entitĂ© customer. En plus, la recherche de l’entitĂ© Ă  synchroniser Ă  l’import se complexifie. Beurk… Merci les SmartId.

Si je ne vous ai pas convaincu ce n’est pas très grave car ce n’est pas mon objectif, mais tant mieux si ça peut vous faire rĂ©flĂ©chir. En ce qui me concerne, depuis lors, je n’ai plus aucune envie de faire de la modĂ©lisation Ă  la façon de grand-papa.