Dzień pierwszy - null i blank dla pół modeli, DateField, on_delete
Do naszych dwóch modeli (City
, Cabaret
) dodaliśmy następny (Show
), za pomocą którego będziemy zapisywać występy. Przy okazji miałem możliwość opowiedzenia, że jeśli nie ustawimy inaczej, to nasze modele będą miały ustawione null=False
, co oznacza, że migracja tworząca lub zmieniająca kolumnę w bazie, zaznaczy przy niej, że NOT NULL
, co wymusi dla niej dodanie wartości.
Analogicznie, domyślnie w modelu, będzie ustawiony blank=False
, co oznacza, że teraz już, wyłącznie Django (bez współpracy z bazą danych) nie pozwoli by pole pozostało puste. Jak to z domyślnymi wartościami, możemy powtórzyć te same zapisy dla jasności, albo zadeklarować te parametry z innymi wartościami.
Przy okazji, dodania pierwszy raz pola/columny DateField()
, przypomnieliśmy sobie jeszcze kilka innych typów pól pochodzący z ModelForm, a także opowiedziałem o:
DateField.auto_now
- czyli o parametrze, który automatycznie ustawia bieżącą datę za każdym razem kiedy dodajemy lub aktualizujemy obiekt modelu (wiersza w bazie danych)DateField.auto_now_add
- parametr, który automatycznie ustawia bieżącą datę podczas tworzenia obiektu modelu (wiersza w bazie danych)
Nasz model Show
łączył w sobie dwa poprzednie modele. Przy deklaracji takiego 'połączenia`, możemy zadeklarować, co powinno się stać gdy dane połączone z naszymi dane zostaną usunięte. Służy do tego parametr on_delete
.
Jak już sie przyzwyczailiśmy, modele mają sporo domyślnych ustawień, dla on_delete będzie to on_delete=models.CASCADE
.
Dla połączenia z modelem Cabaret, ustawimy on_delete na wartość domyślną, czyli models.CASCADE
, gdyż po usunięciu kabaretu z bazy, nasz występ traci rację bytu:
cabaret = models.ForeignKey(Cabaret, on_delete=models.CASCADE)
Dla połaczenia z modelem City
ustawiliśmy models.SET_NULL
(co ustawi NULL
zamiast city_id), gdyż uznaliśmy, że po usunięciu miasta, dane o występie będą jeszcze użyteczne:
city = models.ForeignKey(City, on_delete=models.SET_NULL, blank=True, null=True)
Zarejstrowaliśmy nasz nowy model w admin.py i przystąpiliśmy do dodawania pierwszych występów za pomocą panelu administracyjnego.
Dzień drugi
Skoro już:
- 1) mamy nasze modele (City, Cabaret, Show),
- 2) możemy korzystań z nich w panelu administracyjnym /admin/ uzupełniając/zmieniając dane w bazie danych,
to nastepnym krokiem będzie:
- 3) wyświetlanie danych z bazy danych na tzw. frontendzie (w uproszczeniu na wszystkich url-ach, które nie są urlami panelu administracyjnego)
Jak się dowiedzieliśmy na Documentation:Managers, Managers, są to takie "ustrojstrwa", dzięki którym, nasze przyszłe i wcześniejesze modele mają dostęp do bazy i dzięki, którym modele mogą wykonywać operację na bazach danych. Kazdy model musi posiadać przynajmniej jeden Manager, a domyślnie jest to maanager objects (.objects
).
Pokazałem też, jak w prosty sposób można, dodać już na istniejącym już projekcie i na intensywnie używanych modelach jednoznaczny podział na
- 1) obiekty domyślnie widoczne
Report.objects
(które mają ustawione w baziedeclined=False
), obsługiwane są jako domyślny Manager - 2) na te domyślnie ukryte (np. wygaszone)
Report.objects_declined
(gdziedeclined=True
) -
3) na całość (te domyślnie ukryte i domyślnie widocznie)
Report.objects_all
(gdziedeclined=None
)class ReportDefaultManager(models.Manager): def __init__(self, declined=False): super(ReportDefaultManager, self).__init__() self.declined = declined def get_queryset(self): if self.declined in [True, False]: return super(ReportDefaultManager, self).get_queryset().filter(declined=self.declined) return super(ReportDefaultManager, self).get_queryset() class Report(models.Model): objects = ReportDefaultManager(declined=False) objects_declined = ReportDefaultManager(declined=True) objects_all = ReportDefaultManager(declined=None)
A wszytko dzięki temu, że każda z definicji modelu miała predefiniowany już jeden warunek, czyli wartość pola (columny w bazie) declined, czyli zazwyczaj nie będziemy pytać o całą tabelę (wszystkie obiekty danego modelu), ale zawyczaj zapytamy o Raporty declined=False
, a tylko czasem o declined=True
lub declined=None
.
Postanowiłem także wytłumaczyć, mój "dziwny" zapis powodujący wyświetlenie się domyślnego stringa oznaczającego obiekt.
def __str__(self):
return "{} {} {}".format(self.cabaret.name, self.city, self.date)
Przeszyliśmy więc na stronę PyFormat, na której w przystępny sposób, pokazane są formatowania stringów, a także pokazana róznica między "%s," % "some string"
(stary styl formatowania), a "".format()
(nowy styl formatowania).
Stare formatowanie | Nowe formatowanie | Wynik | Opis |
---|---|---|---|
'%s %s' % ('one', 'two') |
'{} {}'.format('one', 'two') |
one two |
porządek wg stringu taki jak porządek parametrów |
brak | '{1} {0}'.format('one', 'two') |
two one |
użycie indeksów odnoszącyh się do kolejności parametrów |
'%10s' % ('test',) |
'{:>10}'.format('test') |
test |
przynajmniej 10 znaków będzie miał cały string, a tekst zostanie wyrównany do prawej |
Dzień trzecie
Teraz kiedy relacje między wszystkimi naszymi modelami stały się ciut bardziej skomplikowane, dobrze jest odpowiedzieć o tym, czym jest, tzw. lazy evalutation (leniwa ewaluacja). Tak na prawdę nie wiedzałem do dzisiaj, że obsługa operatorów logiczny w zasadzie w większości języków programowania, to także, w rzeczy samej, lazy evalution. W sumie oczywiste.
if True or get_my_number() > 0 or locals():
print('True is always True, so get_my_number() won\'t be triggered')
Jednym słowem, pierwsza część warunku złóżonego, czyli (True
), będzie zawsze spełnione, więc funkcje get_my_number()
i locals()
nigdy nie zostaną wywołane, gdyż or
, który czytamy tutaj jako jeżeli poprzedni warunek nie zostanie spełniony to:, sprawi, że będą już zbędne, nieistotne, czyli nie warto już ich sprawdzać, nie warto wykonywać.
Wróćmy do Django. QuerySets are lazy, czyli Django QuerySet nie są domyślnie, wykonywane od razu, ale leniwie, czyli w ostatnio możliwym momencie, a dokładnie w momencie kiedy dane z bazy danych będą potrzebne, np. w momencie wyświetlenia ich w szablonie. Jeszcze innym słowami - można to porównać do kupienia vouchera na piwo na Festiwalu Woodstock, kupujemy od razu, ale zostanie on wykorzystany, dopiero kiedy zostaniemy o niego zapytani przy wydawaniu złocistego napoju.
cities = City.objects.order_by("name")
Dla przykładu taka deklaracja, nie oznacza więc, że w zmiennej cities
mamy już nasze miasta, ale tylko oznacza, że pojawią się one tam, wtedy kiedy, np. będziemy chcieli je wyświetlić, albo 'przejść po nich' petlą cities_ids = [city.id for city in cities]
.
Lazy loading / evalution, to niezwykle potężne i wygodne narzędzie.
class Show(models.Model):
cabaret = models.ForeignKey(Cabaret, on_delete=models.CASCADE) # default -> null=False
city = models.ForeignKey(City, on_delete=models.SET_NULL, blank=True, null=True)
date = models.DateField(blank=True, null=True)
def __str__(self):
return "({}) ({}) ({})".format(self.cabaret.name, self.city, self.date)
Jak widać, metoda __str__()
używa wszystkich naszych trzech modeli. To znaczy, że jeśli zrobimy show = Show.objects.filter()
, a następnie wszystkie obiekty modelu Show pobierzemy z bazy i wyświetlimy, to one automatycznie pobiorą obiekty będące w relacjach z nimi, czyli odpowiednie miasto (City
) i odpowiedni kabaret (Cabaret
). To znacza, że w teorii nie musimy sobie zaprzątać tym głowy, po prostu wszystko jest połączone i wszystko się samo zrobi (pobierze, połączy i wyświetli). Prawda, że Magia? Z tą magią byw różnie...
Lazy loading / evalution, niesie też za sobą zagrożenie bezsensownego użycia zasobów, co często może wrócić do nas ze zdwojoną siłą, kiedy nasza aplikacja zacznie dziwnie zwalniać. Szczególnie łatwe to będzie do zaobserwowania, kiedy takich obiektów, gdzie niejako w utajony sposóby (bo korzystając z __str__()
pokazanego w kodzie powyżej nie musimy wiedzieć co się wykonuje jak robimy tylko proste {{show}} w szablonach), wyciągniemy z bazy tysiące. Wtedy może się okazać, że:
- do bazy danych poszedło jedno zapytanie o
Show
, - które zwróciło 1000 wyników,
- przy wyświetlaniu każdego z nich
Show
'poprosił' jeszcze oCity
, - potem jeszcze o
Cabaret
,
, także wyszło nam 2001 zapytań do bazy danych (1 do tabeli z występami, 1000 do tabeli z miastami i 1000 do tabeli z kabaretami). Każde zapytanie to strata czasu i zasobów serwera, a jeśli mamy dodatkowe 2000 zbędnych zapytań do bazy i nam to nie przeszkadza to znaczy, że mamy za szybki serwer po prostu ;).
Tak do końca magii nie ma, ale nie ma idealnych technologii, nie ma też sposobów, które w każdym przypadku mają tylko same zalety. Trzeba więc trzymać rękę na pulsie. W tym przypadku (lazdy evaluation) trzeba nauczyć się sprawdzania ile faktycznie zapytań jest wykonywanych do bazy danych.
Pomocne w tym okazuje się proste narzędzie, czyli dostarczanego nam wraz z Django django.db.connection
. Od razu trzeba napomknąć, że działa ono tylko kiedy settings.DEBUG = True
(w skrócie kiedy mamy żółte komunikaty o błędach), a nie powinien działać na tzw. produkcji (kiedy te błędy chcemy ukryć, wyświetlić ładniejszy komunikat), wtedy settings.DEBUG = False
.
Tak więc zaczynamy:
- importujemy
connection
zdjango.db
from django.db import connection
- a następnie, np. za pomocą prostego
raise Exception()
wyświetić:connection.queries
- czyli listę wszystki zapytań do bazy, które zostały wykonan przed wywołaniem naszegoraise Exception()
, lublen(connection.queries)
- czyli liczbę wszystkich wcześniejszych zapytań, lublen(connection.queries), connection.queries
razem, czyliraise Exception(len(connection.queries), connection.queries)
"cities": City.objects.order_by("name"),
"cabarets": Cabaret.objects.all(),
"shows": Show.objects.all(),
}
# raise Exception(len(connection.queries), connection.queries) # Exception no. 1
response = render(request, 'index.html', args)
raise Exception(len(connection.queries), connection.queries) # Exception no. 2
return response
Wyszło 23 zapytania (Exception no. 2), to nie dużo, ale gdyby występów byłoby faktycznie 1000 (tutaj tylko 4), to wynik byłby faktycznie spory. Na uwagę wskazuje też fakt, że (Exception no. 1) dał wynik 0, czyli przed wykonaniem render()
, żadne zapytanie nie zostało wykonane, a dopiero przy wyświetlaniu naszych danych w szablonie.
Dlatego z pomocą przychodzi nam .select_related()
, czyli łączenie tabel/modeli tak aby caly taki 'zestaw' został pobrany od razu, a nie 'dograny' dopiero kiedy zapytam o dane z połączonych tabel. Jako argument w .select_related()
podajemy pola łączące (te z ForeignKey()
), a także, jeśli potrzebujemy ich wewnętrzne połączenia, np. "cabaret__home_city"
oddzielone podwójnym podkreśleniem __
. Poniżej przykład:
W wersji z .select_related()
wyglądało to tak:
"citys": City.objects.order_by("name"),
"cabarets": Cabaret.objects.select_related("home_city").all(),
"shows": Show.objects.select_related("cabaret", "city", "cabaret__home_city").all(),
}
# raise Exception(len(connection.queries), connection.queries)
response = render(request, 'index.html', args)
raise Exception(len(connection.queries), connection.queries)
return response
Oto szybka statystyka dla obu przykładów:
x | bez select_related() |
z select_related() |
---|---|---|
przed render() | 0 /zapytania | 0 /zapytania |
po render() | 23 /zapytania | 5 /zapytania |
Dzień czwarty
Postanowiłem w końcu zagłębić się tematykę Formularzy. Najpierw na tapetę poszły Formularze HTML, by pokazać podstawy działania i ich budowy. Formularze Django zostawiliśmy sobie na później.
<form>
<label>Twój Login <input name="surname" type="text"></label>
<label>Twój email <input name="email" type="email"></label>
<label>Twoje hasło <input name="hasło" type="password"></label>
<input type="submit">
</form>
Omówiliśmy budowe formularza, a także jego domyślne atrybuty czyli action="<current_url>"
i method="get"
, gdzie action to ustawienie na jaki adres powinien zostać wysłany formularz, a method to ustawienie metody wysyłanie formularza (POST lub GET). Każdy z pokazanych input-ów miał inny typ. Zmyślne typy email i password sprawiały kolejno, że pola o takich typa miały: email - walidację adresu email i password - gwiazdy zamiast znaków przy wpisywniu hasła. Input o typie submit okazał się pokazywać jako przycisk, który też jest odpowiedzialny za wysyłkę formularza, w którym się znajduje.
Gdy już stworzyliśmy nasz formularz, zapragnęliśmy wysłać go gdzieś. Tak więc wysłaliśmy go do bieżącego widoku i wyrzuciliśmy sprawdzający Exception()
:
def cats(request):
if request.GET:
raise Exception(request.GET)
Dzień piąty
Ostatni dzień tygodnia nauki - powróciliśmy do formularzy. Plan z wczoraj był ambitny, ale jak się okazało na naukę mieliśmy w zasadzie niecałą godzinę, czyli tyle co nic. Na początek wzięliśmy na tapetę przyciski do wysyłania formularzy. Do juź działającego <input type="submit">
dołożylismy atrybut value=""
, którego wypełnienie jest równoznaczne ze zmianą domyślnego napisu na przycisku, na tą przez nas podaną - <input type="submit" value="Potwierdź">
. Przy okazji, do znanych już tagów HTML dodaliśmy <button>
. Omówiliśmy w zasadzie 3 warianty:
<input type="submit" value="Potwierdź"> <!-- wysyła formularz -->
<button> Click </button> <!-- wysyła formularz, tak samo jak input[type=submit], gdyż domyślnie, typem jest zawsze submit -->
<button type="submit"> Click </button> <!-- wysyła formularz, typ jest submit, więc już wszystko powinno byc jasne -->
<button type="reset"> Reset </button> <!-- usuwa wszytkie dane z fomularza -->
<button type="button"> Show all input</button> <!-- nie wysyła formularza, jest używany m.in. do tego by uruchomić kod javascript po kliknięciu -->
Przy okazji tego ostatniego miałem okazję pokazać mojej rekrutce trochę magii, czyli zmiany HTML-a za pomocą JavaScript / jQuery.
<button type="button" onclick="jQuery('input').hide()"> Hide all inputs</button>
<button type="button" onclick="jQuery('input:eq( 1 )').hide(); alert('STOP'); console.log('START')"> Hide second input</button>
<button type="button" onclick="jQuery('input').show()"> Show all inputs</button>
Do naszego <button type="button">
dodaliśmy jeden z dostępnych eventów, czyli atrybut onclick. On - Click, czyli jeśli użytkownik naciśnie przycisk, wykonaj jakiś kod/polecenie JavaScript. Do przykładu użyłem frameworka jQuery:
jQuery(
- zainicjowanie użycia biblioteki jQueryjQuery('input')
- znalezienie wszystkich elementów typu input na stronie- ```jQuery('input').hide() - użycie .hide() na znalezionych elementach, który jak sama nazwa wskazuje, ukrywa nam znalezione wcześniej elementy typu input.
Potem w następnym przycisku, zamienilismy .hide() na .show() i już mieliśmy przycisk przywracający widoczność wszystkich input-ów na stronie. Jako bonus omówiłem:
onclick="jQuery('input:eq( 1 )').hide(); alert('STOP'); console.log('START')"
czyli wykonanie kolejno trzech komend: 1) znalezienie drugiego input-a na stronie i ukrycie go, 2) wyświetlenie tzw. alertu na stronie z komunikatem STOP, 3) wyświetlenie w konsoli JavaScript komunikatu START.
Następnie do portfolio tagów związanych z HTML-em dodaliśmy <textarea rows="4" cols="500"></textarea>
, który umożliwie wygodne wprowadzanie do formularza długich tekstów. Wielkość nominalna textarea ustawiliśmy na rows="4" cols="500">
. Na wcześniejszy tagach pokazałem, że możemy za pomocą atrybutu value, ustawić wartość początkową dla danego elementu formularza <input value="Kowalski" name="surname" type="text">
, a za pomocą atrybutu placeholder, możemy ustawić tekst pomocniczy <input placeholder="Twój login" name="surname" type="text">
, tak aby pokazywał się, przed wpisaniem, a który znika jak tylko zaczniemy wypełniać pole dla którego został zdefiniowany.
Próbowałem także pokazać czym różni się metoda wysyłania GET od metody POST. Niestety przy omawianiu tagu {% csrf_token %}
, z niewiadomych przyczyn, nie udało się wygenerować tokenu csrf. Później okazało się, że użycie render()
zamiast render_to_response()
załatwiało sprawę.
Podsumowanie
To nie był imponujący tydzień nauki, głównie z powodu braku czasu. Zaledwie 5 dni nauki, większość po godzinie, to raczej słabo. Niemniej jednak, w końcu poruszyliśmy dwa bardzo ważne zagadnienia, czyli Modele/Queryset/Bazy Danych, a także Formularze HTML. Następny tydzień zaczniemy Formularzami Django.