Używanie UUID jako klucza podstawowego w modelach Django (wpływ na relacje ogólne)

91

Z wielu powodów ^ chciałbym użyć UUID jako klucza podstawowego w niektórych moich modelach Django. Jeśli to zrobię, czy nadal będę mógł korzystać z aplikacji zewnętrznych, takich jak „contrib.comments”, „django-voting” lub „django-tagging”, które używają ogólnych relacji za pośrednictwem ContentType?

Na przykładzie „django-voting” model Vote wygląda następująco:

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)

Ta aplikacja wydaje się zakładać, że kluczem podstawowym modelu, na który głosowano, jest liczba całkowita.

Wydaje się, że wbudowana aplikacja do komentarzy jest w stanie obsłużyć niecałkowite PK:

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

Czy ten problem z zakładaniem liczby całkowitej PK jest powszechną sytuacją w przypadku aplikacji innych firm, która utrudniałaby używanie identyfikatorów UUID? A może źle odczytałem tę sytuację?

Czy istnieje sposób na użycie identyfikatorów UUID jako kluczy podstawowych w Django bez powodowania większych problemów?


^ Niektóre z powodów: ukrywanie liczby obiektów, zapobieganie indeksowaniu adresów URL, używanie wielu serwerów do tworzenia obiektów nie powodujących konfliktów, ...

mitchf
źródło

Odpowiedzi:

56

Klucz podstawowy UUID spowoduje problemy nie tylko z relacjami rodzajowymi, ale z ogólną wydajnością: każdy klucz obcy będzie znacznie droższy - zarówno w przechowywaniu, jak i przy łączeniu - niż słowo maszynowe.

Jednak nic nie wymaga, aby identyfikator UUID był kluczem podstawowym: po prostu uczyń go kluczem pomocniczym , uzupełniając model o pole uuid z rozszerzeniem unique=True. Używaj niejawnego klucza podstawowego jak zwykle (wewnętrznego w systemie) i używaj UUID jako identyfikatora zewnętrznego.

Pi Delport
źródło
16
Joe Holloway, nie ma takiej potrzeby: możesz po prostu dostarczyć funkcję generowania UUID jako funkcję pola default.
Pi Delport,
4
Joe: Używam django_extensions.db.fields.UUIDField do tworzenia moich identyfikatorów UUID w moim modelu. To proste, po prostu definiuję moje pole w ten sposób: user_uuid = UUIDField ()
mitchf
3
@MatthewSchinckel: Kiedy używasz django_extensions.db.fields.UUIDFieldjak wspomniał mitchf, nie będziesz miał problemów z migracjami Django-South - wspomniane przez niego pole ma wbudowaną obsługę migracji południowych.
Tadeck
126
Okropna odpowiedź. Postgres ma natywne (128-bitowe) identyfikatory UUID, które są tylko dwoma słowami na komputerze 64-bitowym, więc nie byłby „znacznie droższy” niż natywny 64-bitowy INT.
postfuturist
8
Piet, biorąc pod uwagę, że ma na sobie indeks btree, ile porównań będzie w danym zapytaniu? Niewiele. Jestem również pewien, że wywołanie memcmp zostanie wyrównane i zoptymalizowane w większości systemów operacyjnych. Biorąc pod uwagę naturę pytań, powiedziałbym, że nie używanie UUID z powodu możliwych (prawdopodobnie nieistotnych) różnic w wydajności jest złą optymalizacją.
postfuturist
219

Jak widać w dokumentacji , od Django 1.8 jest wbudowane pole UUID. Różnice w wydajności podczas używania identyfikatora UUID w porównaniu z liczbą całkowitą są pomijalne.

import uuid
from django.db import models

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

Możesz również sprawdzić tę odpowiedź, aby uzyskać więcej informacji.

keithhackbarth
źródło
@Keithhackbarth jak ustawić django, aby używał tego za każdym razem, gdy automatycznie tworzymy identyfikatory dla tabel?
anon58192932
3
@ anon58192932 Nie do końca jasne, co dokładnie masz na myśli, mówiąc „za każdym razem”. Jeśli chcesz, aby identyfikatory UUID były używane dla każdego modelu, utwórz własny abstrakcyjny model bazowy i używaj go zamiast django.models.Model.
Назар Топольський
4
Różnice w wydajności są pomijalne tylko wtedy, gdy bazowa baza danych obsługuje typ UUID. Django nadal używa charfield dla większości baz danych (postgresql jest jedyną udokumentowaną bazą danych obsługującą pole UUID).
NirIzr
Nie mam pojęcia, dlaczego jest to popularna odpowiedź ... Pytanie dotyczyło trudności z pakietami innych firm. Pomimo, że Django natywnie obsługuje UUID, nadal wydaje się, że istnieje wiele pakietów, które nie uwzględniają UUID. Z mojego doświadczenia wynika, że ​​to ból.
ambe5960
12

