Django: Databázový model podruhé

V minulém díle jsme se naučili ukládat záznamy do databáze, dnes se je naučíme odtamtud vybírat, upravovat a mazat. Rovněž si ukážeme vazby mezi tabulkami a několik tipů, týkajících se databázového modelu Djanga.

Seriál: Hrajeme si s Djangem (16 dílů)

  1. Django: Úvod a instalace 14.8.2009
  2. Django: Nastavení projektu a první pokusy 21.8.2009
  3. Django: Databázový model 28.8.2009
  4. Django: Databázový model podruhé 4.9.2009
  5. Django: Administrace 11.9.2009
  6. Django: Prezentace dat 18.9.2009
  7. Django: Prezentace dat podruhé 25.9.2009
  8. Django: Zpracovávání formulářů 2.10.2009
  9. Django: Autentizace a autorizace 9.10.2009
  10. Django: Nahrávání souborů 16.10.2009
  11. Django: Zabudované aplikace 23.10.2009
  12. Django: Rozšiřování možností Djanga 30.10.2009
  13. Django: Internacionalizace 6.11.2009
  14. Django: Nasazování projektu 13.11.2009
  15. Django: Kešování a škálování 20.11.2009
  16. Django: Závěr 27.11.2009

Vybírání dat z tabulky

Naše databáze by měla obsahovat z minula několik záznamů, které si odtamtud zkusíme získat. To se dělá pomocí kolekce objektů QuerySet. Výstupem této třídy je instance objektu, která se  chová podobně jako seznam. Metody výběru dat se volají přes atribut objects a dají se kombinovat. K nejpoužívaněj­ším patří:

  • all: vybere všechny záznamy z tabulky
  • filter: omezuje výběr podle jednoho nebo více parametrů
  • exclude: vybere doplněk toho, co by vybrala metoda  filter
  • order_by: řazení výběru podle určitého atributu

Nejjednodušší je výběr všech záznamů:

>>> from hrajeme_si.video_store.models import Store
>>> Store.objects.all()
[<Store: Videostore Praha 1>, <Store: Videostore Praha 2>, <Store: Videostore Brno>]

