Le problème des IDs séquentiels
Quand on crée une application Rails classique, chaque enregistrement reçoit un identifiant numérique auto-incrémenté : /users/1, /users/2, /users/3… C'est simple, lisible, et ça fonctionne très bien dans la plupart des cas.
Mais cette simplicité a un coût. Avec des IDs séquentiels, on expose involontairement plusieurs informations : le volume approximatif de données dans la table, la possibilité d'énumérer les ressources (vulnérabilité IDOR — Insecure Direct Object Reference), et parfois des informations métier sensibles. Un concurrent pourrait par exemple déduire votre nombre de clients simplement en regardant l'URL.
Un UUID comme 550e8400-e29b-41d4-a716-446655440000 ne révèle rien de tout cela.
Les avantages des UUID
Sécurité et opacité — C'est l'argument principal. Les endpoints exposés dans des URLs ou des webhooks ne permettent plus le scraping séquentiel. Impossible de deviner l'identifiant de la ressource suivante.
Génération côté client — On peut créer l'identifiant avant l'insertion en base. C'est particulièrement utile pour du offline-first, des systèmes distribués, ou du pré-assignement dans des formulaires multi-étapes.
Fusion de bases facilitée — Pas de collision d'IDs entre environnements ou entre tenants. C'est très pertinent dans un contexte SaaS multi-tenant où l'on pourrait avoir besoin de fusionner des données.
Sécurité des APIs — Les identifiants exposés dans les APIs REST ou GraphQL ne donnent aucune prise pour de l'énumération.
UUIDv7 : le meilleur des deux mondes
Le reproche historique fait aux UUID v4, c'est leur caractère aléatoire. Les insertions fragmentent l'index B-tree de la clé primaire, provoquant des page splits et des cache misses sur les tables à fort volume d'écriture.
UUIDv7, supporté nativement depuis Rails 7.1 et PostgreSQL, résout ce problème grâce à un préfixe temporel. Les UUIDs sont désormais triés chronologiquement, ce qui élimine la fragmentation d'index tout en conservant tous les avantages d'opacité.
Les inconvénients à connaître
Taille en stockage — Un UUID occupe 16 octets contre 4 (integer) ou 8 (bigint). Cette différence se multiplie sur chaque foreign key, chaque index, chaque jointure. Sur des tables avec beaucoup de relations, l'empreinte mémoire et disque augmente significativement.
Lisibilité dégradée — En debug, dans les logs, en console Rails… User.find("550e8400-e29b-41d4-a716-446655440000") est nettement moins pratique que User.find(42). Même chose dans les URLs et lors du support.
Jointures plus lentes — Comparer des UUIDs est plus coûteux que comparer des entiers, surtout sur de grosses jointures. En pratique avec PostgreSQL et le type natif uuid, la différence reste souvent acceptable, mais elle existe.
Migration complexe — Si vous souhaitez migrer une application existante d'integer à UUID, c'est une opération lourde : toutes les foreign keys à mettre à jour, les associations polymorphiques à revoir, les dépendances externes à adapter.
Mise en œuvre dans Rails
Avec PostgreSQL, l'activation est simple. Dans la configuration des générateurs :
config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
endDans une migration :
create_table :users, id: :uuid do |t|
t.string :name
t.timestamps
endPour activer UUIDv7 nativement depuis Rails 7.1 :
# config/initializers/generators.rb
Rails.application.config.active_record.generate_uuids_version = 7Verdict
Pour un nouveau projet, UUIDv7 + PostgreSQL est aujourd'hui un choix solide et quasi sans compromis. On conserve le tri chronologique, on élimine les problèmes de fragmentation d'index, et on gagne tous les avantages de sécurité. Le surcoût en stockage est rarement un facteur bloquant sur des applications métier.
En revanche, migrer un existant uniquement pour passer aux UUID mérite une réflexion au cas par cas : le gain doit justifier la complexité de la migration.