Přejít k navigační liště

Zdroják » Různé » Django: Databázový model podruhé

Django: Databázový model podruhé

Články Různé

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.

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.

Komentáře

Subscribe
Upozornit na
guest
18 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Honza Kral

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

>>> q = Store.objects.all()
>>> q = q.filter(id__lte=10)
>>> q = q.exclude(id=1)
>>> q = q.order_by('-id')
>>> stores = q[:2]

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

Tomaasch

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.

jjjjj

neviete ako django robi limit napr. na DB oracle, alebo inych, ktore pojem limit nepoznaju?

pcicman

V tomto pripade django ide standardnou cestou cez vlozeny select a rownum, zbytok podobne ako u limit…

pcicman

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.

Jirka Vejrazka

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.qu­ery.as_sql()

Snad to nekomu pomuze.

Jirka

Jirka Vejrazka

Ehm, v tom prikladu mam samozrejme chybu, protoze store_brno neni QuerySet. Predstavte si, ze na za filter(…) neni ono [0] :-)

Honza Kral

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.

limit_false

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.

Jozef

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

Mintaka

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.

goliash

Diky za clanok, mam otazku:
>>> Store.objects­.exclude(city=‚Brno‘)­.filter(id__gt=0, id__lt=10).or­der_by(‚-city‘)

specificky mi nie je jasne:order_by(‚-city‘)

Preco je tam znamienko ‚-‘ ? Videl som to uz aj v off. dokumentacii/tu­torialoch,
ale nikde som nenasiel vysvetlenie preco to znamienko tam je ?

Diky

goliash

Som idiot, ta poznamka v clanku mi fakt unikla.
Diky a prajem hodne zdaru pri dalsich castiach serialu.

Frantisek S.

Ahoj, diky za skveli serial, btw Kounicova 15?

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.