Natknąłem się na podobną sytuację i dowiedziałem się w oficjalnej dokumentacji Django , że object_idnie musi być tego samego typu, co klucz primary_key powiązanego modelu. Na przykład, jeśli chcesz, aby Twój związek generic być ważna zarówno dla IntegerField i Charfield id, zaledwie Określ object_idbyć Charfield . Ponieważ liczby całkowite mogą przekształcić się w łańcuchy, będzie dobrze. To samo dotyczy UUIDField .

Przykład:

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
źródło
4

Prawdziwym problemem związanym z UUID jako PK jest fragmentacja dysku i degradacja wstawki związana z identyfikatorami nieliczbowymi. Ponieważ PK jest indeksem klastrowym, gdy nie jest on automatycznie zwiększany, silnik bazy danych będzie musiał odwołać się do dysku fizycznego podczas wstawiania wiersza z identyfikatorem o niższej liczbie porządkowej, co będzie się działo przez cały czas z identyfikatorami UUID. Gdy masz dużo danych w swojej bazie danych, wstawienie jednego nowego rekordu może zająć wiele sekund lub nawet minut. W końcu dysk zostanie pofragmentowany, co będzie wymagało okresowej defragmentacji dysku. To wszystko jest naprawdę złe.

Aby je rozwiązać, niedawno wymyśliłem następującą architekturę, którą moim zdaniem warto podzielić.

Pseudo-klucz-podstawowy UUID

Ta metoda pozwala wykorzystać zalety UUID jako klucza podstawowego (przy użyciu unikalnego indeksu UUID), przy jednoczesnym zachowaniu automatycznie zwiększanej wartości PK w celu rozwiązania problemu fragmentacji i obniżenia wydajności w przypadku posiadania nieliczbowej PK.

Jak to działa:

  1. Utwórz automatycznie inkrementowany klucz podstawowy wywoływany pkidw modelach bazy danych.
  2. Dodaj unikatowe indeksowane idpole UUID, aby umożliwić wyszukiwanie według identyfikatora UUID zamiast numerycznego klucza podstawowego.
  3. Skieruj ForeignKey na UUID (używając to_field='id'), aby umożliwić Twoim kluczom obcym prawidłowe reprezentowanie Pseudo-PK zamiast numerycznego identyfikatora.

Zasadniczo wykonasz następujące czynności:

Najpierw utwórz abstrakcyjny model bazowy Django

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

Pamiętaj, aby rozszerzyć model podstawowy zamiast modeli

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

Upewnij się również, że klucze obce wskazują idpole UUID zamiast pola automatycznie zwiększanego pkid:

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

Jeśli używasz Django Rest Framework (DRF), upewnij się, że utworzyłeś również klasę Base ViewSet, aby ustawić domyślne pole wyszukiwania:

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

I rozszerz to zamiast podstawowego ModelViewSet dla widoków interfejsu API:

class SiteViewSet(UUIDModelViewSet):
    model = Site

class PageViewSet(UUIDModelViewSet):
    model = Page

Więcej informacji o tym, dlaczego i jak, w tym artykule: https://www.stevenmoseley.com/blog/uuid-primary-keys-django-rest-framework-2-steps

Steven Moseley
źródło
0

można to zrobić przy użyciu niestandardowego podstawowego modelu abstrakcyjnego, wykonując następujące kroki.

Najpierw utwórz folder w swoim projekcie, nazwij go basemodel, a następnie dodaj plik abstractmodelbase.py z poniższym:

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

po drugie: we wszystkich plikach modelu dla każdej aplikacji zrób to

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) 

Zatem powyższy incydent związany z modelem jest nieodłącznym elementem całego pola w modelu bazowym.

Fadipe Ayobami
źródło
-1

Pytanie można przeformułować w następujący sposób: „czy istnieje sposób, aby Django używał UUID dla wszystkich identyfikatorów bazy danych we wszystkich tabelach zamiast automatycznie zwiększanej liczby całkowitej?”.

Jasne, mogę zrobić:

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

we wszystkich moich tabelach, ale nie mogę znaleźć sposobu, aby to zrobić dla:

  1. Moduły innych firm
  2. Django wygenerowało tabele ManyToMany

Wydaje się więc, że jest to brakująca funkcja Django.

EMS
źródło