Verwendung einer UUID als Primärschlüssel in Django-Modellen (Auswirkungen auf generische Beziehungen)

87

Aus mehreren Gründen möchte ich in einigen meiner Django-Modelle eine UUID als Primärschlüssel verwenden. Wenn ich dies tue, kann ich dann weiterhin externe Apps wie "Contrib.comments", "Django-Voting" oder "Django-Tagging" verwenden, die generische Beziehungen über ContentType verwenden?

Am Beispiel von "Django-Voting" sieht das Abstimmungsmodell folgendermaßen aus:

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)

Diese App scheint davon auszugehen, dass der Primärschlüssel für das Modell, über das abgestimmt wird, eine Ganzzahl ist.

Die integrierte Kommentar-App scheint jedoch in der Lage zu sein, nicht ganzzahlige PKs zu verarbeiten:

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

Ist dieses "Integer-PK-angenommene" Problem eine häufige Situation für Apps von Drittanbietern, die die Verwendung von UUIDs zu einem Problem machen würden? Oder verstehe ich diese Situation möglicherweise falsch?

Gibt es eine Möglichkeit, UUIDs als Primärschlüssel in Django zu verwenden, ohne zu viele Probleme zu verursachen?


^ Einige der Gründe: Ausblenden der Anzahl der Objekte, Verhindern des "Crawls" von URLs, Verwenden mehrerer Server zum Erstellen nicht widersprüchlicher Objekte, ...

mitchf
quelle

Antworten:

56

Ein UUID-Primärschlüssel verursacht nicht nur Probleme mit generischen Beziehungen, sondern auch mit der Effizienz im Allgemeinen: Jeder Fremdschlüssel ist sowohl beim Speichern als auch beim Verbinden erheblich teurer als ein Maschinenwort.

Für nichts ist jedoch erforderlich, dass die UUID der Primärschlüssel ist: Machen Sie sie einfach zu einem Sekundärschlüssel , indem Sie Ihr Modell durch ein UUID-Feld mit ergänzen unique=True. Verwenden Sie den impliziten Primärschlüssel wie gewohnt (systemintern) und die UUID als externe Kennung.

Pi Delport
quelle
16
Joe Holloway, das ist nicht nötig: Sie können einfach die UUID-Generierungsfunktion als Feld bereitstellen default.
Pi Delport
4
Joe: Ich benutze django_extensions.db.fields.UUIDField, um meine UUIDs in meinem Modell zu erstellen. Es ist einfach, ich definiere mein Feld einfach so: user_uuid = UUIDField ()
mitchf
3
@MatthewSchinckel: Wenn Sie django_extensions.db.fields.UUIDFieldwie von mitchf erwähnt verwenden, werden Sie keine Probleme mit Django-Süd-Migrationen haben - das von ihm erwähnte Feld hat eine integrierte Unterstützung für Süd-Migrationen.
Tadeck
122
Schreckliche Antwort. Postgres verfügt über native (128-Bit) UUIDs, die auf einem 64-Bit-Computer nur aus 2 Wörtern bestehen. Dies wäre also nicht "wesentlich teurer" als native 64-Bit-INT.
Postfuturist
8
Piet, da es einen Btree-Index gibt, wie viele Vergleiche wird es für eine bestimmte Abfrage geben? Nicht viele. Ich bin mir auch sicher, dass der memcmp-Aufruf auf den meisten Betriebssystemen ausgerichtet und optimiert wird. Basierend auf der Art der Fragen, würde ich sagen , nicht UUID verwenden wegen der möglichen (wahrscheinlich vernachlässigbar) Leistungsunterschiede der falsche Optimierung ist.
Postfuturist
214

Wie in der Dokumentation zu sehen ist , gibt es ab Django 1.8 ein eingebautes UUID-Feld. Die Leistungsunterschiede bei Verwendung einer UUID gegenüber einer Ganzzahl sind vernachlässigbar.

import uuid
from django.db import models

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

Sie können diese Antwort auch überprüfen, um weitere Informationen zu erhalten.

keithhackbarth
quelle
@Keithhackbarth Wie stellen wir Django so ein, dass dies jedes Mal verwendet wird, wenn automatisch IDs für Tabellen erstellt werden?
anon58192932
3
@ anon58192932 Nicht wirklich klar, was genau du mit "jedes Mal" meinst. Wenn Sie möchten, dass UUIDs für jedes Modell verwendet werden, erstellen Sie Ihr eigenes abstraktes Basismodell und verwenden Sie es anstelle von django.models.Model.
1азар Топольський
4
Leistungsunterschiede sind nur dann vernachlässigbar, wenn die zugrunde liegende Datenbank den UUID-Typ unterstützt. Django verwendet für die meisten DBs immer noch ein Zeichenfeld (postgresql ist die einzige dokumentierte Datenbank, die das UUID-Feld unterstützt).
NirIzr
Ich bin verwirrt, warum dies eine beliebte Antwort ist ... Die Frage war nach Schwierigkeiten mit Paketen von Drittanbietern. Obwohl Django UUID nativ unterstützt, scheint es immer noch eine Reihe von Paketen zu geben, die UUIDs nicht berücksichtigen. Nach meiner Erfahrung ist es ein Schmerz.
ambe5960
12

