Разработчики, которые ранее использовали обычные реляционные базы данных часто сразу не могут переложить свои навыки на хранилище App Engine и заваливают буквально шквалом вопросов. Один из них: Как выполнить операции SUM и AVG с данными хранилища?. Вы не можете напрямую выполнять в хранилище подобные запросы, вместо этого необходимо постоянно вести учет итогов и хранить их в специальном счетчике. Вот пример такой реализации:
Файл models.py:
from appengine_django.models import BaseModel
from google.appengine.ext import db
from google.appengine.ext.db import NotSavedError
from decimal import Decimal, getcontext
class Callable:
# это класс выполняет обертывание метода, так что мы можем вызвать его напрямую без создания экземпляра.
def __init__(self, anycallable):
self.__call__ = anycallable
class GlobalCounter(BaseModel):
name = db.StringProperty(required=True)
value = db.IntegerProperty(required=True)
class Rating(BaseModel):
user = db.UserProperty(required=True)
rating = db.IntegerProperty(required=True) # скрываем его
oldRating = 0
def set_rating(self, val):
self.oldRating = self.rating
self.rating = val
def put(self):
self.save()
def save(self):
# получаем значения счетчиков из хранилища
votes = GlobalCounter.all().filter("name = ", "numberOfVotes").get()
total = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get()
if (votes == None):
votes = GlobalCounter(name = "numberOfVotes", value = 0)
votes.save()
if (total == None):
total = GlobalCounter(name = "totalAmountOfVotes", value = 0)
votes.save()
# проверяем, является ли рейтинг новым или пользователь просто меняет свой выбор
isSaved = False
try:
self.key()
isSaved = True
except NotSavedError:
isSaved = False
if (isSaved):
# если пользователь пересохраняет свой выбор (возможно меняя его) мы вычитаем из итогов старое значение и добавляем новое
total.value = total.value - self.oldRating + self.rating
else:
votes.value += 1
total.value += self.rating
votes.save()
total.save()
BaseModel.save(self)
def ___getAverage___():
votesCounter = GlobalCounter.all().filter("name = ", "numberOfVotes").get()
totalCounter = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get()
getcontext().prec = 3
return Decimal(str(totalCounter.value)) / Decimal(str(votesCounter.value))
average = Callable(___getAverage___) # average() будет теперь вести себя как статическая переменная
def ___getTotal___():
totalCounter = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get()
return totalCounter.value
sum = Callable(___getTotal___) # sum() будет теперь вести себя как статическая переменная
А использовать это можно так… Файл tests.py:
import os
from decimal import Decimal, getcontext
#импортируем загрушки
from google.appengine.api import apiproxy_stub_map
from google.appengine.api import datastore_file_stub
from google.appengine.api import mail_stub
from google.appengine.api import urlfetch_stub
from google.appengine.api import user_service_stub
from google.appengine.api import users
# импортируем замену Django Forms из SDK App Enginee
from google.appengine.ext.db.djangoforms import ModelForm
# Я использую библиотеку Python Mocker для создания тестовых объектов: http://labix.org/mocker
import mocker
from mocker import MockerTestCase
# импортируем нашу модель
from avgtest.models import Rating
from avgtest.models import GlobalCounter
class TestStart(MockerTestCase):
def setUp(self):
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
# Используем мнимое хранилище.
# В этой точке приложения задается, что все обращения к хранилищу, например операции get и put,
# будут выполняться с временными данными, расположенными в памяти.
stub = datastore_file_stub.DatastoreFileStub(u'myTemporaryDataStorage', '/dev/null', '/dev/null')
apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub)
# Используем заглушку для UserService.
apiproxy_stub_map.apiproxy.RegisterStub('user', user_service_stub.UserServiceStub())
os.environ['AUTH_DOMAIN'] = 'gmail.com'
os.environ['USER_EMAIL'] = 'myself@appengineguy.com' # set to '' for no logged in user
os.environ['SERVER_NAME'] = 'fakeserver.com'
os.environ['SERVER_PORT'] = '9999'
# Используем заглушку для urlfetch.
apiproxy_stub_map.apiproxy.RegisterStub('urlfetch', urlfetch_stub.URLFetchServiceStub())
# Используем заглушку для почты.
apiproxy_stub_map.apiproxy.RegisterStub('mail', mail_stub.MailServiceStub())
self.HttpResponseRedirect = self.mocker.replace("django.http.HttpResponseRedirect")
self.render_to_response = self.mocker.replace("django.shortcuts.render_to_response")
def testAverage(self):
rA = Rating(user = users.get_current_user(), rating = 1)
rA.save()
self.assertEquals(1, Rating.average())
rB = Rating(user = users.get_current_user(), rating = 4)
rB.put()
self.assertEquals(Decimal("2.5"), Rating.average())
rC = Rating(user = users.get_current_user(), rating = 2)
rC.save()
self.assertEquals(Decimal("2.33"), Rating.average())
def testSum(self):
rA = Rating(user = users.get_current_user(), rating = 2)
rA.save()
rB = Rating(user = users.get_current_user(), rating = 4)
rB.put()
rC = Rating(user = users.get_current_user(), rating = 3)
rC.save()
self.assertEquals(9, Rating.sum())
def testAverageChangeVote(self):
rA = Rating(user = users.get_current_user(), rating = 1)
rA.save()
self.assertEquals(1, Rating.average())
rB = Rating(user = users.get_current_user(), rating = 4)
rB.put()
self.assertEquals(Decimal("2.5"), Rating.average())
rC = Rating(user = users.get_current_user(), rating = 2)
rC.save()
self.assertEquals(Decimal("2.33"), Rating.average())
rC.set_rating(3) # пользователь C изменил свою оценку на 3
rC.save()
self.assertEquals(Decimal("2.67"), Rating.average())
Замечание по производительности: Приведенный выше пример показывает неэффективную работу с счетчиками, однако он указан только для объяснения принципов работы. На практике же необходимо использовать разделение счетчиков.