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ů)
- Django: Úvod a instalace 14. 8. 2009
- Django: Nastavení projektu a první pokusy 21. 8. 2009
- Django: Databázový model 28. 8. 2009
- Django: Databázový model podruhé 4. 9. 2009
- Django: Administrace 11. 9. 2009
- Django: Prezentace dat 18. 9. 2009
- Django: Prezentace dat podruhé 25. 9. 2009
- Django: Zpracovávání formulářů 2. 10. 2009
- Django: Autentizace a autorizace 9. 10. 2009
- Django: Nahrávání souborů 16. 10. 2009
- Django: Zabudované aplikace 23. 10. 2009
- Django: Rozšiřování možností Djanga 30. 10. 2009
- Django: Internacionalizace 6. 11. 2009
- Django: Nasazování projektu 13. 11. 2009
- Django: Kešování a škálování 20. 11. 2009
- Django: Závěr 27. 11. 2009
Nálepky:
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 tabulkyfilter
: omezuje výběr podle jednoho nebo více parametrůexclude
: vybere doplněk toho, co by vybrala metodafilter
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.
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
- Modely a tutoriál na Djangoproject.com
- Tutoriál na Djangoproject.cz
- Pátá kapitola v The Definitive Guide to Django
- Ukázkový příklad ke stažení.
Příště si z našich databázových modelů necháme vygenerovat rozhraní pro jednoduchou správu projektu.
Ke zjistovani poctu zaznamu je nevhodne pouzivat funkci len – stahne vsechny zaznamy z DB a spocita je misto toho aby se proste DB zeptala na pocet (SELECT COUNT(*) …) jako do tela metoda .count()
Misto .filter()[0] je lepsi vetsinou pouzit .get(), ma tu vyhodu ze sam hlida ze DB ma vratit prave jeden objekt a neni tak krypticky.
Mozna by take stalo za to zminit lazy vlastnost querysetu a jak se daji querysety za sebou retezit, tedy ze muzu udelat
A ve vysledku se udela jen jeden dotaz do DB. Obecne dotazy do DB se provedou pri iterovani pres queryset, sliceovani ([]) a volani .get ci .count. Specialni pripad je v ramci prace v interaktivnim rezimu, ktery vzdy vola repr() na vracene hodnote aby ho zobrazil uzivateli – repr() u querysetu vyvola iteraci a dotaz se tak provede.
Jinak dekuji za clanky, myslim si, ze je neco takoveho potreba :).
Díky za hodnotné připomínky, get jsem tam stručně zmínil, lenost a řetězení taky. S tím countem máte naprostou pravdu, je to efektivnější a neměl jsem o tom ani tušení. Django mě neustále překvapuje novými funkcemi…
No, ten count() neni ani tak funkce Djanga, ale vychazi z vlastnosti SQL databaze. Navic tyto ‚nove funkce‘ jsou zminene i v originalni dokumentaci, takze to by Vas prilis prekvapovat nemelo – narazim hlavne na ‚.get()‘ metodu, ktera se od Vami popsanych vyberovych kriterii atributu object lisi jen tim, ze nevraci Queryset, ale primo vlastni objekt.
Ano, samozřejmě, databázovou klauzuli COUNT znám, jenom jsem nevěděl, že to má Django implementováno jako zvláštní metodu – příště musím lépe nastudovat manuál. A metodu get jsem tam zmínil a přiložil na ní odkaz, i když jsem ji v rámci zpřehlednění nepoužil, rozhodně s ní však budu pracovat v nějakém dalším díle.
neviete ako django robi limit napr. na DB oracle, alebo inych, ktore pojem limit nepoznaju?
V tomto pripade django ide standardnou cestou cez vlozeny select a rownum, zbytok podobne ako u limit…
Kedze sa v clanku v hodnej miere spominaju metody QuerySet, a metoda filter (dokonca viac ako by mala), mozno stalo zato spomenut aj Q object.
Máte pravdu, Q objekt je hodně užitečná věc. Bohužel se to sem už nevešlo, databázový model Djanga je natolik komplexní, že by se jenom tomuto tématu dal věnovat celý seriál. Lepší je popsat jeden způsob pořádně než více povrchně.
Ja bych si jenom dovolil doplnit, ze vygenerovane dotazy se do connection.queries ukladaji jenom pokud je v settings.py nastaveno DEBUG = True (a je to mimochodem hlavni duvod, proc si zacatecnici stezuji na memory leak v Djangu).
Pokud chceme zjistit SQL, je jednodussi na QuerySet objektu pouzit .query.as_sql()
store_brno = Store.objects.filter(id=3)[0]
print store_brno.query.as_sql()
Snad to nekomu pomuze.
Jirka
Ehm, v tom prikladu mam samozrejme chybu, protoze store_brno neni QuerySet. Predstavte si, ze na za filter(…) neni ono [0] :-)
Ja jeste doplnim: nespolehejte na tohle. Je to tvar SQL __PRED__ expanzi a escapovanim promennych, casto to ani neni validni SQL. Pokud chcete 100% jistotu, pouzijte query log ve vasi DB.
Pro měření a inspekci skutečných dotazů je výborný django debug toolbar (http://github.com/…/tree/master), umí třeba zobrazit odkud z kódu queries lezou a další informace. Debug toolbar vyžaduje django >= 1.0.
Jinak pozor na fakt, že čas reportovaný djangem u query je skutečně jenom cena DB query a nepočítá se do toho overhead djanga, který může být klidně jednou tolik, viz následovní screenshot z kcachegrindu:
http://imgur.com/JMHSE.png
Taky šablonovací systém djanga rozhodně nepatří mezi nejrychlejší, i s překompilovanou šablonou to trvá celkem dlouho. Což se dá obejít cachováním a/nebo použitím jiného šablonovacího systému.
Zdravim,
zaujimalo by ma ako v django orm riesit eager loading, resp. N+1 query problem. select_related() pouzivam, ale potreboval by som to opacne. (Presiel som na django z Rails, kde to bola bezna vec). Vdaka :)
Jozef
RE opačně:
To jako že chceš zjistit, které záznamy v jedné tabulce mají foreign-key na nějakou hodnotu v druhé?
Navím jak to řeší Rails, ale předpokládám že na to pod kapotou používají některý z joinů.
Zkus mrknout sem http://www.djangoproject.com/…many_to_one/, třeba ti to pomůže.
Diky za clanok, mam otazku:
>>> Store.objects.exclude(city=‚Brno‘).filter(id__gt=0, id__lt=10).order_by(‚-city‘)
specificky mi nie je jasne:order_by(‚-city‘)
Preco je tam znamienko ‚-‘ ? Videl som to uz aj v off. dokumentacii/tutorialoch,
ale nikde som nenasiel vysvetlenie preco to znamienko tam je ?
Diky
„Když potřebujeme položky seřadit obráceně, napíšeme znak mínus před název atributu.“
Tedy jedná se o řazení sestupně, ne vzestupně (od největšího po nejmenší, ne naopak).
Som idiot, ta poznamka v clanku mi fakt unikla.
Diky a prajem hodne zdaru pri dalsich castiach serialu.
Ahoj, diky za skveli serial, btw Kounicova 15?