Unikalne pola, które dopuszczają wartości null w Django


135

Mam model Foo, który ma pasek pola. Pole słupka powinno być unikalne, ale dopuszczać w nim wartości null, co oznacza, że ​​chcę zezwolić na więcej niż jeden rekord, jeśli pole słupka jest null, ale jeśli tak nie jest, nullwartości muszą być unikalne.

Oto mój model:

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

A oto odpowiedni kod SQL dla tabeli:

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

Kiedy używam interfejsu administratora do tworzenia więcej niż 1 obiektów foo, gdzie bar jest pusty, pojawia się błąd: „Foo z tym paskiem już istnieje”.

Jednak kiedy wstawiam do bazy danych (PostgreSQL):

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

To działa, po prostu dobrze, pozwala mi wstawić więcej niż 1 rekord z pustym słupkiem, więc baza danych pozwala mi robić co chcę, po prostu coś jest nie tak z modelem Django. Jakieś pomysły?

EDYTOWAĆ

Przenośność rozwiązania o ile DB nie jest problemem, jesteśmy zadowoleni z Postgres. Próbowałem ustawić unikalny dla wywoływanego, co było moją funkcją zwracającą True / False dla określonych wartości słupka , nie dawało to żadnych błędów, jednak zszywane tak, jakby nie miało żadnego efektu.

Do tej pory usunąłem unikalny specyfikator z właściwości paska i obsługuję unikalność paska w aplikacji, jednak nadal szukam bardziej eleganckiego rozwiązania. Jakieś zalecenia?


Nie mogę jeszcze komentować, więc tutaj mały dodatek do mightyhal: Od Django 1.4 potrzebujesz def get_db_prep_value(self, value, connection, prepared=False)jako wywołania metody. Sprawdzić groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ więcej informacji. U mnie też działa następująca metoda: def get_prep_value (self, value): if value == "": #if Django spróbuje zapisać ciąg „”, wyślij db None (NULL) return None else: return value #otherwise, just przekazać wartość
Jens

Otworzyłem w tym celu bilet Django. Dodaj swoje wsparcie. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

Odpowiedzi:


154

Django nie uważa NULL za równe NULL do celów kontroli unikalności, ponieważ bilet nr 9039 został naprawiony, zobacz:

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

Problem polega na tym, że znormalizowana wartość „pusta” dla formularza CharField jest pustym ciągiem, a nie None. Więc jeśli zostawisz to pole puste, otrzymasz pusty ciąg, a nie NULL, przechowywany w DB. Puste ciągi są równe pustym ciągom do sprawdzania unikalności, zarówno w ramach Django, jak i reguł bazy danych.

Możesz zmusić interfejs administratora do przechowywania NULL dla pustego ciągu, dostarczając własny, dostosowany formularz modelu dla Foo z metodą clean_bar, która zamienia pusty ciąg na None:

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

2
Jeśli pasek jest pusty, zamień go na None w metodzie pre_save. Przypuszczam, że kod będzie bardziej SUCHY.
Ashish Gupta,

6
Ta odpowiedź pomaga tylko w przypadku wprowadzania danych na podstawie formularzy, ale nie robi nic, aby faktycznie chronić integralność danych. Dane mogą być wprowadzane za pomocą skryptów importu, z powłoki, poprzez API lub w jakikolwiek inny sposób. Znacznie lepiej jest przesłonić metodę save () niż tworzyć niestandardowe przypadki dla każdego formularza, który może dotknąć danych.
shacker

Django 1.9+ wymaga atrybutu fieldsor excludew ModelForminstancjach. Możesz obejść ten problem, pomijając Metaklasę wewnętrzną z ModelForm do użytku w admin. Źródła
user85461

62

** edycja 30.11.2015 : W Pythonie 3 __metaclass__zmienna globalna modułu nie jest już obsługiwana . Dodatkowo, jak z Django 1.10tej SubfieldBaseklasy została zaniechana :

z dokumentów :

django.db.models.fields.subclassing.SubfieldBasezostał uznany za przestarzały i zostanie usunięty w Django 1.10. W przeszłości był używany do obsługi pól, w których wymagana była konwersja typu podczas ładowania z bazy danych, ale nie była używana w .values()wywołaniach ani w agregacjach. Został zastąpiony from_db_value(). Należy zauważyć, że nowe podejście nie wywołuje to_python()metody przy przypisywaniu, jak miało to miejsce w przypadku SubfieldBase.

Dlatego jak sugeruje from_db_value() dokumentacja i ten przykład , to rozwiązanie należy zmienić na:

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

Myślę, że lepszym sposobem niż nadpisanie clean_data w adminie byłoby podklasa charfielda - w ten sposób bez względu na to, jaki formularz uzyskuje dostęp do pola, będzie "po prostu działać". Możesz złapać to ''tuż przed wysłaniem do bazy danych i złapać NULL zaraz po tym, jak wyjdzie z bazy danych, a reszta Django nie będzie wiedzieć / dbać. Szybki i brudny przykład:

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  

