Einzigartige Felder, die in Django Nullen zulassen

135

Ich habe Modell Foo, das Feldleiste hat. Das Balkenfeld sollte eindeutig sein, aber Nullen zulassen, was bedeutet, dass ich mehr als einen Datensatz zulassen möchte, wenn das Balkenfeld vorhanden ist null, aber wenn dies nicht nullder Fall ist , müssen die Werte eindeutig sein.

Hier ist mein Modell:

class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)

Und hier ist die entsprechende SQL für die Tabelle:

CREATE TABLE appl_foo
(
    id serial NOT NULL,
     "name" character varying(40) NOT NULL,
    bar character varying(40),
    CONSTRAINT appl_foo_pkey PRIMARY KEY (id),
    CONSTRAINT appl_foo_bar_key UNIQUE (bar)
)   

Wenn ich die Admin-Oberfläche verwende, um mehr als 1 foo-Objekte zu erstellen, bei denen der Balken null ist, wird der Fehler angezeigt: "Foo mit diesem Balken ist bereits vorhanden."

Wenn ich jedoch in die Datenbank (PostgreSQL) einfüge:

insert into appl_foo ("name", bar) values ('test1', null)
insert into appl_foo ("name", bar) values ('test2', null)

Dies funktioniert, ganz gut, es ermöglicht mir, mehr als einen Datensatz einzufügen, wobei der Balken null ist. Die Datenbank ermöglicht mir also, das zu tun, was ich will. Es stimmt einfach nicht mit dem Django-Modell. Irgendwelche Ideen?

BEARBEITEN

Die Portabilität der Lösung für die DB ist kein Problem, wir sind mit Postgres zufrieden. Ich habe mit einem abrufbaren einzigartigen versucht Einstellung, die meine Funktion Wahr / Falsch für bestimmte Werte von zurückkehrt bar , es hat keine Fehler geben, aber gefalzt wie es keinen Effekt auf alle hatte.

Bisher habe ich den eindeutigen Bezeichner aus der Balkeneigenschaft entfernt und die Eindeutigkeit der Balken in der Anwendung behandelt, suche jedoch immer noch nach einer eleganteren Lösung. Irgendwelche Empfehlungen?

Sergey Golovchenko
quelle
Ich kann noch nicht kommentieren, also hier eine kleine Ergänzung zu mächtig: Seit Django 1.4 würden Sie def get_db_prep_value(self, value, connection, prepared=False)als Methodenaufruf benötigen . Weitere Informationen finden Sie unter groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ . Die folgende Methode funktioniert auch für mich: def get_prep_value (self, value): Wenn value == "": #wenn Django versucht, einen String zu speichern, senden Sie die Datenbank None (NULL) return None else: Rückgabewert #otherwise, just Übergeben Sie den Wert
Jens
Ich habe dafür ein Django-Ticket geöffnet. Fügen Sie Ihre Unterstützung hinzu. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

Antworten:

154

Django hat NULL zum Zweck der Eindeutigkeitsprüfung nicht als gleich NULL angesehen, seit Ticket Nr. 9039 repariert wurde, siehe:

http://code.djangoproject.com/ticket/9039

Das Problem hierbei ist, dass der normalisierte "leere" Wert für ein Formular CharField eine leere Zeichenfolge ist, nicht None. Wenn Sie das Feld leer lassen, wird in der Datenbank eine leere Zeichenfolge und nicht NULL gespeichert. Leere Zeichenfolgen entsprechen leeren Zeichenfolgen für Eindeutigkeitsprüfungen, sowohl nach Django- als auch nach Datenbankregeln.

Sie können die Administrationsoberfläche zwingen, NULL für eine leere Zeichenfolge zu speichern, indem Sie Ihr eigenes benutzerdefiniertes Modellformular für Foo mit einer clean_bar-Methode bereitstellen, die die leere Zeichenfolge in None umwandelt:

class FooForm(forms.ModelForm):
    class Meta:
        model = Foo
    def clean_bar(self):
        return self.cleaned_data['bar'] or None

class FooAdmin(admin.ModelAdmin):
    form = FooForm
Karen Tracey
quelle
2
Wenn der Balken leer ist, ersetzen Sie ihn in der pre_save-Methode durch None. Code wird wohl trockener sein.
Ashish Gupta
6
Diese Antwort hilft nur bei der formularbasierten Dateneingabe, trägt jedoch nicht zum Schutz der Datenintegrität bei. Daten können über Importskripte, aus der Shell, über eine API oder auf andere Weise eingegeben werden. Es ist viel besser, die save () -Methode zu überschreiben, als benutzerdefinierte Fälle für jedes Formular zu erstellen, das die Daten berühren könnte.
Shacker
Django 1.9+ erfordert in Instanzen ein fieldsoder excludeAttribut ModelForm. Sie können dies Metaumgehen, indem Sie die innere Klasse in der ModelForm für die Verwendung in admin weglassen. Referenz: docs.djangoproject.com/de/1.10/ref/contrib/admin/…
user85461
62

** edit 30.11.2015 : In Python 3 wird die modul -globale __metaclass__Variable nicht mehr unterstützt . Zusätzlich wurde ab Django 1.10der SubfieldBaseKlasse veraltet :

