Django

Query

Django - Dotazy (Query)

Query je databázový nástroj Djanga (API), umožňující pracovat s Modely. Přidávat, měnit a mazat data. Vytvářet dotazy nad existujícími daty.

Třída modelu představuje databázovou tabulku, instance modelu pak jednotlivý záznam (řádek) v databázové tabulce.

Udělejme si zase nějaké modely a prvotní data:

 class Classroom(models.Model):
     name = models.CharField(max_length=10)

     def __unicode__(self):
         return self.name

 class Student(models.Model):
     name = models.CharField(max_length=60)
     classroom = models.ForeignKey('Classroom')
     height = models.IntegerField()
     weight = models.IntegerField()

     def __unicode__(self):
         return self.name

     def bmi(self):
         return float(self.height) / float(self.weight)

Máme tedy školní třídy, které se nějak jmenují a máme studenty. Každý student chodí do jedné třídy a jedna třída má více studentů (1:N - ForeignKey). A budeme ještě sledovat kolik měří a váží a jaký mají BMI, ať máme i něco číselného do agregačních příkladů.

1.  Vytvoření a úprava záznamu

 from .models import Classroom
 c = Classroom(name="6.B")
 c.save()

Vyvolá databázový příkaz INSERT.

 c.name = "6.C"
 c.save()

Vyvolá databázový příkas UPDATE - provádíme změnu v již existujícím záznamu.

1.1  Vkládání klíčů do záznamu

 from .models import Student
 s = Student()
 s.name = "John Lonel"
 s.classroom = c
 s.height=166
 s.weight=37
 s.save()

Do údaje klíče vkládáme instanci spřaženého modelu. V kapitole o modelech jsem se tomu už věnoval dost. Takže: U ManyToMany máme dvě situace: Pokud je zprostředkující tabulka vytvořena automaticky, přidáváme záznamy pomocí metody .add(). Používáme-li vlastní zprostředkující tabulku, musíme záznamy přidávat přímo do ní.

Co tedy nově vytvořený student nabízí za data:

 >>> s.name
 u'John Lonel'
 >>> s.classroom
 <Classroom: 6.C>
 >>> s.classroom.name
 u'6.C'
 >>> s.bmi()
 4.486486486486487

Pomocí tečkové notace se můžeme dostat k údajům nadřízeného modelu. Zde jméno třídy (classroom.name).

Nyní slíbená úvodní data:

 c6a = Classroom.objects.create(name='6.A')
 c6b = Classroom.objects.create(name='6.B')

 s6a1 = Student.objects.create(name='Anthon Black', classroom=c6a, weight=45, height=173)
 s6a2 = Student.objects.create(name='Anetha Blue', classroom=c6a, weight=39, height=158)
 s6a3 = Student.objects.create(name='George Green', classroom=c6a, weight=51, height=159)
 s6a4 = Student.objects.create(name='Judith Transparent', classroom=c6a, weight=52, height=171)
 s6a5 = Student.objects.create(name='Viola Livonec', classroom=c6a, weight=47, height=164)
 s6a6 = Student.objects.create(name='Martin Lobels', classroom=c6a, weight=39, height=161)

 s6b1 = Student.objects.create(name='Gustavo Bone', classroom=c6b, weight=27, height=129)
 s6b2 = Student.objects.create(name='Illiane Slewere', classroom=c6b, weight=63, height=137)
 s6b3 = Student.objects.create(name='Method Bianco', classroom=c6b, weight=54, height=159)
 s6b4 = Student.objects.create(name='Robert Blue', classroom=c6b, weight=39, height=164)
 s6b5 = Student.objects.create(name='Maria Blue', classroom=c6b, weight=44, height=164)

2.  Získávání dat - dotazy

Django ke každému modelu vytvoří automaticky "Manager" s názvem "objects", který poskytuje metody pro práci s daty. Pár základních:

all
Načte všechna data
filter
Načte vybraná data (to bude ještě srandy)
exclude
Jako filter, ale opačná data
get
Načte jeden konkrétní záznam (je-li jich více, nebo žádný, skončí to chybou)
order_by
Jako all, ale můžeme zadat řadící kriterium.
create
Pro vytvoření záznamu v databázi.

Poslední "create" není pro načítaní, ale mám ho tu pro úplnost - je to taky metoda manageru.

Ještě je potřeba si dávat pozor na to, že některé metody vracejí sadu záznamů (tzv. QuerySet) a některé jen jediný záznam. Z výše uvedených je to "get", co vrací jediný záznam. Takže pozor, není pak například možné dělat slice [0]. QuerySet se chová jako seznam (list).

