Usando um UUID como chave primária em modelos Django (impacto de relações genéricas)

89

Por vários motivos ^, gostaria de usar um UUID como chave primária em alguns dos meus modelos Django. Se eu fizer isso, ainda poderei usar aplicativos externos como "contrib.comments", "django-votes" ou "django-tagging", que usam relações genéricas via ContentType?

Usando "django-vote" como exemplo, o modelo Vote se parece com isto:

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)

Este aplicativo parece estar assumindo que a chave primária para o modelo que está sendo votado é um número inteiro.

O aplicativo de comentários integrado parece ser capaz de lidar com PKs não inteiros, embora:

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

É este problema "assumido por PK inteira" uma situação comum para aplicativos de terceiros que tornaria o uso de UUIDs uma dor? Ou, possivelmente, estou interpretando mal esta situação?

Existe uma maneira de usar UUIDs como chaves primárias no Django sem causar muitos problemas?


^ Algumas das razões: ocultar contagens de objetos, evitar "rastreamento de id" de url, usar vários servidores para criar objetos não conflitantes, ...

Mitchf
fonte

Respostas:

57

Uma chave primária UUID causará problemas não apenas com relações genéricas, mas com eficiência em geral: cada chave estrangeira será significativamente mais cara - tanto para armazenar quanto para juntar - do que uma palavra de máquina.

No entanto, nada exige que o UUID seja a chave primária: apenas torne-o uma chave secundária , complementando seu modelo com um campo uuid com unique=True. Use a chave primária implícita normalmente (interna ao seu sistema) e use o UUID como seu identificador externo.

Pi Delport
fonte
16
Joe Holloway, não há necessidade disso: você pode simplesmente fornecer a função de geração de UUID como o campo default.
Pi Delport de
4
Joe: Eu uso django_extensions.db.fields.UUIDField para criar meus UUIDs em meu modelo. É simples, eu apenas defino meu campo assim: user_uuid = UUIDField ()
mitchf
3
@MatthewSchinckel: Quando você usa django_extensions.db.fields.UUIDFieldconforme mencionado por mitchf, você não terá problemas com as migrações Django-South - o campo mencionado por ele tem suporte embutido para migrações South.
Tadeck
122
Resposta terrível. O Postgres tem UUIDs nativos (128 bits) que são apenas 2 palavras em uma máquina de 64 bits, portanto, não seria "significativamente mais caro" do que o INT nativo de 64 bits.
postfuturist
8
Piet, visto que ele tem um índice btree, quantas comparações haverá em uma determinada consulta? Nao muitos. Além disso, tenho certeza de que a chamada memcmp será alinhada e otimizada na maioria dos sistemas operacionais. Com base na natureza das perguntas, eu diria que não usar UUID por causa de possíveis (provavelmente insignificantes) diferenças de desempenho é a otimização errada.
postfuturist
215

Como visto na documentação , no Django 1.8 existe um campo UUID embutido. As diferenças de desempenho ao usar um UUID vs inteiro são insignificantes.

import uuid
from django.db import models

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

Você também pode verificar esta resposta para obter mais informações.

Keithhackbarth
fonte
@Keithhackbarth como configuramos o django para usar isso sempre que criar IDs para tabelas automaticamente?
anon58192932
3
@ anon58192932 Não está muito claro o que exatamente você quer dizer com "todas as vezes". Se você deseja que UUIDs sejam usados ​​para cada modelo, crie seu próprio modelo base abstrato e use-o ao invés de django.models.Model.
Назар Топольський
4
As diferenças de desempenho são insignificantes apenas quando o banco de dados subjacente oferece suporte ao tipo UUID. Django ainda usa um charfield para a maioria dos bancos de dados (postgresql é o único banco de dados documentado que suporta o campo UUID).
NirIzr
Estou confuso por que essa é uma resposta popular ... A pergunta era sobre dificuldade com pacotes de terceiros. Apesar do Django suportar originalmente UUID, ainda parece haver um número de pacotes que não contabilizam UUIDs. Na minha experiência, é uma dor.
ambe5960
12