aus den Dokumenten :

django.db.models.fields.subclassing.SubfieldBasewurde veraltet und wird in Django 1.10 entfernt. In der Vergangenheit wurde es verwendet, um Felder zu behandeln, in denen beim Laden aus der Datenbank eine Typkonvertierung erforderlich war, aber es wurde nicht in .values()Aufrufen oder in Aggregaten verwendet. Es wurde ersetzt durch from_db_value(). Beachten Sie, dass der neue Ansatz die to_python()Methode bei der Zuweisung nicht wie bei aufruft SubfieldBase.

Daher muss diese Lösung , wie in der from_db_value() Dokumentation und in diesem Beispiel vorgeschlagen , geändert werden in:

class CharNullField(models.CharField):

    """
    Subclass of the CharField that allows empty strings to be stored as NULL.
    """

    description = "CharField that stores NULL but returns ''."

    def from_db_value(self, value, expression, connection, contex):
        """
        Gets value right out of the db and changes it if its ``None``.
        """
        if value is None:
            return ''
        else:
            return value


    def to_python(self, value):
        """
        Gets value right out of the db or an instance, and changes it if its ``None``.
        """
        if isinstance(value, models.CharField):
            # If an instance, just return the instance.
            return value
        if value is None:
            # If db has NULL, convert it to ''.
            return ''

        # Otherwise, just return the value.
        return value

    def get_prep_value(self, value):
        """
        Catches value right before sending to db.
        """
        if value == '':
            # If Django tries to save an empty string, send the db None (NULL).
            return None
        else:
            # Otherwise, just pass the value.
            return value

Ich denke, ein besserer Weg als das Überschreiben der bereinigten_Daten im Administrator wäre, das Zeichenfeld zu unterklassifizieren - auf diese Weise funktioniert es "einfach", egal welches Formular auf das Feld zugreift. Sie können ''den NULL abfangen, bevor er an die Datenbank gesendet wird, und den NULL abfangen, sobald er aus der Datenbank kommt, und der Rest von Django wird es nicht wissen / sich darum kümmern. Ein schnelles und schmutziges Beispiel:

from django.db import models


class CharNullField(models.CharField):  # subclass the CharField
    description = "CharField that stores NULL but returns ''"
    __metaclass__ = models.SubfieldBase  # this ensures to_python will be called

    def to_python(self, value):
        # this is the value right out of the db, or an instance
        # if an instance, just return the instance
        if isinstance(value, models.CharField):
            return value 
        if value is None:  # if the db has a NULL (None in Python)
            return ''      # convert it into an empty string
        else:
            return value   # otherwise, just return the value

    def get_prep_value(self, value):  # catches value right before sending to db
        if value == '':   
            # if Django tries to save an empty string, send the db None (NULL)
            return None
        else:
            # otherwise, just pass the value
            return value  

Für mein Projekt habe ich dies in eine extras.pyDatei geschrieben, die sich im Stammverzeichnis meiner Site befindet. Dann kann ich es einfach from mysite.extras import CharNullFieldin der models.pyDatei meiner App tun. Das Feld verhält sich wie ein CharField. Denken Sie daran, es blank=True, null=Truebeim Deklarieren des Felds festzulegen. Andernfalls wird Django einen Validierungsfehler auslösen (Feld erforderlich) oder eine Datenbankspalte erstellen, die NULL nicht akzeptiert.

mächtig
quelle
3
In get_prep_value sollten Sie den Wert entfernen, falls der Wert mehrere Leerzeichen enthält.
Ax003d
1
Die aktualisierte Antwort hier funktioniert 2016 gut mit Django 1.10 und mit EmailField.
k0nG
4
Wenn Sie a aktualisieren, um a CharFieldzu sein CharNullField, müssen Sie dies in drei Schritten tun. Fügen Sie null=Truezunächst das Feld hinzu und migrieren Sie es. Führen Sie dann eine Datenmigration durch, um alle leeren Werte so zu aktualisieren, dass sie Nullen sind. Konvertieren Sie schließlich das Feld in CharNullField. Wenn Sie das Feld vor der Datenmigration konvertieren, führt Ihre Datenmigration nichts aus.
mlissner
3
Beachten Sie, dass in der aktualisierten Lösung from_db_value()dieser zusätzliche contexParameter nicht vorhanden sein sollte . Es sollte seindef from_db_value(self, value, expression, connection):
Phil Gyford
1
Der Kommentar von @PhilGyford gilt ab 2.0.
Shaheed Haque
16

Da ich neu im Stackoverflow bin, darf ich noch nicht auf Antworten antworten, aber ich möchte darauf hinweisen, dass ich aus philosophischer Sicht der beliebtesten Antwort auf diese Frage nicht zustimmen kann. (von Karen Tracey)

Das OP verlangt, dass sein Balkenfeld eindeutig ist, wenn es einen Wert hat, andernfalls null. Dann muss das Modell selbst sicherstellen, dass dies der Fall ist. Es kann nicht dem externen Code überlassen werden, dies zu überprüfen, da dies bedeuten würde, dass es umgangen werden kann. (Oder Sie können vergessen, dies zu überprüfen, wenn Sie in Zukunft eine neue Ansicht schreiben.)