W przypadku mojego projektu wrzuciłem to do extras.pypliku, który znajduje się w katalogu głównym mojej witryny, a następnie mogę po prostu from mysite.extras import CharNullFieldw models.pypliku mojej aplikacji . Pole zachowuje się jak CharField - pamiętaj tylko, aby ustawić je blank=True, null=Truepodczas deklarowania pola, w przeciwnym razie Django zgłosi błąd walidacji (pole wymagane) lub utworzy kolumnę db, która nie akceptuje NULL.


3
w get_prep_value, powinieneś usunąć wartość, jeśli ma ona kilka spacji.
ax003d

1
Zaktualizowana tutaj odpowiedź działa dobrze w 2016 z Django 1.10 i przy użyciu EmailField.
k0nG

4
Jeśli aktualizujesz a CharFieldna a CharNullField, musisz to zrobić w trzech krokach. Najpierw dodaj dane null=Truedo pola i przeprowadź migrację. Następnie przeprowadź migrację danych, aby zaktualizować wszystkie puste wartości, tak aby były puste. Na koniec przekonwertuj pole na CharNullField. Jeśli przekonwertujesz pole przed migracją danych, migracja danych nic nie da.
mlissner

3
Zauważ, że w zaktualizowanym rozwiązaniu from_db_value()nie powinno mieć tego dodatkowego contexparametru. Powinno byćdef from_db_value(self, value, expression, connection):
Phil Gyford

1
Komentarz od @PhilGyford obowiązuje od 2.0.
Shaheed Haque

16

Ponieważ jestem nowy w stackoverflow, nie mogę jeszcze odpowiadać na odpowiedzi, ale chciałbym zwrócić uwagę, że z filozoficznego punktu widzenia nie mogę zgodzić się z najpopularniejszą odpowiedzią na to pytanie. (przez Karen Tracey)

OP wymaga, aby jego pole słupka było unikalne, jeśli ma wartość, lub zerowe w przeciwnym razie. W takim razie musi być tak, że sam model zapewnia, że ​​tak jest. Nie można pozostawić tego kodu zewnętrznemu, aby to sprawdzić, ponieważ oznaczałoby to, że można go ominąć. (Lub możesz zapomnieć o sprawdzeniu tego, jeśli napiszesz nowy widok w przyszłości)

Dlatego, aby Twój kod był prawdziwie OOP, musisz użyć wewnętrznej metody swojego modelu Foo. Modyfikacja metody save () lub pola to dobre opcje, ale użycie formularza do tego z pewnością nie.

Osobiście wolę używać sugerowanego CharNullField, aby móc przenosić się do modeli, które mogę zdefiniować w przyszłości.


13

Szybka naprawa to:

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)

2
należy pamiętać, że użycie MyModel.objects.bulk_create()spowoduje obejście tej metody.
BenjaminGolder,

Czy ta metoda jest wywoływana, gdy zapisujemy z panelu administratora? Próbowałem, ale nie udało się.
Kishan Mehta

1
@Kishan panel django-admin pominie te haki niestety
Vincent Buscarello

@ e-satis Twoja logika jest dobra, więc zaimplementowałem to, ale błąd nadal jest problemem. Powiedziano mi, że null to duplikat.
Vincent Buscarello

6

Inne możliwe rozwiązanie

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

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)

To nie jest dobre rozwiązanie, ponieważ tworzysz niepotrzebną relację.
Burak Özdemir


1

Niedawno miałem ten sam wymóg. Zamiast tworzyć podklasy różnych pól, zdecydowałem się zastąpić metodę save () w moim modelu (o nazwie „MójModel” poniżej) w następujący sposób:

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

1

Jeśli masz model MyModel i chcesz, aby my_field miało wartość Null lub unikalną, możesz nadpisać metodę zapisu modelu:

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)

W ten sposób pole nie może być puste, będzie tylko niepuste lub puste. wartości zerowe nie zaprzeczają wyjątkowości


0

Lepiej lub gorzej, Django uważa NULLza równoważne do NULLcelów kontroli unikalności. Naprawdę nie NULLda się tego obejść, jak tylko napisać własną implementację kontroli niepowtarzalności, która uważa się za unikalną, bez względu na to, ile razy występuje w tabeli.

(i pamiętaj, że niektóre rozwiązania DB mają ten sam pogląd NULL, więc kod oparty na pomysłach jednej bazy danych NULLmoże nie być przenośny dla innych)


6
To nie jest poprawna odpowiedź. Zobacz tę odpowiedź, aby uzyskać wyjaśnienie .
Carl G

2
Uzgodniono, że to nie jest poprawne. Właśnie przetestowałem IntegerField (blank = True, null = True, unique = True) w Django 1.4 i zezwala na wiele wierszy z wartościami null.
slacy

0

Możesz dodać UniqueConstraintwarunek nullable_field=nulli nie uwzględniać tego pola na fieldsliście. Jeśli potrzebujesz również ograniczenia, którego nullable_fieldwartość nie jest null, możesz dodać dodatkowe.

Uwaga: UniqueConstraint została dodana od wersji django 2.2

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')
        ]
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.