Eu me deparei com uma situação semelhante e descobri na documentação oficial do Django , que o object_idnão precisa ser do mesmo tipo que o primary_key do modelo relacionado. Por exemplo, se você deseja que seu relacionamento genérico seja válido para os IDs de IntegerField e CharField , apenas defina seu object_idcomo CharField . Uma vez que inteiros podem coagir em strings, tudo bem. O mesmo vale para UUIDField .

Exemplo:

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
fonte
2

O verdadeiro problema do UUID como PK é a fragmentação do disco e a degradação da inserção associada a identificadores não numéricos. Como o PK é um índice agrupado, quando não é incrementado automaticamente, seu mecanismo de banco de dados terá que recorrer ao seu drive físico ao inserir uma linha com um id de ordinalidade inferior, o que acontecerá o tempo todo com UUIDs. Quando você obtém muitos dados em seu banco de dados, pode levar vários segundos ou até minutos apenas para inserir um novo registro. E seu disco eventualmente ficará fragmentado, exigindo desfragmentação periódica do disco. Isso tudo é muito ruim.

Para resolver isso, recentemente criei a seguinte arquitetura que achei que valeria a pena compartilhar.

O UUID Pseudo-Chave Primária

Este método permite que você aproveite os benefícios de um UUID como uma chave primária (usando um UUID de índice exclusivo), enquanto mantém um PK incrementado automaticamente para lidar com a fragmentação e inserir questões de degradação de desempenho de ter um PK não numérico.

Como funciona:

  1. Crie uma chave primária auto-incrementada chamada pkid em seus modelos de banco de dados.
  2. Adicionar um UUID de índice exclusivo id campo para permitir que você pesquise por um ID de UUID, em vez de uma chave primária numérica.
  3. Aponte a ForeignKey para o UUID (usando to_field='id') para permitir que suas chaves estrangeiras representem adequadamente o Pseudo-PK em vez da ID numérica.

Essencialmente, você fará o seguinte:

Primeiro, crie um Django Base Model abstrato

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

Certifique-se de estender o modelo básico em vez de models.Model

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

Além disso, certifique-se de que suas Chaves estrangeiras apontem para o idcampo UUID em vez do pkidcampo incrementado automaticamente :

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

Se você estiver usando Django Rest Framework (DRF), certifique-se de criar também uma classe Base ViewSet para definir o campo de pesquisa padrão:

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

E estenda isso em vez do ModelViewSet básico para suas visualizações de API:

class SiteViewSet(UUIDModelViewSet):
    model = Site

class PageViewSet(UUIDModelViewSet):
    model = Page

Mais notas sobre o porquê e como neste artigo: https://www.stevenmoseley.com/blog/uuid-primary-keys-django-rest-framework-2-steps

Steven Moseley
fonte
0

isso pode ser feito usando um modelo abstrato base personalizado, usando as etapas a seguir.

Primeiro crie uma pasta em seu projeto, chame-a de basemodel e, em seguida, adicione um abstractmodelbase.py com o seguinte:

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: em todo o seu arquivo de modelo para cada aplicativo, faça isso

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) 

Portanto, o incidente do modelo acima é inerente a todo o campo do modelo básico.

Fadipe Ayobami
fonte
-1

A questão pode ser reformulada como "existe uma maneira de fazer o Django usar um UUID para todos os ids de banco de dados em todas as tabelas ao invés de um inteiro auto-incrementado?".

Claro, eu posso fazer:

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

em todas as minhas tabelas, mas não consigo encontrar uma maneira de fazer isso por:

  1. Módulos de terceiros
  2. Django gerou tabelas ManyToMany

Portanto, este parece ser um recurso ausente do Django.

EMS
fonte