Zapraszam do bonusowego posta w serii "Zrozumieć Django: null vs blank w polach modeli"
O co w ogóle chodzi?
W dokumentacji Django dotyczącej modeli, znajduje się ostrzeżenie:
Avoid using null on string-based fields such as CharField and TextField. If a string-based field has null=True, that means it has two possible values for “no data”: NULL, and the empty string. In most cases, it’s redundant to have two possible values for “no data;” the Django convention is to use the empty string, not NULL. One exception is when a CharField has both unique=True and blank=True set. In this situation, null=True is required to avoid unique constraint violations when saving multiple objects with blank values.
Unikaj używania wartości null w polach opartych na ciągach, takich jak CharField i TextField. Jeśli pole tekstowe ma wartość null=True, oznacza to, że ma dwie możliwe wartości dla „brak danych”: NULL i pusty ciąg. W większości przypadków zbędne jest posiadanie dwóch możliwych wartości dla „brak danych”. Konwencja Django polega na użyciu pustego ciągu, a nie NULL. Jedynym wyjątkiem jest sytuacja, gdy CharField ma zarówno zestaw unique=True, jak i blank=True. W tej sytuacji null=True jest wymagane, aby uniknąć naruszeń ograniczeń unikalności podczas zapisywania wielu obiektów z pustymi wartościami. (tłumaczenie Google)
źródło: https://docs.djangoproject.com/en/4.2/ref/models/fields/
Postaram się to wyjaśnić.
Otóż pola modeli Django, oparte na ciągach znaków, czyli:
- CharField
- TextField
- EmailField
- itp.
mogą przyjmować wartości null=True
i blank=True
jednocześnie, co prowadzi do niejednoznaczności w danych. Obydwa te parametry odnoszą się do różnych aspektów pola:
- null=True: Określa, czy pole w bazie danych może przechowywać wartość NULL (brak wartości).
- blank=True: Określa, czy pole jest wymagane podczas tworzenia lub aktualizacji obiektu poprzez formularze, czy interfejsy użytkownika.
Teraz, jeśli pole ma null=True
i blank=True
jednocześnie, to ma dwie wartości określające "brak danych". Wskazana wyżej dokumentacja określa, że preferowane jest blak=True
, jako wystarczające. Jedynym wyjątkiem od tej reguły jest nadanie polu wymogu wartości unikalnej unique=true
- wtedy null=True
i blank=True
są konieczne, by pozostawić pole puste w istniejących wpisach w bazie.
Pojawiają się pytania: jeśli pole ma wartość NULL w bazie danych, to czy to oznacza, że jest to brak wartości czy po prostu pole nie zostało uzupełnione przez użytkownika? A może jest gdzieś błąd w kodzie, czy też pole zostało skądś zaimportowane? A może Null oznacza wartość nieznaną, a "" to że tego nie wpisał nikt w formularzu podczas tworzenia obiektu?
Dodatkowo można wpaść w pułapkę pisząc warunki if
w kodzie: np. if user.address is None
, "przepuści" pole address
z wartością "". Pytanie, czy tego oczekujemy? Znowu niejednoznaczność.
Bardzo dużo pytań, które utrudniają analizę kodu i ewentualne szukanie błędów.
Popatrzmy na przykłady. Do eksperymentów można wykorzystać kod z poprzednich wpisów i zmodyfikować - np. stąd: https://github.com/jacoor/django-w-godzine-webinar/blob/master/django_in_one_hour/notes/models.py
# models.py
from django.db import models
class Address(models.Model):
name = models.CharField(max_length=100, null=True, blank=True)
city = models.CharField(max_length=100, null=True, blank=True)
Powyższy kod deklaruje model Adress z polami name
i city
, przy czym oba te pola mają zadeklarowane null=True, blank=True
.
Żeby zmiany były widoczne w aplikacji, należy przygotować i wykonać migrację.
./manage.py makemigrations
./manage.py migrate
Uruchamiam konsole Django, żeby przeprowadzić test. Stworzę 3 obiekty Address
, o różnej wartości city
i spróbuję znaleźć te, które mają niezdefiniowaną nazwę miasta. Przez brak definicji rozumiem zarówno wartość null
jak i "".
» ./manage.py shell
Python 3.10.12 (main, Jun 20 2023, 17:00:24) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from notes.models import Address
>>> address1 = Address.objects.create(name="city is none", city=None)
>>> address2 = Address.objects.create(name="city is empty string", city="")
>>> address3 = Address.objects.create(name="city equals example", city="example")
>>> print(Address.objects.count()) # weryfikacja
3 # wynik
# sprawdzam typy danych
>>> for address in Address.objects.all():
... print(address.name)
... print(address.city)
... print(type(address.city))
address 1
None # city jest None
<class 'NoneType'> # city jest typu None
address 2
# city jest ""
<class 'str'> # city jest typu str
address 2
example
<class 'str'> # city jest typu str
# szukam adresu bez city
# pytanie. Jak to zrobic? Address.city is None czy Adress.city == ""?
# sprawdzam
>>> Address.objects.filter(city__isnull=True)
<QuerySet [<Address: Address object (1)>]> # tylko jeden? Dwa obiekty przecież nie mają city.
# sprawdzam pusty string
>>> Address.objects.filter(city="")
<QuerySet [<Address: Address object (2)>]> # też tylko jeden.
# a oba obiekty nie mają zdefiniowanego miasta.
# znalezienie obu adresów robi się skomplikowane:
>>> Address.objects.filter(city__isnull=True).filter(city="")
<QuerySet []> # nie zadziałało.
# zeby znalezc obiekty spełniąjące wymagania, czyli nie posiadające miasta, trzeba użyć ciężkiej artylerii: obiektów Q, czyli zaawansowanych zapytań
>>> from django.db.models import Q
>>> Address.objects.filter(Q(city__isnull=True) | Q(city=""))
<QuerySet [<Address: Address object (1)>, <Address: Address object (2)>]>
# Sukces.Mamy oba adresy bez miast!
Teraz, zmieniając deklaracje pola city
poprzez usunięcie null=True
cały ten efekt uzysalibyśmy prostym zapytaniem: Address.objects.filter(city="")
. Prawda, że łatwiej?
Uff, skomplikowane to było. I dużo pułapek. Mam nadzieję, że udało mi się wyjaśnić ten temat.
Podsumowanie
Wniosek: Nie używajcie null=True
i blank=True
dla pól tekstowych. Można się bardzo zdziwić. Django nie bez powodu zaleca pozostawienie blank=True
. Jedynym wyjątkiem od tej reguły jest nadanie polu wymogu wartości unikalnej unique=true
- wtedy null=True
i blank=True
są konieczne, by pozostawić pole puste w istniejących wpisach w bazie.
Dziękuję Ci za uwagę :-). Czy jest coś, co obecnie w Django sprawia Ci trudność? Napisz do mnie a może powstanie z tego wpis na blogu? Czytam wszystkie maile.
Zapisz się na newsletter, by otrzymać informacje kiedy pojawi się nowy post w tej serii. Zapraszam!
Ps. Spodobał Ci się post? Udostępnij go na swoich kanałach.
Ps2. Masz uwagi do posta, chcesz porozmawiać, szukasz pomocy z Pythonem i Django? Napisz do mnie!