Příklady nade vše:

 Classroom.objects.all()
 [<Classroom: 6.C>, <Classroom: 6.A>, <Classroom: 6.B>]

 Student.objects.all()
 [<Student: John Lonel>, ...

Metoda all tedy odpovídá databázovému SELECT * FROM Model.

 Student.objects.filter(height__gt=170)

Použití metody filter umožňuje vybrat jen některé záznamy. Odpovídá parametrům WHERE a LIMIT příkazu SELECT. Vypsali jsme si studenty jejichž výška je vyšší než 170cm.

Zápis je na první pohled krkolomný, ale dá se zvyknout. Vždycky je tam znak rovná se. Vždycky. A každý typ údaje má svoje odpovídající operátory. Height je číselný, takže číselné operátory. Name je znakové, takže operátory má znakové. Operátor od názvu údaje oddělujeme dvěma podtržítky.

 Student.objects.filter(name__startswith='J')
 [<Student: John Lonel>, <Student: Judith Transparent>]

 Student.objects.filter(name__istartswith='j')
 [<Student: John Lonel>, <Student: Judith Transparent>]

Nemá smysl tu opisovat dokumentaci. Operátorů je spousta.

Filtry můžeme kombinovat dvěma způsoby:

 Student.objects.filter(name__istartswith='j', height__gt=168)
 [<Student: Judith Transparent>]

 Student.objects.filter(name__istartswith='j').filter(height__gt=168)
 [<Student: Judith Transparent>]

Tedy student, jehož jméno začíná na "j" (bez rozlišení velikosti písmene) a současně je vyšší než 168cm. První způsob vloží logický operátor AND, druhý je vlastně zřetězením filtrů - výsledkem je ale taky AND. Pokud chceme operátor OR, nebo NOT, musíme použít Q object - viz dále.

Odpovědi si můžeme nechat dávat postupně a dílčí odpovědi si ukládat. Každá odpověď je opět QuerySet a tedy má všechny metody manageru:

 sa = Student.objects.all()
 sjt = sa.filter(height__gt=168)
 sjl = sa.filter(height__lte=140)
 sjm = sa.filter(height__gt=140, height__lte=168)
 sa
 [<Student: John Lonel>, ... všichni
 sjt
 [<Student: Anthon Black>, <Student: Judith Transparent>]
 sjl
 [<Student: Gustavo Bone>, <Student: Illiane Slewere>]
 sjm
 [<Student: John Lonel>, ... taky hodně :)

A víme, kteří studenti jsou dlouháni, kteří prckové, a kteří akorát.

Co se stane, když úvodní filr změníme? Co nám ty další filtry dají za výsledky?

 sa = Student.objects.filter(classroom=c6a)
 sjm
 (stejné jako prve, vypíše středně velké studenty ze všech tříd). Pro kontrolu:
 sjm.filter(classroom=c6a)
 (teď už opravdu jen ty ze 6.a)

Z toho vyplývá, že QuerySet vytvořený postupným filtrováním si pamatuje celou svoji "cestu", a že dodatečná změna "mezifiltru" už ho neovlivní.

Ještě příklad na operátor "in":

 c6c = Classroom.objects.get(name__exact='6.C')
 sa.filter(classroom__in=[c6b, c6c])

Zdvojená podtržítka ve filtrech fungují jako tečková notace:

 sa.filter(classroom__name__contains='.B')

Vypíše všechny studenty, kteří chodí do libovolného béčka (vhodné pro přípravu školní bitvy).

Přes dvojpodtržítka se dá bloudit napříč klíči provázanými modely do alelůja.

3.  Pohled směrem "dolů"

Ve vztahu 1:N, tedy v našem případě třída:student máme u každého studenta jednu třídu. Jak je to ale obrácene? Záznam na straně "1" nutně vrací celou sadu záznamů ze strany "N". A tak je to i pojmenované (model_set) (set je anglicky sada):

 c = Classroom.objects.get(name__exact='6.B')
 c.student_set
 <django.db.models.fields.related.RelatedManager object at 0x35dac10>

Zatímco instancí studentů jsme měli vždy jedinečný údaj "classroom", u instancí školních tříd máme "student_set" - tedy celou sadu záznamů. Jedná se manager, tudíž obsahuje všechny metody manageru:

 c.student_set.all()
 c.student_set.filter(name__icontains='blue')

Druhý příkaz vypíše studenty, kteří mají ve jméně slovo "blue".

A kdo to chce podle abecedy, udělá si mašinku:

 sa.filter(classroom__name__contains='.B').order_by('name')

4.  Kruťácké dotazy, aneb mistrem, nebo šílencem

4.1  Vypsat třídy s dlouhánama

Můžeme vypsat třídy, které obsahují studenty vyšší než 170 cm? Asi musíme začít modelem Classroom, že? Jenže jak budeme filtrovat sadu student_set? Zkusíme to. Nejdřív si zjistím, kteří studenti toto kriterium splňují:

 s170 = Student.objects.filter(height__gt=170)
 s170
 [<Student: Anthon Black>, <Student: Judith Transparent>]

Ale do jakých chodí tříd? Pomůže trocha Pythonovského kódu:

 for st in s170:
     print(st.name, st.classroom.name, st.height)

 (u'Anthon Black', u'6.A', 173)
 (u'Judith Transparent', u'6.A', 171)

A nebo použijeme správnou metodu Djanga:

 s170.values('name', 'classroom')
 s170.values_list()

Tak je máme. Oba chodí do 6.A. Způsob s metodou "values" a "values_list" bohužel vrací Id třídy, ne její název.

Budeme pátrat ve všech třídách:

 c = Classroom.objects.all()

A budeme se pídit po studentech se správnou výškou:

 c.filter(student__height__gt=170)
 [<Classroom: 6.A>, <Classroom: 6.A>]

Co že nám to vrátilo? No prostě třídu pro každého studenta, kterého se to týkalo. Měli by to být Anthon s Judith, ale kdo ví...? ;)

Když už máme správnou třídu, stačilo by nám to říct jen jednou, ne tolikrát, kolik je studentů splňujících kriterium, že? Vzpomínáte si na SELECT DISTINCT? Django to umí taky:

 c.filter(student__height__gt=170).distinct()
 [<Classroom: 6.A>]

Zkusme to s více studenty, ať je to zábavnější. Dáme 150cm:

 c.filter(student__height__gt=150)
 c.filter(student__height__gt=150).distinct()

Funguje? Funguje. Jen nám to vrátilo všechny třídy. Zkusíme tedy něco "mezi":

 c.filter(student__name__icontains='blue')
 c.filter(student__name__icontains='blue').distinct()

Studenti s příjmením "Blue" chodí jen do A a B.

Ještě možná otázka, proč tu píšeme "student" a ne "student_set"? Protože to jsou dvě zcela jiné situace. Zápis "_set" se vztahuje k instanci nadřízené tabulky. Tedy "c6a.student_set...". Tady jsme ale uvnitř filtru, takže použijeme název údaje jak je. Jinak řečeno, za "student_set" bude následovat filtr "trida.student_set.filter(...)", tedy to je to co se bude filtrovat. (no hlavně se nezamotat do vysvětlování).

5.  Limit, offset

Jsou pojmy z SQL. V Djangu se do dělá pomocí pythonovského plátkování. Limit:

 Student.objects.order_by('name')[:5]

Offset - počínaje třetím:

 Student.objects.order_by('name')[2:5]

Je možné i použít krok (každý druhý:

  Student.objects.order_by('name')[:8:2]
  Student.objects.order_by('name')[::2]

Ale nejde použít záporný index:

 # Tohle nejde:
 Student.objects.order_by('name')[-2]

6.  F()

Normálně query porovnává hodnotu pole s konstatntou. F() umožní porovnat hodnoty dvou polí mezi sebou:

 from django.db.models import F
 Entry.objects.filter(n_comments__gt=F('n_pingbacks'))

Při aktualizaci údajů v databázi brání F případům race conditions tím, že nechá operaci update na databázi místo, aby probíhala na úrovni aplikace a umožnila tak, že údaj mezi tím změní někdo jiný. Například:

 # problemovy pripad:
 p = Product.objects.get(pk=710)
 p.items += 1
 p.save()

 # reseni
 from django.db.models import F
 p = Product.objects.get(pk=710)
 p.items = F('items') + 1
 p.save()

 # nebo:
  Producst.objects.filter(pk=710).update(items=F('items') + 1)

V prvním případě probíhá aktualzace na úrovni aplikace. Tad hrozí, že mezi tím někdo jiný tentýž údaj změní, my pak jeho změnu přepíšeme naší hodnotou. Jestli on inkrementoval 10 na 11 a my si taky načetli 10, pak i po naší inkrementaci bude výsledek 11, místo správných 12.

Druhý případ zajistí, že inkrementaci provede až databáze, takže race condition nenastane.

Třetí případ je o něco přímočařejší. Pozor - neje použít get místo fitler, protože get nevrací QuerySet, ale je konečnou.

7.  Q objekty

Normální vyhledávání podle více kriterií je vždycky AND. Q objekty umožňují komplexnější dotazy. Zpřístupnění:

 from django.db.models import Q

Lze použít operátory: | (OR), & (AND), ~ (NOT) Příklad:

 Poll.objects.get(
     Q(question__startswith='Who'),
     Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
 )

Se přeloží do SQL jako:

 SELECT * from polls WHERE question LIKE 'Who%'
     AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

Lze kombinovat Q dotazy s klíči, ale klíč pak musí být až za Q:

 Poll.objects.get(
     Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
     question__startswith='Who')

Příklad negace:

 Q(question__startswith='Who') | ~Q(pub_date__year=2005)