Omezování záznamů pomocí metod filter a exclude se převádí na SQL klauzuli WHERE, respektive WHERE NOT. Při zadání více parametrů se jednotlivá omezení řetězí pomocí logické spojky AND. Za názvem atributu může následovat dvojité podtržítko spolu s nějakým specifikátorem omezení, který magicky zadává klíčové argumenty WHERE klauzule. Ukážeme si to na několika komentovaných příkladech (mřížka # uvozuje komentář):

>>> Store.objects.filter(id=1) # záznam s id = 1 (odpovídá id__exact=1)
[<Store: Videostore Praha 1>]
>>> Store.objects.filter(id__gt=1) # záznamy s id > 1
[<Store: Videostore Praha 2>, <Store: Videostore Brno>]
>>> Store.objects.filter(id__gte=1) # záznamy s id ≥ 1
[<Store: Videostore Praha 1>, <Store: Videostore Praha 2>, <Store: Videostore Brno>]
>>> Store.objects.filter(city='Praha') # město odpovídá přesně řetězci 'Praha'
[]
>>> Store.objects.filter(city__startswith='Praha') # město začíná řetězcem 'Praha'
[<Store: Videostore Praha 1>, <Store: Videostore Praha 2>]
>>> Store.objects.filter(city__istartswith='prAhA') # to stejné, jenom ignorujeme velikost písmen
[<Store: Videostore Praha 1>, <Store: Videostore Praha 2>]
>>> Store.objects.filter(address__icontains='náměstí') # pobočka sídlí na náměstí
[<Store: Videostore Brno>]
>>> Store.objects.filter(email='') # záznamy s nevyplněným e-mailem
[<Store: Videostore Praha 2>, <Store: Videostore Brno>]
>>> Store.objects.exclude(email='') # záznamy s vyplněným e-mailem
[<Store: Videostore Praha 1>]

Záznamy lze řadit podle jednoho či více atributů, výběr se implicitně řadí od prvního záznamu po poslední, stejně jako u SQL klauzule ORDER BY. Když potřebujeme položky seřadit obráceně, napíšeme znak mínus před název atributu. Není potřeba nejprve volat metodu all, stačí zavolat order_by rovnou:

>>> Store.objects.order_by('id') # řazení dle sloupce id vzestupně
[<Store: Videostore Praha 1>, <Store: Videostore Praha 2>, <Store: Videostore Brno>]
>>> Store.objects.order_by('-id') # řazení dle sloupce id sestupně
[<Store: Videostore Brno>, <Store: Videostore Praha 2>, <Store: Videostore Praha 1>]
>>> Store.objects.order_by('?') # náhodné řazení
[<Store: Videostore Praha 1>, <Store: Videostore Brno>, <Store: Videostore Praha 2>]

Kombinování metod výběru vypadá například takhle:

>>> Store.objects.exclude(city='Brno').filter(id__gt=0, id__lt=10).order_by('-city')
[<Store: Videostore Praha 2>, <Store: Videostore Praha 1>]

Tento příkaz vybere všechny pobočky, které nejsou v Brně, mají id větší než nula a menší než deset a seřadí je podle města, sestupně. Je dobré vědět, že se QuerySet chová líně, tedy že se SQL dotaz zavolá, až když data v aplikaci skutečně potřebujeme. Můžeme tak SQL dotaz postupně sestavovat, aniž by došlo k výraznému zpomalení.

Limitování a zjišťování počtů záznamů

Když už jsme si ukázali ekvivalenty klauzulí WHERE a ORDER BY, přidáme k tomu ještě klauzuli LIMIT. Ta má stejnou syntaxi jako indexování seznamů, protože se instance objektu QuerySet chová podobně jako pythonový seznam. Pomocí ní vybíráme podmnožinu z našich záznamů. Nejlépe to ilustrujeme na několika příkladech:

>>> Store.objects.order_by('id')[0] # první záznam v tabulce
<Store: Videostore Praha 1>
>>> Store.objects.order_by('-id')[0] # poslední záznam v tabulce
<Store: Videostore Brno>
>>> Store.objects.order_by('id')[:2] # první dva záznamy z tabulky
[<Store: Videostore Praha 1>, <Store: Videostore Praha 2>]
>>> Store.objects.order_by('-id')[:2] # poslední dva záznamy z tabulky
[<Store: Videostore Brno>, <Store: Videostore Praha 2>]
>>> Store.objects.exclude(city='Brno')[1] # druhý pobočka, která není v Brně
<Store: Videostore Praha 2>

Stejně jako u seznamů je potřeba si dát pozor na indexování mimo rozsah, protože pak je vyhozena výjimka IndexError, kterou je potřeba odchytit a zpracovat. To se naučíme v dalších dílech. Vyhození této výjimky nastává například při vybrání nulového počtu záznamů:

>>> Store.objects.filter(city='Ostrava')[0]
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/lib/python2.5/site-packages/django/db/models/query.py", line 159, in __getitem__
    return list(qs)[0]
IndexError: list index out of range

A jak zjistit počet záznamů? Stačí použít zabudovanou funkci  len:

>>> len(Store.objects.all())
3
>>> len(Store.objects.filter(city='Brno'))
1
>>> len(Store.objects.filter(city='Ostrava'))

Upravování a mazání dat

Záznamy, které z databáze vybereme, jsou opět instancí našeho modelu Store. Ten jsme minule použili k přidání záznamů do tabulky. Podobně se záznamy dají upravovat a mazat. Vyzkoušíme si přestěhovat brněnskou pobočku:

>>> store_brno = Store.objects.filter(id=3)[0]
>>> store_brno.address = 'Kounicova 15'
>>> store_brno.postal_code = '611 00'
>>> store_brno.save()

První příkaz nám vybere záznam s číslem 3, tedy naší brněnskou pobočku, a přiřadí ji do proměnné store_brno. Kratšího zápisu lze docílit i pomocí metody get, která se ovšem chová trochu jinak. Na dalších třech řádcích můžeme vidět, že došlo k upravení atributů a uložení záznamu do tabulky. Podobně funguje i mazání, jenom místo metody save zavoláme metodu  delete:

>>> store_praha2 = Store.objects.filter(id=2)[0]
>>> store_praha2.delete()

Tabulka teď obsahuje jenom dva záznamy, protože jsme druhou pražskou pobočku zrušili:

>>> Store.objects.all()
[<Store: Videostore Praha 1>, <Store: Videostore Brno>]

Ladění SQL dotazů

Často je užitečné vědět, jaký SQL dotaz ORM transformace v Djangu vyprodukovala a jak dlouho jeho vykonání trvalo. Tyto informace je průběžně ukládají do seznamu queries z objektu django.db.connection. Takto vypadá jednoduché použití:

>>> from django.db import connection
>>> Store.objects.exclude(id=1).order_by('city')[0]
<Store: Videostore Brno>
>>> connection.queries[-1]
{'time': '0.038', 'sql': u'SELECT "video_store_store"."id", "video_store_store"."store",
"video_store_store"."address", "video_store_store"."city", "video_store_store"."postal_code",
"video_store_store"."email", "video_store_store"."description" FROM "video_store_store"
WHERE NOT ("video_store_store"."id" = 1 ) ORDER BY "video_store_store"."city" ASC LIMIT 1'}

Tento dotaz trval 38 setin sekundy a byl při něm použit uvedený SQL kód.

Vztahy mezi modely

Rozšíříme si funkcionalitu naší aplikace přidáním modelu představujícího položku v katalogu filmů videopůjčovny. Otevřeme si soubor video_store/models.py a přidáme na jeho konec definice modelů. Tentokrát to bude o něco složitější:

FORMAT_CHOICES = (
    (0, 'Ostatní'),
    (1, 'VHS'),
    (2, 'DVD'),
    (3, 'Blu-ray'),
)

class Film(models.Model):
    store = models.ManyToManyField('Store')
    name_czech = models.CharField('Český název filmu', max_length=100)
    name_original = models.CharField('Původní název filmu', max_length=100, blank=True)
    year = models.PositiveIntegerField('Rok natočení')
    director = models.ForeignKey('Director', verbose_name='Režisér', null=True, blank=True)
    price = models.DecimalField('Cena za půjčení', max_digits=4, decimal_places=2, default=20)
    format = models.PositiveSmallIntegerField('Formát', choices=FORMAT_CHOICES)
    description = models.TextField('Popis', blank=True)
    added = models.DateTimeField('Čas přidání', auto_now_add=True)

    def __unicode__(self):
        return self.name_czech

    class Meta:
        ordering = ['name_czech']
        verbose_name = 'film'
        verbose_name_plural = 'filmy'

class Director(models.Model):
    name = models.CharField('Jméno a příjmení', max_length=100)

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = 'režisér'
        verbose_name_plural = 'režiséři'

N-tice FORMAT_CHOICES je pro výběr formátu filmu v modelu Film. Ten je provázan přes cizí klíč relací 1:N (one-to-many) s pomocným modelem Director, který obsahuje režiséry. Vytváříme ho proto, abychom mohli jednoduše zobrazovat související filmy od toho stejného režiséra. Pro zjednodušení může být u každého filmu uveden nanejvýše jeden režisér. Tabulky Store a Film jsou propojené relací M:N, často nazývanou many-to-many. Každý film se může vyskytovat ve více videopůjčovnách a každá videopůjčovna zpravidla obsahuje více než jeden film. Pro lepší pochopení vztahů je dobré prostudovat přiložený diagram.

Django - model

Z atributů tady máme několik nových polí: PositiveIntegerField a PositiveSmallIntegerField se používají pro ukládání přirozených čísel, liší se jenom v maximální hodnotě, která se do nich dá uložit. Pole DecimalField je určeno pro desetinná čísla (je potřeba nastavit maximální počet číslic a počet desetinných míst). Další pole DateTimeField slouží k ukládání času a data, použitý parametr auto_now_add nám usnadňuje práci, protože nastaví automaticky aktuální čas při založení. Parametr default nastavuje výchozí hodnotu atributu price. Za zmínku stojí i meta vlastnost ordering, která nastavuje výchozí řazení při výběru dat.

A na závěr si opět synchronizujeme modely s databází, abychom je měli nachystané pro další pokračování:

$ python manage.py syncdb
Creating table video_store_film
Creating table video_store_director
Installing index for video_store.Film model 

Související odkazy

Příště si z našich databázových modelů necháme vygenerovat rozhraní pro jednoduchou správu projektu.

Autor je dlouhodobým studentem Fakulty informatiky, webový nadšenec a programátor — nejraději programuje v jazycích Haskell a Python.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 18

Přehled komentářů

Honza Kral len vs .count() a dalsi drobnosti
Pavel Dvořák Re: len vs .count() a dalsi drobnosti
Tomaasch Re: len vs .count() a dalsi drobnosti
Pavel Dvořák Re: len vs .count() a dalsi drobnosti
jjjjj limit
pcicman Re: limit
pcicman Q object?
Pavel Dvořák Re: Q object?
Jirka Vejrazka Zjistovani vygenerovaneho SQL
Jirka Vejrazka Re: Zjistovani vygenerovaneho SQL
Honza Kral Re: Zjistovani vygenerovaneho SQL
limit_false debug toolbar, performance
Jozef Eager loading / N+1 query problem
Mintaka Re: Eager loading / N+1 query problem
goliash order_by syntax
Pavel Dvořák Re: order_by syntax
goliash Re: order_by syntax
Frantisek S. Kounicova 15?
Zdroj: https://www.zdrojak.cz/?p=3080