Migrer nos clés primaires d’id à uuid, pourquoi et comment ?

Lorsqu’on gère des données au sein d’une base de données, il est nécessaire de les identifier de manière unique. C’est le rôle de la clé primaire. L’implémentation la plus couramment utilisé est l’entier auto-incrémenté. Une autre possibilité est un Universally Unique IDentifier (UUID) conçu de manière à être unique dans le monde.

Un Universally Unique IDentifier

Celui-ci est codé sur 128 bits et se présente sous la forme de groupes de caractères hexadécimaux en minuscules séparés par des tirets : 110e8400-e29b-11d4-a716–446655440000. Depuis la version 4, les UUIDs sont générés de façon aléatoire (avec un risque négligeable de confit). L’intérêt est alors que nous ne pouvons pas les compter, les comparer, ou savoir combien de valeurs sont présentes dans chaque table. À l’inverse de l’auto-incrémentation, nous n’avons pas non plus besoin de communiquer avec la base de données pour le créer, puisque l’uuid est généré avant d’être persisté.

Dans notre cas, nous développons une API pour permettre la transparence des données des projets participatifs de manière pseudo-anonymisée. Nous devons donc permettre une exploitation des données sans exposer l’identité réelle d’un participant (nom d’utilisateur, email, …) mais uniquement un identifiant. De plus, certains projets peuvent être privés durant la période de dépôt, en analysant la clé primaire de nos propositions, il était possible de savoir combien de personnes avaient proposé un projet avant nous… C’est avec ces contraintes que nous avons choisi d’utiliser des UUIDs, plutôt que d’exposer une clé primaire auto-incrémentée.

S’il existe de nombreux articles (ici ou ) expliquant l’intérêt des UUIDs sur les IDs de type entier, peu expliquent comment migrer de l’un à l’autre, c’est ce que nous allons voir dans la partie qui suit !

Comment migrer ?

Nous prendrons pour exemple nos technologies (MySQL, PHP) ainsi que l’ORM Doctrine.

Générer un UUID est simple, Doctrine nous fournit la classe UuidGenerator. En PHP, nous pouvons utiliser la librairie uuid.

Concrètement, nous allons prendre l’exemple des entités suivantes :

type User { id: Int } type Proposition { id: Int author_id: Int } type Vote { id: Int proposition_id: Int author_id: Int }

Nous ne pouvons modifier directement les valeurs des clés primaires des éléments de la table User sans modifier les clés étrangères qui leur sont liées. Avec MySQL, vous pouvez identifier ces clés étrangères en effectuant une requête sur la base de données information_schema , en indiquant la BDD concernée (ici, symfony):

[information_schema]> select TABLE_NAME, COLUMN_NAME from KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 'user' AND REFERENCED_COLUMN_NAME = 'id' AND TABLE_SCHEMA = 'symfony';

Nous avons listé toutes les informations nécessaires, modélisons comment migrer d’id à uuid :

1ère étape : Ajouter un champ uuid, sur notre table principale, puis dupliquer les champs des clés étrangères pour leur ajouter une version uuid temporaire ;

2ème étape : Ajouter les uuids dans la table que nous mettons à jour et stocker la correspondance entre l’id et l’uuid dans un objet de type Map(id => uuid) ;

3ème étape : Ajouter les UUIDs dans les champs de clés étrangères à l’aide de la map qui fait correspondance ;

4ème étape : Supprimer les clés étrangères qui pointent vers id ;

5ème étape : Renommer les nouvelles clés étrangères à leur nom d’origine (author_uuid -> author_id) ;

6ème étape : Supprimer la clé primaire id de type entier pour la remplacer par id de type uuid. Nous n’avons plus de clés primaires qui référence id donc nous pouvons supprimer la clé primaire avant de mettre la nouvelle ;

7ème étape : Restaurer les contraintes et index.

Et voilà ! Mais ce n’est l’explication que du cas le plus simple…

Il est nécessaire de gérer les cas de type clé primaire lexicale:

Pour la table vote, nous pouvons nous passer du champ id et prendre comme clé primaire (proposition_id, author_id) puisqu’un utilisateur ne peut voter qu’une fois par proposition.

type Vote { proposition_id: Int author_id: Int }

Ou encore, le cas d’une table many to many:

type Friend { user_a_id: Int user_b_id: Int }

Migrer facilement

Nous avons eu à résoudre tous ces cas lors de la migration de nos tables. Voilà pourquoi nous avons choisi de partager avec vous notre librairie id-to-uuid. Celle-ci implémente directement le raisonnement vu plus haut !

Pas besoin de lister vous même les clés étrangères, la classe est capable de le faire à votre place. Concrètement, migrer la table user revient à créer la migration suivante :

Ainsi, vous pouvez intégrer à vos migrations Doctrine classiques le passage d’id à uuid. Attention tout de même, la migration peut provoquer un downtime conséquent selon la taille de vos tables (1 heure chez nous pour la table la plus conséquente) ! Testez bien la migration avant de l’appliquer en production ;-)

Attention également à vérifier que des clés étrangères sont bien utilisées dans toutes vos tables… Sinon le lien sera perdu !

Et si nous évitions de migrer ?

Il est possible de conserver une clé primaire de type entier auto-incrémenté et de générer un uuid supplémentaire. En exposant uniquement l’uuid plutôt que la clé primaire, vous bénéficiez d’une partie des avantages des UUIDs mais ajoutez de la complexité dans votre code.

Nous n’avons pas choisi cette solution mais tenions à vous en faire part !

Originally published at developers.cap-collectif.com.