Scukařina, žádná dřina – díl třetí: Djangová je náš táta

V třetím díle Scukařiny vám mladý poutník Scuk dot cz představí účinný nástroj na hubení dotěrného hmyzu, odhalí temné denormalizační praktiky a jako dezert nám naservíruje špetku Javascriptu, map a geografie. No to bude vzrůšo! Předpokládáme, že znáte hlavní postavu a víte, kdo je vrah. Pokud ne, navštivte prosím předchozí díl!
Seriál: Scukařina, žádná dřina (5 dílů)
- Scukařina, žádná dřina – díl první: agilně! 18. 8. 2010
- Scukařina, žádná dřina – díl druhý: Django je naše máma 25. 8. 2010
- Scukařina, žádná dřina – díl třetí: Djangová je náš táta 1. 9. 2010
- Scukařina, žádná dřina – díl čtvrtý: jiná práce s CSS 8. 9. 2010
- Scukařina, žádná dřina – appendix 15. 9. 2010
Nálepky:
Mucholapka
Tak. Máme hotový čupr systém, jedna vlastnost lepší než druhá. Vystavíme jej slavnostně na web a po čtvrthodině máme na lopatkách. Co se stalo? Aplikace přetížila server a hostingová společnost vám ji odstřelila.
Jedním z výkonnostních problémů často bývá počet a složitost SQL dotazů, které na vykreslení stránky potřebujete vykonat. Jak ale v rozrostlé aplikaci identifikovat problémová místa? Aplikací django-debug-toolbar!
Po nainstalování se vpravo objeví panel, ze kterého vyčtete zajímavé informace, včetně počtu SQL dotazů a době jejich zpracování:
Django debug toolbar nasazený na Scuku.
Po rozkliknutí sekce „SQL“ získáte kompletní seznam, přičemž u každého řádku můžete zjistit další podrobnosti:
- čas vykonání dotazu,
- výsledek dotazu (SELECT),
- analýzu dotazu (EXPLAIN),
- referenci na místo ve zdrojovém kódu, kde byl dotaz vykonán (Toggle Stacktrace),
- samotný SQL dotaz,
- vizuální vyjádření doby zpracování dotazu (viz čára nad dotazem),
django-debug-toolbar
toho ale umí víc – viz video Django Debug Toolbar 0.8 od Idan Gazit na Vimeo.
Vyzkoušejte ho!
Denormalizace
Téma, které úzce souvisí s předchozí kapitolou. Začněme tentokrát příkladem z praxe:
Scuk je průvodce po místech, kde se dobře jí a nakupuje. Slovo „dobře“ z technického pohledu znamená, že veřejnosti v katalogu zobrazujeme pouze ty podniky, které splní následující parametry:
- Musí mít alespoň jednu recenzi.
- Recenze musí podnik dostatečně silně ohodnotit.
- Samotná recenze musí být důvěryhodná.
Tři různé podmínky pro jediný podnik, přičemž v její formulaci se bavíme nejen o podniku, ale i o počtu recenzí, jejich síle a důvěryhodnosti recenzenta. Procházka minimálně po třech modelech. Je jisté, že pokud bychom měli takto podrobnou analýzu provádět s každým requestem, odrazí se to negativně na výkonnosti celé aplikace.
Můžeme si ale pomoci denormalizací — praktikou, která nám při určitých změnách v databázi automaticky ukládá pomocné informace k modelu. Podívejte se na následující obrázek:
Aktuální počet recenzí bychom mohli zjistit například takto:
good_shops = [] for shop in Shop.objects.all().iterator(): reviews = Review.objects.filter(shop=shop) if reviews.count() > 0: good_shops.append(shop) print good_shops
V seznamu good_shops
budeme mít všechny podniky, které mají alespoň jednu recenzi (poznámka: na Scuku máme podmínku složitější, protože nás zajímají jen kvalitní recenze). Uvedený algoritmus je ale velmi neefektivní, musí projít všechny podniky a pro každý z nich zjistit počet recenzí.
Díky několika úpravám ale můžeme kód nahradit jediným rychlým dotazem:
Shop.objects.filter(reviews_count__gt=0)
Jak toho dosáhnout?
-
Přidáme do modelu
Shop
nové polereviews_count
(s pomocí Jižana, samozřejmě):class Shop(models.Model): # ... puvodni pole modelu reviews_count = models.IntegerField(editable=False)
-
Vytvoříme soubor
reviews/signals.py
a vložíme do něj následující kód:from django.db.models.signals import post_save, post_delete from scuk.reviews.models import Review def update_reviews_count(sender, **kwargs): # z recenze (instance) vytahneme referenci na podnik shop = kwargs.get('instance').shop # zjistime aktualni pocet recenzi u podniku count = shop.review_set.count() # vlozime jej do pole reviews_count shop.reviews_count = count # ulozime podnik shop.save() post_save.connect(update_reviews_count, sender=Review, dispatch_uid='update_reviews_count.post_save') post_delete.connect(update_reviews_count, sender=Review, dispatch_uid='update_reviews_count.post_delete')
-
Na poslední řádek souboru
reviews/models.py
přidáme:import scuk.reviews.signals
Hotovo. Jakmile dojde k uložení nebo smazání jakékoliv recenze, spustí se funkce update_reviews_count
. Ta nejdříve vytáhne referovaný podnik, pak pro něj z DB vytáhne údaj o celkovém počtu recenzí a nakonec počet uloží do instance modelu Shop
. Více se o signálech dočtete v oficiální dokumentaci.
Podobným způsobem na Scuku ukládáme informace o počtech recenzí v profilech uživatelů, kategoriích podniku nebo průměrném hodnocení podniku. S jejich pomocí pak můžeme pokládat jednoduché a rychlé SQL dotazy i pro relativně složité vazby.
Poznámka (příjemná): od Djanga 1.2 je možné reagovat i na signály od M2M vazeb.
Poznámka (praktická): kód pro denormalizaci můžete vložit i do modelu, konkrétně do metodsave
a delete
. V obou případech (s využitím signálu i překrytím) ale musíte pamatovat na to, že některé ORM operace v Djangu signály neodpalují (update) a metody instance nevolají (queryset delete). Tak bacha!
Poznámka (varovná): pamatujte, že denormalizace je temná a Gandalf se za ni na vás bude zlobit (minimálně kvůli porušení principu DRY, či možnosti desynchronizace dat). Podobného (a mnohem čistšího) efektu můžete dosáhnout optimalizací kódu nebo nasazením keše. Je to jen na vás.
S mapou i bez buzoly
Na Scuku máme dvě vyhledávací políčka. První „Co“ prochází štítky, kategorie a titulky podniků, hodnotou v poli „Kde“ pak můžete dotaz zúžit na konkrétní geografickou oblast. Realizace druhého políčka byla paradoxně jednodušší. Stačilo vložit do hrnce službu Google Geocoder, zprovoznit GeoDjango (knihovna rozšiřující Django o geografické služby), zaGooglit a promíchat.
Pokud do pole „Kde“ vložíte nějakou hodnotu (např. adresu „Milíčova, 4, Ostrava“) a kliknete na „Hledat“, klientský Javascriptový kód pošle dotaz na službu Google Geocoder. Odpověď přijde ve formě JSON dat:
{ u'Status': { u'code': 200, u'request': u'geocode' }, u'Placemark': [{ u'Point': {u'coordinates': [18.2891279, 49.836991300000001, 0]}, u'ExtendedData': { u'LatLonBox': { u'west': 18.285980299999999, u'east': 18.292275499999999, u'north': 49.840138899999999, u'south': 49.833843700000003 } }, u'AddressDetails': { u'Country': { u'CountryName': u'u010ceskxe1 republika', u'AdministrativeArea': { u'SubAdministrativeArea': { u'SubAdministrativeAreaName': u'Ostrava', u'Locality': { u'PostalCode': {u'PostalCodeNumber': u'702 00'}, u'Thoroughfare': {u'ThoroughfareName': u'Milxedu010dova 1939/4'}, u'LocalityName': u'Moravskxe1 Ostrava'} }, u'AdministrativeAreaName': u'Moravskoslezskxfd' }, u'CountryNameCode': u'CZ' }, u'Accuracy': 8 }, u'id': u'p1', u'address': u'Milxedu010dova 1939/4, 702 00 Moravskxe1 Ostrava, u010ceskxe1 republika' }], u'name': u'Milxedu010dova, 4, Ostrava, u010ceskxe1 republika' }
Nejzajímavější pasáží je v tomto případě záznam ['Placemark'][0]['ExtendedData']['LatLonBox']
, tedy oblast, která odpovídá vašemu dotazu. Stačí tyto informace vyzobnout a přilepit k datům odesílaným na server. Tam se jich chopí Django a vytáhne z DB pouze ty záznamy, které do poptávané oblasti spadají:
geodata = {'lon1': 18.292275499999999, 'lon2': 18.285980299999999, 'lat1': 49.840138899999999, 'lat2': 49.833843700000003} polygon = "POLYGON((%(lon1)f %(lat1)f,%(lon1)f %(lat2)f,%(lon2)f %(lat2)f, %(lon2)f %(lat1)f,%(lon1)f %(lat1)f))" % geodata shops = Shop.objects.filter(point__within=polygon)
Poznámka: within
je operátor z GeoDjanga a point
je pole modelu Shop
, např.:
from django.db import models from django.contrib.gis.db import models as geomodels class Shop(models.Model): # ...ostatni pole modelu point = geomodels.PointField(u"Zeměpisná poloha", blank=True, null=True)
Podobným způsobem ve Scuku probíhá i automatické zařazování podniků do krajů. Pouze s tím rozdílem, že nás nezajímá oblast na mapě, ale podrobné informace o zadané adrese (viz ['Placemark']['AddressDetails']
). K provádění dotazů ze strany serveru můžete využít knihovnu geocoders od Simona Willisona.
Předání žezla
Tímto dílem sice tóny Djanga dozněly, ale Scukopříběh žije dál. Příště se dočtete o dobrodružství, které zažil Martin Michálek během kódování HTML stránek (Zdroják přece nečtou jen programátoři… Nebo jo?).
Opět díky za skvělý článek, geolokaci v Djangu zrovna řeším a o Geocoder class jsem nevěděl.
tolik prace to da a pak nekdo vymysli jmeno SCUK, ach jo :(
Pročpak? Scuk je skvělé jméno! :)
Co to vlastně přesně znamená? S Cuketkou?
scuk je sraz
Je to jak píše předchozí komentátor. Jako děcka jsme říkali třeba: „Dáme si scuka v šest“ (sejdeme se v šest).
Myslel jsem si to… a taky myslím že pak máte trochu problém, protože tento dětinský slang „ahojky, určo si dáme scuka s Mirčou, Věrčou a Ifčou za hoďku na tramvošce, děcka“ dost lidí docela spolehlivě odpudí.
Upřímně řečeno, pokud je to takto (samozřejmě znám slovo „scuk“, akorát jsem doufal, že to vysvětlení bude vtipnější, už proto, že v projektu Pan Cuketka opravdu figuruje), taky se mi ten název nelíbí.
Vysvětlovali to jako „S CUketkou Kvalitně“ :)
Začíná to na S, bude se to dobře prodávat Seznamu.
Nešlo by vyrobit jeden dotaz prostě použitím agregace?
http://docs.djangoproject.com/en/1.2/topics/db/aggregation/#filtering-on-annotations