• Home
  • Учебник по ExtJS
  • О сайте
  •  



    Операции AVG и SUM в Google App Engine

    Разработчики, которые ранее использовали обычные реляционные базы данных часто сразу не могут переложить свои навыки на хранилище 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())

    Замечание по производительности: Приведенный выше пример показывает неэффективную работу с счетчиками, однако он указан только для объяснения принципов работы. На практике же необходимо использовать разделение счетчиков.

    Leave a Reply