Die Grenzen des Django ORM erkunden

15.11.2023
Development Python Django ORM Postgres

Das ORM

ORMs oder Object Relational Mappings sind systeme, die es uns beim programmieren erlauben, in der Programmiersprache unseres vertrauens zu bleiben und dennoch mit Datenbanken zu interagieren.

In Django beispielsweise werden Models definiert, die die Daten repräsentieren, die wir gerne in einer Datenbanktabelle abbilden möchten. Beispielswesie einen Kunden

class Customer(models.Model):

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=200)
    customer_number = models.PositiveIntegerField(blank=True, null=True)
    zip_code = models.CharField(max_length=20)

    # additional fields...

Dann können wir im Python code so beispielsweise einen neuen Kunden anlegen:

mycustomer = Customer(
    name="Screenion GmbH",
    customer_number=3215,
    zip_code="61440"
)

mycustomer.save()

Mit .save() wird SQL Code generiert, der den neuen Kunden in die Datenbank schreibt.

Eindeutigkeit

Für unseren Kundenstamm ist jedoch wichtig, dass kein Kunde doppelt angelegt wird. Normalerweise bietet sich für diese Aufgabenstellung an, zu fordern, dass beispielswesie die Email Adresse eindeutig ist. Wir haben in diesem Beispiel jedoch kein Feld für die Email adresse. Stattdessen fordern wir, dass die Kombination aus Namen, Kundennummer und Postleitzahl eindeutig sein soll. Das lässt sich so im Model mit Constraints abbilden, zum Beispiel so:

class Customer(models.Model):

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=200)
    customer_number = models.PositiveIntegerField(blank=True, null=True)
    zip_code = models.CharField(max_length=20)

    class Meta:
        constraints = [
            UniqueConstraint(
                "name",
                "customer_number",
                "zip_code",
                name="unique_name_customer_number_zip_code"
            ),
        ]

Das Problem

Hierbei haben wir jedoch einen Fehler gemacht, der nicht offensichtlich ist. Die Kundennummer ist optional. Wird sie nicht angegeben, steht in der Datenbank "null". Die Datenbank vergleicht bei Eindeutigkeitsrandbedingungen (UniqueConstraints) die einzelnen Felder die in der Bedingung angegeben worden sind. Dabei sind zwei "null" - Werte nicht gleich, sondern werden von der Datenbank als unterschiedlich betrachtet. Wir können also ohne, dass wir von der Datenbank aufgehalten werden, folgendes tun:

mycustomer = Customer(
    name="Screenion GmbH",
    zip_code="61440"
)

mycustomer.save()

othercustomer = Customer(
    name="Screenion GmbH",
    zip_code="61440"
)

othercustomer.save()

Das ist schlecht, denn intuitiv hätten wir hier erwartet, dass unser Eindeutigkeitskritierium in der Datenbank das anlegen des zweiten Kunden verhindert.

Die Lösung

Ohne ORM gäbe es die Möglichkeit, der Datenbank zu sagen, dass null Werte nicht als unterschiedlich interpretiert werden sollten.

...
CONSTRAINT unique_name_customer_number_zip_code UNIQUE NULLS NOT DISTINCT ("name", "customer_number", "zip_code")
...

Im Django ORM gibt es diese Möglichkeit leider noch nicht, zumindest war sie nicht auffindbar.

Stattdessen lässt sich das Problem mit zwei Randbedingungen lösen. Eine für den Fall, dass keine Kundennummer vorliegt und eine für den Fall, dass eine Kundennummer vorliegt. Unsere Modeldefinition ist also:

class Customer(models.Model):

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=200)
    customer_number = models.PositiveIntegerField(blank=True, null=True)
    zip_code = models.CharField(max_length=20)

    class Meta:
        constraints = [
            UniqueConstraint(
                "name",
                "zip_code",
                name="unique_name_zip_code",
                condition=Q(customer_number__isnull=True)

            ),       
            UniqueConstraint(
                "name",
                "customer_number",
                "zip_code",
                name="unique_name_customer_number_zip_code"
            ),
        ]

Mit einem Q-Objekt haben wir hier beschrieben, dass die erste Randbedingung nur gelten soll, wenn keine Kundennummer vorhanden ist. Dann müssen Name und Postleitzahl eindeutig sein. Falls eine Kundennummer vorliegt, gilt nur noch die zweite Randbedingung: Name, Kundennummer und Postleitzahl müssen eindeutig sein.

Impressum

Screenion GmbH

Büroanschrift:
Adenauerallee 21, 1. OG
61440 Oberursel

Rechnungsanschrift und Firmensitz:
Oberhöchstadter Straße 70a
61440 Oberursel
Deutschland

Fon: +49 (0)6171 9519800
Fax: 06171-9519808
post@screenion.de
Web: https://www.screenion.de

Geschäftsführer: Reto M. Kiefer
Amtsgericht: Bad Homburg HRB 13769
UmSt-Id gemäß §27a Umsatzsteuergesetz: DE273300425

Datenschutz

Mit Ihrem Zugriff auf unsere Website werden Daten, die eine Identifizierung ermöglichen könnten (z.B. IP-Adresse) und weitere Angaben wie Datum, Uhrzeit und aufgerufene Seite In Log-Files gespeichert.

Eine Auswertung der Daten, außer für statistische Zwecke sowie zur Optimierung unseres Internetangebots in anonymisierter Form, findet nicht statt. Sie können unsere Website grundsätzlich ohne Offenlegung Ihrer Identität nutzen.

Des Weiteren verwenden wir keine Cookies oder ähnliche Technologien. Sicher haben Sie schon den Hinweis vermisst :)


Wir verwenden Fotos von unsplash sowie pixabay und danken: