Utilisation d'un UUID comme clé primaire dans les modèles Django (impact des relations génériques)

91

Pour un certain nombre de raisons ^, j'aimerais utiliser un UUID comme clé primaire dans certains de mes modèles Django. Si je le fais, est-ce que je pourrai toujours utiliser des applications extérieures comme "contrib.comments", "django -oting" ou "django-tagging" qui utilisent des relations génériques via ContentType?

En utilisant "django-vote" comme exemple, le modèle Vote ressemble à ceci:

class Vote(models.Model):
    user         = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id    = models.PositiveIntegerField()
    object       = generic.GenericForeignKey('content_type', 'object_id')
    vote         = models.SmallIntegerField(choices=SCORES)

Cette application semble supposer que la clé primaire du modèle soumis au vote est un entier.

L'application de commentaires intégrée semble être capable de gérer les PK non entiers, cependant:

class BaseCommentAbstractModel(models.Model):
    content_type   = models.ForeignKey(ContentType,
            verbose_name=_('content type'),
            related_name="content_type_set_for_%(class)s")
    object_pk      = models.TextField(_('object ID'))
    content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")

Ce problème «supposé PK entier» est-il une situation courante pour les applications tierces qui rendraient l'utilisation des UUID pénibles? Ou, peut-être, ai-je mal interprété cette situation?

Existe-t-il un moyen d'utiliser les UUID comme clés primaires dans Django sans causer trop de problèmes?


^ Certaines des raisons: masquer le nombre d'objets, empêcher l'exploration d'url "id crawling", utiliser plusieurs serveurs pour créer des objets non conflictuels, ...

mitchf
la source

Réponses:

56

Une clé primaire UUID posera des problèmes non seulement avec les relations génériques, mais aussi avec l'efficacité en général: chaque clé étrangère coûtera beaucoup plus cher - à la fois à stocker et à joindre - qu'un mot machine.

Cependant, rien n'exige que l'UUID soit la clé primaire: faites-en simplement une clé secondaire , en complétant votre modèle avec un champ uuid avec unique=True. Utilisez la clé primaire implicite comme d'habitude (interne à votre système) et utilisez l'UUID comme identifiant externe.

Pi Delport
la source
16
Joe Holloway, pas besoin de cela: vous pouvez simplement fournir la fonction de génération d'UUID comme champ default.
Pi Delport
4
Joe: J'utilise django_extensions.db.fields.UUIDField pour créer mes UUID dans mon modèle. C'est simple, je viens de définir mon champ comme ceci: user_uuid = UUIDField ()
mitchf
3
@MatthewSchinckel: Lorsque vous utilisez django_extensions.db.fields.UUIDFieldcomme mentionné par mitchf, vous n'aurez aucun problème avec les migrations Django-Sud - le champ mentionné par lui a un support intégré pour les migrations Sud.
Tadeck
126
Terrible réponse. Postgres a des UUID natifs (128 bits) qui ne sont que 2 mots sur une machine 64 bits, donc ne seraient pas "beaucoup plus chers" que les INT 64 bits natifs.
postfuturiste
8
Piet, étant donné qu'il a un index btree dessus, combien de comparaisons y aura-t-il sur une requête donnée? Pas beaucoup. De plus, je suis sûr que l'appel memcmp sera aligné et optimisé sur la plupart des systèmes d'exploitation. Sur la base de la nature des questions, je dirais que ne pas utiliser UUID en raison de différences de performances possibles (probablement négligeables) est une mauvaise optimisation.
postfuturiste
219

Comme vu dans la documentation , à partir de Django 1.8, il existe un champ UUID intégré. Les différences de performances lors de l'utilisation d'un UUID par rapport à un entier sont négligeables.

import uuid
from django.db import models

class MyUUIDModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

Vous pouvez également consulter cette réponse pour plus d'informations.

keithhackbarth
la source
@Keithhackbarth Comment configurer django pour qu'il l'utilise à chaque fois lors de la création automatique d'ID pour les tables?
anon58192932
3
@ anon58192932 Pas vraiment clair ce que vous entendez exactement par "à chaque fois". Si vous souhaitez que les UUID soient utilisés pour chaque modèle, créez votre propre modèle de base abstrait et utilisez-le à la place de django.models.Model.
Назар Топольський
4
Les différences de performances ne sont négligeables que lorsque la base de données sous-jacente prend en charge le type UUID. Django utilise toujours un charfield pour la plupart des bases de données (postgresql est la seule base de données documentée à prendre en charge le champ UUID).
NirIzr
Je ne comprends pas pourquoi c'est une réponse populaire ... La question portait sur la difficulté avec les packages tiers. Bien que Django supporte nativement l'UUID, il semble toujours y avoir un certain nombre de packages qui ne tiennent pas compte des UUID. D'après mon expérience, c'est une douleur.
ambe5960 le
12

Je suis tombé sur une situation similaire et j'ai découvert dans la documentation officielle de Django , que le object_idne doit pas nécessairement être du même type que le primary_key du modèle associé. Par exemple, si vous souhaitez que votre relation générique soit valide pour les identifiants IntegerField et CharField , définissez simplement votre object_idcomme CharField . Étant donné que les entiers peuvent être convertis en chaînes, tout ira bien. Il en va de même pour UUIDField .

Exemple:

class Vote(models.Model):
    user         = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id    = models.CharField(max_length=50) # <<-- This line was modified 
    object       = generic.GenericForeignKey('content_type', 'object_id')
    vote         = models.SmallIntegerField(choices=SCORES)
Jordi
la source
4

Le vrai problème avec l'UUID en tant que PK est la fragmentation du disque et la dégradation des insertions associées aux identifiants non numériques. Étant donné que le PK est un index clusterisé, lorsqu'il n'est pas auto-incrémenté, votre moteur de base de données devra recourir à votre lecteur physique lors de l'insertion d'une ligne avec un identifiant d'ordinal inférieur, ce qui se produira tout le temps avec les UUID. Lorsque vous obtenez beaucoup de données dans votre base de données, cela peut prendre plusieurs secondes, voire quelques minutes, simplement pour insérer un nouvel enregistrement. Et votre disque finira par devenir fragmenté, ce qui nécessitera une défragmentation périodique du disque. Tout cela est vraiment mauvais.

Pour résoudre ces problèmes, j'ai récemment proposé l'architecture suivante qui, à mon avis, mériterait d'être partagée.

La pseudo-clé primaire UUID

Cette méthode vous permet de tirer parti des avantages d'un UUID en tant que clé primaire (à l'aide d'un UUID d'index unique), tout en conservant une PK auto-incrémentée pour résoudre la fragmentation et insérer les problèmes de dégradation des performances liés à une PK non numérique.

Comment ça fonctionne:

  1. Créez une clé primaire auto-incrémentée appelée pkidsur vos modèles de base de données.
  2. Ajoutez un idchamp UUID indexé unique pour vous permettre de rechercher par un identifiant UUID, au lieu d'une clé primaire numérique.
  3. Pointez le ForeignKey sur l'UUID (en utilisant to_field='id') pour permettre à vos clés étrangères de représenter correctement le Pseudo-PK au lieu de l'ID numérique.

Essentiellement, vous effectuerez les opérations suivantes:

Tout d'abord, créez un modèle de base Django abstrait

class UUIDModel(models.Model):
    pkid = models.BigAutoField(primary_key=True, editable=False)
    id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        abstract = True

Assurez-vous d'étendre le modèle de base au lieu des modèles.

class Site(UUIDModel):
    name = models.CharField(max_length=255)

Assurez-vous également que vos ForeignKeys pointent vers l'UUID id champ au lieu du champ auto-incrémenté pkid:

class Page(UUIDModel):
    site = models.ForeignKey(Site, to_field='id', on_delete=models.CASCADE)

Si vous utilisez Django Rest Framework (DRF), assurez-vous de créer également une classe Base ViewSet pour définir le champ de recherche par défaut:

class UUIDModelViewSet(viewsets.ModelViewSet):
    lookup_field = 'id' 

Et étendez cela au lieu du ModelViewSet de base pour vos vues API:

class SiteViewSet(UUIDModelViewSet):
    model = Site

class PageViewSet(UUIDModelViewSet):
    model = Page

Plus de notes sur le pourquoi et le comment dans cet article: https://www.stevenmoseley.com/blog/uuid-primary-keys-django-rest-framework-2-steps

Steven Moseley
la source
0

cela peut être fait en utilisant un modèle abstrait de base personnalisé, en utilisant les étapes suivantes.

Créez d'abord un dossier dans votre projet, appelez-le basemodel, puis ajoutez un abstractmodelbase.py avec ce qui suit ci-dessous:

from django.db import models
import uuid


class BaseAbstractModel(models.Model):

    """
     This model defines base models that implements common fields like:
     created_at
     updated_at
     is_deleted
    """
    id=models.UUIDField(primary_key=True, ,unique=True,default=uuid.uuid4, editable=False)
    created_at=models.DateTimeField(auto_now_add=True,editable=False)
    updated_at=models.DateTimeField(auto_now=True,editable=False)
    is_deleted=models.BooleanField(default=False)

    def soft_delete(self):
        """soft  delete a model instance"""
        self.is_deleted=True
        self.save()

    class Meta:
        abstract=True
        ordering=['-created_at']

deuxième: dans tout votre fichier de modèle pour chaque application, faites ceci

from django.db import models
from basemodel import BaseAbstractModel
import uuid

# Create your models here.

class Incident(BaseAbstractModel):

    """ Incident model  """

    place = models.CharField(max_length=50,blank=False, null=False)
    personal_number = models.CharField(max_length=12,blank=False, null=False)
    description = models.TextField(max_length=500,blank=False, null=False)
    action = models.TextField(max_length=500,blank=True, null=True)
    image = models.ImageField(upload_to='images/',blank=True, null=True)
    incident_date=models.DateTimeField(blank=False, null=False) 

Ainsi, l'incident du modèle ci-dessus est inhérent à tout le champ du modèle abstrait de base.

Fadipe Ayobami
la source
-1

La question peut être reformulée comme suit: "existe-t-il un moyen pour que Django utilise un UUID pour tous les identifiants de base de données dans toutes les tables au lieu d'un entier auto-incrémenté?".

Bien sûr, je peux faire:

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

dans toutes mes tables, mais je ne trouve pas de moyen de le faire pour:

  1. Modules tiers
  2. Django a généré des tables ManyToMany

Donc, cela semble être une fonctionnalité Django manquante.

EMS
la source