Ich bin auf eine ähnliche Situation gestoßen und habe in der offiziellen Django-Dokumentation herausgefunden , dass der object_idnicht vom selben Typ sein muss wie der primäre Schlüssel des zugehörigen Modells. Zum Beispiel, wenn Sie Ihre allgemeine Beziehung für beide gültig sein sollen Integer und CharField ids einfach Ihr Set object_idein sein CharField . Da ganze Zahlen zu Strings gezwungen werden können, ist dies in Ordnung. Gleiches gilt für UUIDField .

Beispiel:

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
quelle
1

Das eigentliche Problem mit UUID als PK ist die Fragmentierung der Festplatte und die Verschlechterung der Einfügung, die mit nicht numerischen Kennungen verbunden sind. Da es sich bei der PK um einen Clustered-Index handelt, muss Ihre DB-Engine, wenn sie nicht automatisch inkrementiert wird, beim Einfügen einer Zeile mit einer ID niedrigerer Ordinalität auf Ihr physisches Laufwerk zurückgreifen, was bei UUIDs immer der Fall ist. Wenn Sie viele Daten in Ihrer Datenbank haben, kann es viele Sekunden oder sogar Minuten dauern, nur einen neuen Datensatz einzufügen. Und Ihre Festplatte wird schließlich fragmentiert und erfordert eine regelmäßige Defragmentierung der Festplatte. Das ist alles sehr schlecht.

Um diese Probleme zu lösen, habe ich kürzlich die folgende Architektur entwickelt, von der ich dachte, dass sie es wert wäre, geteilt zu werden.

Der UUID-Pseudo-Primärschlüssel

Mit dieser Methode können Sie die Vorteile einer UUID als Primärschlüssel (unter Verwendung einer eindeutigen Index-UUID) nutzen und gleichzeitig eine automatisch inkrementierte PK beibehalten, um die Fragmentierungsprobleme zu lösen und Leistungseinbußen bei einer nicht numerischen PK einzufügen.

Wie es funktioniert:

  1. Erstellen Sie einen automatisch inkrementierten Primärschlüssel mit dem Namen pkid in Ihren DB-Modellen .
  2. Fügen Sie eine eindeutig indizierte UUID hinzu id Feld hinzu, damit Sie anstelle eines numerischen Primärschlüssels nach einer UUID-ID suchen können.
  3. Zeigen Sie mit dem ForeignKey auf die UUID (using to_field='id'), damit Ihre Fremdschlüssel die Pseudo-PK anstelle der numerischen ID korrekt darstellen können.

Im Wesentlichen werden Sie Folgendes tun:

Erstellen Sie zunächst ein abstraktes Django-Basismodell

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

Stellen Sie sicher, dass Sie das Basismodell anstelle von models.Model erweitern

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

Stellen Sie außerdem sicher, dass Ihre ForeignKeys auf das UUID- idFeld anstatt auf das automatisch inkrementierte pkidFeld verweisen :

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

Wenn Sie Django Rest Framework (DRF) verwenden, müssen Sie auch eine Base ViewSet-Klasse erstellen, um das Standardsuchfeld festzulegen:

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

Und erweitern Sie dies anstelle des Basis-ModelViewSet für Ihre API-Ansichten:

class SiteViewSet(UUIDModelViewSet):
    model = Site

class PageViewSet(UUIDModelViewSet):
    model = Page

Weitere Hinweise zum Warum und Wie in diesem Artikel: https://www.stevenmoseley.com/blog/uuid-primary-keys-django-rest-framework-2-steps

Steven Moseley
quelle
0

Dies kann mithilfe eines benutzerdefinierten abstrakten Basismodells mithilfe der folgenden Schritte erfolgen.

Erstellen Sie zuerst einen Ordner in Ihrem Projekt, nennen Sie ihn Basemodell, und fügen Sie dann eine abstractmodelbase.py mit den folgenden Angaben hinzu:

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

Zweitens: Führen Sie dies in Ihrer gesamten Modelldatei für jede App aus

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) 

Der obige Modellvorfall ist also dem gesamten Feld des Baseabstract-Modells eigen.

Fadipe Ayobami
quelle
-1

Die Frage kann wie folgt umformuliert werden: "Gibt es eine Möglichkeit, Django dazu zu bringen, eine UUID für alle Datenbank-IDs in allen Tabellen anstelle einer automatisch inkrementierten Ganzzahl zu verwenden?".

Klar kann ich machen:

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

in allen meinen Tabellen, aber ich kann keinen Weg finden, dies zu tun für:

  1. Module von Drittanbietern
  2. Django hat ManyToMany-Tabellen generiert

Dies scheint also eine fehlende Django-Funktion zu sein.

EMS
quelle