Utiliser des UUID comme primary key avec l’ORM de Django 5


Par défaut Django ajoute automatiquement un champ id à tous les modèles, et le configure pour être un entier qui s’auto incrémente puis le désigne comme la clé primaire. Il est néanmoins possible d’utiliser un autre champ comme clé primaire pour sa table: un slug ou un identifiant métier. Dans notre cas, on va voir comment utiliser un UUID.

Un UUID est un identifiant généré de manière pseudo aléatoire qui a une forte probabilité d’être unique dans le monde entier, il y a 4×10³⁷ combinaisons possibles pour les versions des algos récents. Selon le paradoxe des anniversaires, il faudrait créer un milliard de UUID par seconde pendant 100 ans pour que le prochain ait 50% de chance d’être un doublon. Normalement pour votre site de fans club des limules hermaphrodites, ça devrait être suffisant.

Utiliser des UUID possède de nombreux avantages car ils sont plus ou moins garantis d’être uniques, et ainsi:

  • Vos serveurs peuvent les générer indépendemment les uns des autres.
  • ./manage.py loadata ne va pas vous crasher à la gueule à cause d’une duplicate key.
  • On peut faire du sharding sur la clé très facilement.
  • Faire des réplications et des synchronisations est beaucoup plus simple qu’avec un AUTO INT.
  • Votre modèle n’a pas besoin d’un champ significatif pour être unique.
  • Vous pouvez générer l’ID côté client et l’envoyer au serveur.
  • Faire communiquer des systèmes complètement séparés, différents ou par API interposée ne pose aucun problème de référence.

Pour toutes ces raisons, les bases de données NoSQL (CouchDB, MongoDB, etc) utilisent depuis longtemps les UUID comme clés primaires par défaut. Le moteur de base de données de Google, Big Table, utilise des UUID.

Pourtant les UUID ne sont pas exempts de défauts:

  • Les JOINS sont beaucoup plus lents.
  • Il prennent plus d’espace disque et en mémoire.
  • On ne peut pas les mettre dans des URL sans que ce soit très très moche.
  • Ils sont difficiles à retenir, dicter et rechercher manuellement.
  • Ils sont non significatifs. Regardez un UUID ne vous donne aucune info sur les données sous-jacentes.
  • Ils ne sont pas ordonnés. Cela a un impact sur la manipulation des données, et sur certains moteurs de BDD à l’insertion.

Les UUID ne sont donc pas la solution miracle à tous les soucis, mais ils sont tout de même mon choix par défaut en ce moment ne serait-ce que pour les fixtures. Je gagne les performances sur le caching et le parallélisme, et un peu de lenteur sur les JOINS est quelque chose que je peux supporter. Pour les gros sites, les JOINS sont de tout façon votre ennemi juré et on finit toujours par tweaker à grand coup de dé-normalisation.

On peut faire passer ses modèles Django aux UUID en faisant:

from uuid import uuid4
 
class MotDElle(object):
    ...
    id = models.CharField(max_length=36, primary_key=True,
                          default=lambda: str(uuid4()), editable=False)

Ou, si vous utilisez django_extensions (et vous devriez, ne serait-ce que pour shell_plus et runserver_plus):

from django_extensions.db.fields import UUIDField
 
class MotDElle(object):
    ...
    id = UUIDField(primary_key=True)

Ce qui a l’avantage de caster la valeur en un objet UUID, et non une bête string, à la lecture.

Malheureusement, pour le moment il n’y a aucune implémentation officielle de Django pour utiliser les UUID. Cela a des conséquences fort ennuyeuses:

  • Les implémentations actuelles utilisent un VARCHAR pour stocker l’UUID au lieu d’un type natif quand c’est possible, et donc ce n’est pas du tout optimisé. L’exception étant django-pdfield, mais dans ce cas, adieu la compatibilité entre plusieurs SGBD.
  • django.contrib.auth.models.User n’a aucun moyen d’utiliser proprement les UUID (il ne vous reste que le monkey patching). Cela pourrait changer avec Django 1.5 qui prévoit un modèle User extensible. \o/

Un ticket pendouille depuis 5 ans sur la question. Je vous invite d’ailleurs à le spammer jusqu’à ce que réouverture s’en suive. Et le premier qui me dit “t’as qu’à l’implémenter, toi”, je lui retourne un tampon dans la poire.


You can use Github Flabored Markdown to format your comments.

Leave a comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

5 thoughts on “Utiliser des UUID comme primary key avec l’ORM de Django

  • Sam Post author

    Au passage, je précise qu’aucun ID n’est garanti à 100% d’être unique. Le un AUTO INT (qui ne l’est pas d’un serveur à l’autre), ni même truc généré comme le sacro saint numéro de sécurité social. On a vu des cas de duplications avec des conséquences marrantes sur des réclamations de dette :-D

  • Sam Post author

    Un autre point que j’ai manqué de mentionné dans l’article que j’ai lu en comment sur coding horror:

    “you can generate any number of guids at once, in parallel, but you can only generate one ‘autonumber’ at once (they have to be queued up). Hence, in a highly concurrent application, guids avoid one major bottleneck.”

    Donc dans un algo massivement parallele, le UUID wins.

  • Hobbestigrou

    Salut,

    Il est vrai que dans certain cas, il est vraiment très pratique d’utiliser un uuid pour la clé primaire, on s’assure de l’unicité des clés. Autrement dans le première exemple tu importe uuid4, tu as donc pas besoin de faire uuid.uuid4() et faire directement uuid4(). Aussi je te conseil d’utiliser django-uuidfield qui est développé par david cramer qui est donc de bonne facture. Le code bien que très proche de UUIDField est plus propre, car il ne fait pas un Charfield mais un type uuid.