Query
Django - Dotazy (Query)
Obsah stránky (hide)
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)