7 November 2008

Przepisanie bloga na GAE - opis problemów

Postaram się pokrótce przybliżyć problemy związane z przenoszeniem aplikacji ze standardowego Django do Google App Engine + Django 1.0 + GAE Helper.

Cel

W ramach testowania Google App Engine i Django, postanowiłem zrobić coś praktycznego i przepisać istniejącą aplikację bloga z tagami, komentarzami i stronami statycznymi. Zacznę od wskazania głównych aplikacji stosowanych ze standardowego Django lub aplikacji dodatkowych, bo to w zasadzie główny problem Django na GAE -- niemożność ich bezpośredniego użycia.

  • Admin z Django (jeszcze wersja przed new forms admin)
  • Flatpages i Sites
  • Feedy i sitemap
  • Komentarze z Django
  • Comment-Utils dla Django
  • Tagging dla Django

Żadnego z tych elementów nie można bez modyfikacji zastosować w Google App Engine (nawet posiłkując się Helperem)!

Nowa wersja

Panel administracyjny

Napisałem prosty, własny panel administracyjny, który w bardzo prosty sposób symuluje panel z Django. Muszę powiedzieć, że panel z Django doceniłem tak naprawdę dopiero wtedy, gdy go straciłem. Co prawda istnieje zestaw poprawek zapewniający uruchomienie panelu administracyjnego w GAE, ale to dopiero najprostsze elementy.

Wykonany panel to w zasadzie znacznie uproszczona symulacja standardowego admina umożliwiająca podłączanie nowych aplikacji za pomocą plików admin.py i symulująca klasę Admin w najprostszej postaci. Admin odpowiada za wywołania obsługi dodawania, listowania, edycji i usuwania elementów, zapewniając domyślną implementację formularzy (modelforms) i usuwania (ale nie listowania). Nie umożliwia również filtrowania, zmiany orderingu i wyszukiwania. Elementy te nie są mi potrzebne, a w GAE filtrowanie i ordering to nie zawsze fraszka, o elastycznym wyszukiwaniu nie wspominając ;)

Pomimo zastosowania ograniczenia dostępności admina za pomocą logowania administracyjnego do GAE (czyli administrator ma wszystkie prawa, jeśli się zaloguje), wykonanie tego prostego admina zajęło około 10 godzin, ale dzięki Pythonowi zawarło się w niewielkiej ilości kodu -- 200 wierszy kodu + 7 szablonów.

W tym miejscu chciałbym się podzielić pewnym fragmentem kodu dla formularzy, który zapewnia obsługę w formularzu listy wiele do wielu tworzonej zgodnie z zasadami GAE, czyli jako ListProperty w jednej z encji przechowujące klucze do drugiej encji.

from google.appengine.ext.db import Key

from django.forms.fields import MultipleChoiceField

class ModelChoiceIterator(object):
def __init__(self, field):
self.field = field
self.query = field.query

def __iter__(self):
for obj in self.query:
yield (obj.key(), getattr(obj, self.field.use_field))

class KeyListPropertyField(MultipleChoiceField):
def __init__(self, query, use_field, *args, **kwargs):
self.use_field = use_field
super(KeyListPropertyField, self).__init__(*args, **kwargs)
self.query = query

def _get_query(self):
return self._query

def _set_query(self, query):
self._query = query
self.widget.choices = self.choices

query = property(_get_query, _set_query)

def _get_choices(self):
return ModelChoiceIterator(self)

def _set_choices(self, value):
self._choices = self.widget.choices = list(value)

choices = property(_get_choices, _set_choices)

def clean(self, value):
new_value = super(KeyListPropertyField, self).clean(value)
return [Key(s) for s in new_value]

Pole działa podobnie do pola wielokrotnego wyboru z queryset z Django, ale jest nieco mniej elastyczne. Nie stosuje rozbudowanych walidacji, ale konwertuje otrzymane dane na zbiór obiektów Key.

Domeny i strony statyczne

Domeny i strony statyczne w zasadzie przerobiłem na modele GAE, dodatkowo usuwając niepotrzebne mi elementy modeli. Klasa middleware nie wymagała zmian, ale potrzebował tego widok i w drobnym stopniu metody klasy modelu. Potem pozostało tylko dorobić klasę dla własnego systemu administracyjnego.

RSS i sitemap

