Usar un UUID como clave principal en los modelos de Django (impacto de relaciones genéricas)

91

Por varias razones ^, me gustaría usar un UUID como clave principal en algunos de mis modelos de Django. Si lo hago, ¿podré seguir usando aplicaciones externas como "contrib.comments", "django-vote" o "django-tagging" que usan relaciones genéricas a través de ContentType?

Usando "django-vote" como ejemplo, el modelo Vote se ve así:

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)

Esta aplicación parece asumir que la clave principal del modelo que se vota es un número entero.

Sin embargo, la aplicación de comentarios incorporada parece ser capaz de manejar PK que no son enteros:

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")

¿Es este problema de "PK entero asumido" una situación común para aplicaciones de terceros que haría que el uso de UUID sea un problema? O, posiblemente, ¿estoy malinterpretando esta situación?

¿Hay alguna forma de usar UUID como claves primarias en Django sin causar demasiados problemas?


^ Algunas de las razones: ocultar recuentos de objetos, evitar el "rastreo de id" de la URL, utilizar varios servidores para crear objetos no conflictivos, ...

mitchf
fuente

Respuestas:

56

Una clave primaria UUID causará problemas no solo con las relaciones genéricas, sino con la eficiencia en general: cada clave externa será significativamente más cara, tanto para almacenar como para unir, que una palabra de máquina.

Sin embargo, nada requiere que el UUID sea la clave principal: simplemente conviértalo en una clave secundaria , complementando su modelo con un campo uuid con unique=True. Use la clave primaria implícita como de costumbre (interna a su sistema) y use el UUID como su identificador externo.

Pi Delport
fuente
16
Joe Holloway, no es necesario: simplemente puede proporcionar la función de generación de UUID como la del campo default.
Pi Delport
4
Joe: uso django_extensions.db.fields.UUIDField para crear mis UUID en mi modelo. Es simple, solo defino mi campo así: user_uuid = UUIDField ()
mitchf
3
@MatthewSchinckel: Cuando use django_extensions.db.fields.UUIDFieldcomo lo menciona mitchf, no tendrá problemas con las migraciones Django-South; el campo mencionado por él tiene soporte incorporado para las migraciones South.
Tadeck
126
Terrible respuesta. Postgres tiene UUID nativos (128 bits) que son solo 2 palabras en una máquina de 64 bits, por lo que no sería "significativamente más caro" que el INT nativo de 64 bits.
posfuturista
8
Piet, dado que tiene un índice btree, ¿cuántas comparaciones habrá en una consulta determinada? No muchos. Además, estoy seguro de que la llamada a memcmp se alineará y optimizará en la mayoría de los sistemas operativos. Según la naturaleza de las preguntas, yo diría que no usar UUID debido a posibles diferencias de rendimiento (probablemente insignificantes) es una optimización incorrecta.
postfuturista
219

Como se ve en la documentación , desde Django 1.8 hay un campo UUID integrado. Las diferencias de rendimiento cuando se usa un UUID frente a un número entero son insignificantes.

import uuid
from django.db import models

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

También puede consultar esta respuesta para obtener más información.

Keithhackbarth
fuente
@Keithhackbarth ¿cómo configuramos django para que use esto cada vez que crea automáticamente ID para tablas?
anon58192932
3
@ anon58192932 No está muy claro qué quieres decir exactamente con "siempre". Si desea que se usen UUID para cada modelo, cree su propio modelo base abstracto y utilícelo en lugar de django.models.Model.
Назар Топольський
4
Las diferencias de rendimiento solo son insignificantes cuando la base de datos subyacente admite el tipo UUID. Django todavía usa un campo de caracteres para la mayoría de las bases de datos (postgresql es la única base de datos documentada que admite el campo UUID).
NirIzr
Estoy confundido por qué esta es una respuesta popular ... La pregunta era acerca de la dificultad con los paquetes de terceros. A pesar de que Django admite de forma nativa UUID, todavía parece haber una serie de paquetes que no tienen en cuenta los UUID. En mi experiencia, es un dolor.
ambe5960
12

Me encontré con una situación similar y descubrí en la documentación oficial de Django , que object_idno tiene que ser del mismo tipo que la clave primaria del modelo relacionado. Por ejemplo, si desea que su relación genérica sea válida para los identificadores de IntegerField y CharField , simplemente configúreloobject_id para que sea CharField . Dado que los números enteros pueden coaccionarse en cadenas, estará bien. Lo mismo ocurre con UUIDField .

Ejemplo:

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
fuente
4

El problema real con UUID como PK es la fragmentación del disco y la degradación de inserción asociada con identificadores no numéricos. Debido a que el PK es un índice agrupado, cuando no se incrementa automáticamente, su motor de base de datos tendrá que recurrir a su unidad física al insertar una fila con un ID de menor ordinalidad, lo que sucederá todo el tiempo con UUID. Cuando obtiene muchos datos en su base de datos, puede llevar muchos segundos o incluso minutos insertar un nuevo registro. Y su disco eventualmente se fragmentará, lo que requerirá una desfragmentación periódica del disco. Todo esto es realmente malo.

Para solucionar estos problemas, recientemente se me ocurrió la siguiente arquitectura que pensé que valdría la pena compartir.

La clave pseudoprimaria de UUID

Este método le permite aprovechar los beneficios de un UUID como clave principal (utilizando un UUID de índice único), al tiempo que mantiene un PK autoincrementado para abordar la fragmentación e insertar problemas de degradación del rendimiento de tener un PK no numérico.

Cómo funciona:

  1. Cree una clave primaria autoincrementada llamada pkiden sus modelos de base de datos.
  2. Agregue un idcampo UUID indexado único para permitirle buscar por un ID de UUID, en lugar de una clave primaria numérica.
  3. Apunte la ForeignKey al UUID (usando to_field='id') para permitir que sus claves externas representen correctamente el Pseudo-PK en lugar del ID numérico.

Básicamente, harás lo siguiente:

Primero, cree un modelo base de Django abstracto

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

Asegúrese de extender el modelo base en lugar de los modelos.

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

También asegúrese de que sus ForeignKeys apunten al idcampo UUID en lugar del pkidcampo incrementado automáticamente :

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

Si está utilizando Django Rest Framework (DRF), asegúrese de crear también una clase Base ViewSet para establecer el campo de búsqueda predeterminado:

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

Y amplíe eso en lugar del ModelViewSet base para sus vistas de API:

class SiteViewSet(UUIDModelViewSet):
    model = Site

class PageViewSet(UUIDModelViewSet):
    model = Page

Más notas sobre el por qué y el cómo en este artículo: https://www.stevenmoseley.com/blog/uuid-primary-keys-django-rest-framework-2-steps

Steven Moseley
fuente
0

esto se puede hacer usando un modelo abstracto base personalizado, siguiendo los siguientes pasos.

Primero cree una carpeta en su proyecto, llámelo modelo base y luego agregue un modelo abstracto base.py con lo siguiente a continuación:

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']

segundo: en todo su archivo de modelo para cada aplicación, haga esto

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) 

Entonces, el incidente del modelo anterior es inherente a todo el campo en el modelo baseabstract.

Fadipe Ayobami
fuente
-1

La pregunta se puede reformular como "¿hay alguna manera de hacer que Django use un UUID para todos los ID de base de datos en todas las tablas en lugar de un entero autoincrementado?".

Claro que puedo hacer:

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

en todas mis tablas, pero no puedo encontrar una manera de hacer esto para:

  1. Módulos de terceros
  2. Django generó tablas ManyToMany

Entonces, esto parece ser una característica faltante de Django.

EMS
fuente