Um Ihren Code wirklich OOP zu halten, müssen Sie daher eine interne Methode Ihres Foo-Modells verwenden. Das Ändern der save () -Methode oder des Felds ist eine gute Option, die Verwendung eines Formulars hierfür jedoch mit Sicherheit nicht.

Persönlich bevorzuge ich die Verwendung des vorgeschlagenen CharNullField, um die Portabilität auf Modelle zu gewährleisten, die ich möglicherweise in Zukunft definieren werde.

tBuLi
quelle
13

Die schnelle Lösung lautet:

def save(self, *args, **kwargs):

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
e-satis
quelle
2
Beachten Sie, dass die Verwendung MyModel.objects.bulk_create()diese Methode umgehen würde.
BenjaminGolder
Wird diese Methode aufgerufen, wenn wir im Admin-Bereich speichern? Ich habe es versucht, aber es tut es nicht.
Kishan Mehta
1
@ Kishan Django-Admin Panel wird diese Haken leider überspringen
Vincent Buscarello
@ e-satis Ihre Logik ist solide, also habe ich dies implementiert, aber der Fehler ist immer noch ein Problem. Mir wird gesagt, dass null ein Duplikat ist.
Vincent Buscarello
6

Eine andere mögliche Lösung

class Foo(models.Model):
    value = models.CharField(max_length=255, unique=True)

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)
Radagast
quelle
Dies ist keine gute Lösung, da Sie eine unnötige Beziehung erstellen.
Burak Özdemir
3

Dies wurde behoben, nachdem https://code.djangoproject.com/ticket/4136 behoben wurde. In Django 1.11+ können Sie verwenden, models.CharField(unique=True, null=True, blank=True)ohne leere Werte manuell in konvertieren zu müssen None.

Praseodym
quelle
1

Ich hatte vor kurzem die gleiche Anforderung. Anstatt verschiedene Felder in Unterklassen zu unterteilen, habe ich die Methode save () in meinem Modell (unten 'MyModel' genannt) wie folgt überschrieben:

def save(self):
        """overriding save method so that we can save Null to database, instead of empty string (project requirement)"""
        # get a list of all model fields (i.e. self._meta.fields)...
        emptystringfields = [ field for field in self._meta.fields \
                # ...that are of type CharField or Textfield...
                if ((type(field) == django.db.models.fields.CharField) or (type(field) == django.db.models.fields.TextField)) \
                # ...and that contain the empty string
                and (getattr(self, field.name) == "") ]
        # set each of these fields to None (which tells Django to save Null)
        for field in emptystringfields:
            setattr(self, field.name, None)
        # call the super.save() method
        super(MyModel, self).save()    
captnswing
quelle
1

Wenn Sie ein Modell MyModel haben und möchten, dass my_field Null oder eindeutig ist, können Sie die Speichermethode des Modells überschreiben:

class MyModel(models.Model):
    my_field = models.TextField(unique=True, default=None, null=True, blank=True) 

    def save(self, **kwargs):
        self.my_field = self.my_field or None
        super().save(**kwargs)

Auf diese Weise darf das Feld nicht leer sein, sondern ist nur nicht leer oder null. Nullen widersprechen nicht der Eindeutigkeit

Joseph Bani
quelle
0

Zum Guten oder Schlechten hält Django NULLdies NULLfür Zwecke der Eindeutigkeitsprüfung für gleichwertig . Es führt kein Weg daran vorbei, eine eigene Implementierung der Eindeutigkeitsprüfung zu schreiben, die als NULLeinzigartig angesehen wird, unabhängig davon, wie oft sie in einer Tabelle vorkommt.

(und denken Sie daran, dass einige DB-Lösungen dieselbe Ansicht vertreten NULL, sodass Code, der sich auf die Ideen einer DB stützt, NULLmöglicherweise nicht auf andere portierbar ist.)

James Bennett
quelle
6
Dies ist nicht die richtige Antwort. Siehe diese Antwort zur Erklärung .
Carl G
2
Einverstanden, dass dies nicht korrekt ist. Ich habe gerade IntegerField (leer = True, null = True, unique = True) in Django 1.4 getestet und es erlaubt mehrere Zeilen mit Nullwerten.
Slacy
0

Sie können dieses Feld UniqueConstraintmit der Bedingung hinzufügen nullable_field=nullund nicht in die fieldsListe aufnehmen. Wenn Sie auch eine Einschränkung benötigen nullable_field, deren Wert nicht ist null, können Sie eine zusätzliche hinzufügen.

Hinweis: UniqueConstraint wurde seit Django 2.2 hinzugefügt

class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)
    
    class Meta:
        constraints = [
            # For bar == null only
            models.UniqueConstraint(fields=['name'], name='unique__name__when__bar__null',
                                    condition=Q(bar__isnull=True)),
            # For bar != null only
            models.UniqueConstraint(fields=['name', 'bar'], name='unique__name__when__bar__not_null')
        ]
Андрей Лебедев
quelle