Te dwa elementy w zasadzie działają w GAE, o ile zastosuje się dwie poniższe poprawki, gdyż standardowo oba systemy są uzależnione od klasy Site z głównego Django. Jeśli i tak tworzy się model Sites (jak ja), nie stanowi do dużego problemu. W przeciwnym razie trzeba wykonać własną, minimalną klasę Site z odpowiednim API, by oszukać oba systemy (w takiej sytuacji pomocy szukaj w klasie RequestSite z Django.

Poniższą poprawkę trzeba umieścić w pliku main.py, by zapewnić monkeypatching modułu django.contrib.sites.models:

from sites import models as sites_models
sys.modules['django.contrib.sites.models'] = sites_models

Zakładam, że w sites.models znajduje się klasa Site z odpowiednim API. Dla sitemap to wystarcza, ale klasa Feed testuje jeszcze dane meta klasy Site, więc potrzebna jest dodatkowa poprawka dla klasy Site, najprościej po jej zdefiniowaniu:

Site._meta.installed = True

Po tych zabiegach, stosując główne klasy dla RSS i sitemap (zapomnij o pomocnikach dla stron statycznych!) i definiując metody items() w podklasach, można wygenerować RSS i plik sitemap.

Komentarze

Ponieważ stosowałem jedynie komentarze bez logowania i z aplikacji dodatkowej blokowanie komentowania po określonym czasie, utworzyłem własny model, formularz i kod zapisywania komentarza zgodny z GAE. Oczywiście pamiętajmy, że w GAE wydajne tworzenie komentarzy wymaga również dodania pola licznika komentarzy do wpisów, a to wiąże się z odpowiednią inkrementacją i dekrementacją licznika, najlepiej wewnątrz transakcji (wtedy komentarz musi mieć klucz zawierający wpis jako rodzica). Jak widać coś co wcześniej wymagało kilku wierszy, teraz urasta do rangi około 100 wierszy (model, licznik, widok).

Kategorie (tagi)

Oryginalnie zastosowałem aplikację tagging, która wymagała ode mnie tylko kilku wierszy kodu w starej aplikacji. W GAE zaczynają się schody podobne do tych dla komentarzy, jeśli chcemy dodatkowo zapewnić listę najpopularniejszych tagów (osobny model, liczniki). Jeśli możemy się obejść bez listy wszystkich tagów i listy najpopularniejszych, w zasadzie użycie tagów sprowadza się w GAE do zastosowania pola typu StringListProperty. Filtrację po określonym tagu i przechowywanie wielu tagów zapewni nam wtedy GAE. Gdy jednak chcemy coś więcej, witaj dodatkowy modelu z licznikiem i witaj główkowanie, gdy w adminie chcemy dodać tag, inny usunąć dla tego samego modelu (dodatkowe 15 wierszy kodu, by to zapewnić!). Oczywiście aplikacja tagging również miała te same problemy, ale mnie jako użytkownikowi pozostawało wywoływać odpowiednie metody.

Inne elementy

Na witrynie korzystam również z Markdown + CodeHilite + Pygments, ale ponieważ elementy te nie mają związku z bazą danych, działają bez przeróbek. Drobnych poprawek wymaga również system umiędzynarodowienia, jeśli używamy Django w postaci archiwum ZIP. Pełna poprawka jest dostępna na Google Groups

Podsumowanie

Przeniesienie istniejącej strony WWW mocno powiązanej z tym co oferuje standardowe Django (poprzednia witryna miała tylko około 150 wierszy mojego kodu!) do zadań łatwych i szybkich nie należy. Choć nadal mamy dostęp do wielu elementów Django (formularze, widoki, I18N, URLe, RSS, szablony), musimy napisać znacznie więcej kodu (~800 wierszy). To nadal niedużo, jeśli porównać to z tworzeniem tego całego bloga od podstaw!

Największej zmiany wymaga sposób myślenia zgodnie z tym, czego wymaga DataStore, czyli liczniki, pola wiele do wielu po jednej stronie, stosowanie hierarchii (lista miesięcy i lat po prawej stronie to osobny model powiązany z wpisami, więc wymaga dodawania i usuwania!). W zasadzie prelekcje Building Scalable Web Applications with Google App Engine, Working with Google App Engine Models i Under the Covers of the Google App Engine Datastore. Bez ich obejrzenia do tworzenia bloga takiego jak ten w sposób efektywny lepiej nie podchodzić!

Są elementy, w których Django jednak nie zachwyciło elastycznością -- mam tu na myśli zbyt mocne powiązane feedów i sitemap z modelem Site, które uniemożliwia rozwiązanie problemu przez dziedziczenie z prostą modyfikacją jednej z metod.

Inny problem, który pozostał mi w zasadzie do rozwiązania to dosyć spory czas startu w przypadku "zimnego" interpretera. Django wczytywane z pliku ZIP + wszystkie łatki Helpera (nie są robione leniwie!) i liczba samych modułów Pythona powodują tu pewne spore opóźnienie. Oczywiście problemu nie byłoby, gdyby witryna była bardziej popularna i hity częstsze :)

Generalnie trzeba mocno podkreślić to, co podkreślałem wcześniej na prelekcji o GAE. Istniejący projekt w Django lub innym frameworku Pythona trudno przenieść do GAE z zapewnieniem wydajności, bo wymaga przeorganizowania całego kodu związanego z modelami, jeśli nie są one bardzo proste. Listę wpisów zrobić ławo, ale blog z tagami, historią, komentarzami, listami popularnych już znacznie gorzej. W drugą stronę, czyli z GAE do standardowego Django jest znacznie łatwiej, bo DataStore na relacyjną bazę danych przy niewielkim ruchu mapować łatwo.

Tworzenie nowego projektu od podstaw w GAE to całkowicie inna sprawa. Analizując prelekcje o DataStore można uzyskać ogólny obraz tego, co jest możliwe i jak to wykonać wydajnie. Dopiero później zabierajmy się za projektowanie. Tworzenie własnej złożonej aplikacji GAE pozwoliło mi zrozumieć, dlaczego aplikacje Google wyglądają i działają tak, a nie inaczej. Po prostu muszą, ale jest to dosyć sprytnie wplatane w interfejs :)

0 comments:

Post a Comment