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ů)

  1. Scukařina, žádná dřina – díl první: agilně! 18.8.2010
  2. Scukařina, žádná dřina – díl druhý: Django je naše máma 25.8.2010
  3. Scukařina, žádná dřina – díl třetí: Djangová je náš táta 1.9.2010
  4. Scukařina, žádná dřina – díl čtvrtý: jiná práce s CSS 8.9.2010
  5. Scukařina, žádná dřina – appendix 15.9.2010

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?

  1. Přidáme do modelu Shop nové pole reviews_count (s pomocí Jižana, samozřejmě):

    class Shop(models.Model):
        # ... puvodni pole modelu
        reviews_count = models.IntegerField(editable=False)
  2. 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')
  3. 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?).

Komentáře: 11

Přehled komentářů

petr_praus Re: Scukařina, žádná dřina - díl třetí: Djangová je náš táta
Head of Operational Efficiency Selhani marketingu
msgre Re: Selhani marketingu
Inkvizitor Re: Selhani marketingu
ja Re: Selhani marketingu
msgre Re: Selhani marketingu
pkroh Re: Selhani marketingu
Inkvizitor Re: Selhani marketingu
mikiqex Re: Selhani marketingu
Olda Re: Selhani marketingu
m0rph ad počítání recenzí
Zdroj: https://www.zdrojak.cz/?p=3314