Stronicowanie encji w Datastore
Dość często na stronach www musimy wyświetlić dużą ilość danych - np. listę komentarzy. Żeby strona nie była zbyt duża stronicuje się wyniki, np. 20 wpisów na stronę dając użytkownikowi możliwość przejścia do podstron z kolejnymi wpisami. W GAE dostępne jest fetch(limit, offset=0), lecz jak dokumentacja donosi - limit i offset nie mogą być większe niż 1000. Można użyć tego rozwiązania tylko przy bardzo małych ilościach wpisów (im większy offset tym gorsza wydajność).
Stronicowanie po właściwości
Zamiast stosować fetch można zaimplementować znacznie lepsze rozwiązanie. Poniżej etapami zbudujemy efektywny mechanizm stronicowania (paginacji) wyników po właściwości encji.
Oto model, dla którego zrobimy stronicowanie (model przechowujący sugestie użytkowników strony):
PAGESIZE = 10
class Suggestion(db.Model):
suggestion = db.StringProperty()
when = db.DateTimeProperty(auto_now_add=True)
def get(self):
suggestions = Suggestion.all().order("-when").fetch(PAGESIZE)
# ..szablon itd..
def get(self):
next = None
suggestions = Suggestion.all().order("-when").fetch(PAGESIZE + 1)
if len(suggestions) == PAGESIZE + 1:
next = suggestions[-1].when
suggestions = suggestions[:PAGESIZE]
# ..szablon itd..
def get(self):
next = None
# parametr z GET
bookmark = self.request.get("bookmark")
if bookmark:
suggestions = Suggestion.all().order("-when").filter('when <=', bookmark).fetch(PAGESIZE+1)
else:
suggestions = Suggestion.all().order("-when").fetch(PAGESIZE+1)
if len(suggestions) == PAGESIZE+1:
next = suggestions[-1].when
suggestions = suggestions[:PAGESIZE]
# ..render template..
Jeżeli stronicujesz używając właściwości posiadającej unikalne wartości to powyższy przykład wystarczy i spełni swoje zadania. W naszym przykładzie właściwość "when" nie jest (nie musi być) unikalna pośród wszystkich encji. Istnieje możliwość że więcej niż jedna encja może zostać dodana w tym samym czasie:
| offset | suggestion | when |
|---|---|---|
| ... | ... | ... |
| 9 | Stock Jolt | 2008-10-26 04:38:00 |
| 10 | Allow dogs in the office | 2008-10-26 03:35:58 |
| 11 | Allow cats in the office | 2008-10-26 03:35:58 |
| 12 | Buy some multicolored exercise balls | 2008-10-26 01:10:03 |
| 13 | ... | ... |
Należy zagwarantować unikalność wartości właściwości. Jednym ze sposób jest dodanie licznika, który będziemy inkrementować przy każdym dodaniu sugestii do datastore. Wartość licznika byłaby dodawana do każdej wartości "when" gwarantując unikalność:
| offset | suggestion | when |
|---|---|---|
| ... | ... | ... |
| 9 | Stock Jolt | 2008-10-26 04:38:00|09 |
|
10 |
Allow dogs in the office |
2008-10-26 03:35:58|10 |
|
11 |
Allow cats in the office | 2008-10-26 03:35:58|11 |
| 12 | Buy some multicolored exercise balls | 2008-10-26 01:10:03|12 |
| 13 | ... | ... |
By zaimplementować takie rozwiązanie musimy zmienić nieco nasz model. Właściwość "when" będzie łańcuchem (bo dostawiamy dodatkowe elementy do daty), a żeby mieć łatwą do odczytu datę dodania sugestii - dodajemy nową właściwość:
class Suggestion(db.Model):
suggestion = db.StringProperty()
created = db.DateTimeProperty(auto_now_add=True)
when = db.StringProperty()
Musimy więc przebudować model do postaci:
class Contributor (db.Model):
counter = db.IntegerProperty(default=0)
def unique_user(user):
"""
Tworzy unikalny łańcuch używając inkrementujący się licznik
shardowany per użytkownika
"""
email = user.email()
def txn():
contributor = Contributor.get_by_key_name(email)
if contributor == None:
contributor = Contributor(key_name=email)
contributor.count += 1
contributor.put()
return contributor.count
count = db.run_in_transaction(txn)
return email + "|" + str(count)
Teraz dane wyglądają znacznie lepiej ;)
| offset | suggestion | when |
|---|---|---|
| ... | ... | ... |
| 9 | Stock Jolt | 2008-10-26 04:38:00|joe@bitworking.org|1 |
| 10 | Allow dogs in the office | 2008-10-26 03:35:58|fred@example.org|1 |
| 11 | Allow cats in the office | 2008-10-26 03:35:58|joe@bitworking.org|2 |
| 12 | Buy some multicolored exercise balls | 2008-10-26 01:10:03|joe@bitworking.org|3 |
| 13 | ... | ... |
Używając adresu email użytkownika w wartości, po której stronicujemy będziemy ujawniać je w linkach stronicowania. Żeby temu zapobiec wartość właściwości "when" możemy zahaszować za pomocą np. md5:
def _unique_user(user):
"""
Tworzy unikalny łańcuch używając inkrementujący się licznik
shardowany per użytkownika
Końcowy wynik haszujemy dla ukrycia adresu email
"""
email = user.email()
def txn():
contributor = Contributor.get_by_key_name(email)
if contributor == None:
contributor = Contributor(key_name=email)
contributor.count += 1
contributor.put()
return contributor.count
count = db.run_in_transaction(txn)
return hashlib.md5( email + "|" + str(count)).hexdigest()
| offset | suggestion | when |
|---|---|---|
| ... | ... | ... |
| 9 | Stock Jolt | 2008-10-26 04:38:00|aee15ab24b7b3718596e3acce04fba85 |
| 10 | Allow dogs in the office | 2008-10-26 03:35:58|404a3235076f6651914358680acf3cb5 |
| 11 | Allow cats in the office | 2008-10-26 03:35:58|7574b989df099d4e2b95619c9cf0c2a0 |
| 12 | Buy some multicolored exercise balls | 2008-10-26 01:10:03|c675e87cc990a718979afecc93a77bc1 |
| 13 | ... | ... |
Stronicowanie bez właściwości
Powyższy przykład dotyczył stronicowania i sortowania po konkretnej właściwości. Gdy potrzebujesz stronicować encje, ale nie potrzebujesz określonego sortowania (lub kolejność kluczy ci odpowiada) to możesz zastosować stronicowanie po kluczach encji stosując właściwość __key__. Klucz encji jest unikalny i nie trzeba dodatkowych środków zapewniających unikalność:
def get(self):
next = None
bookmark = self.request.get("__key__")
if bookmark:
suggestions = Suggestion.all().order("__key__").filter('__key__ >=', bookmark).fetch(PAGESIZE+1)
else:
suggestions = Suggestion.all().order("-when").fetch(PAGESIZE+1)
if len(suggestions) == PAGESIZE+1:
next = suggestions[-1].when
suggestions = suggestions[:PAGESIZE]
# ..szablon itd..
Stronicowanie po kluczu i nieunikalnej właściwości
Można także użyć do stronicowania klucza i nieunikalnej właściwości. Zaleta to brak konieczności dodatkowej unikalnej właściwości w modelu, lecz wadą: większa ilość zapytań. Więcej można poczytać o tym na liście dyskusyjnej GAE.
Na podstawie Paging through large datasets.
Comment article