• Официальный сайт SDK
  • Сайт с примерами кода

Обновление схем данных

Марк Ивей, инженер Google
Июнь 2008

Введение

Если вы занимаетесь сопровождением успешного проекта, то часто появляется необходимость в обновлении существующей схемы данных. Эта статья рассказывает о двух основных шагах, которые потребуется сделать, чтобы обновить существующую схему:

  1. Обновление класса моделей
  2. Обновление существующих объектов хранилища (этот шаг не всегда будет необходим, подробнее осветим этот момент по тексту ниже).

До того, как мы начнем

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

Обновление моделей

Например, имеется простая модель, содержащая данные изображения:

class Picture(db.Model):
  author = db.UserProperty()
  png_data = db.BlobProperty()
  name = db.StringProperty(default='')  # Уникальное имя.

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

class Picture(db.Model):
  author = db.UserProperty()
  png_data = db.BlobProperty()
  name = db.StringProperty(default='')  # Уникальное имя.
  num_votes = db.IntegerProperty(default=0)
  avg_rating = db.FloatProperty(default=0)

Теперь все новые объекты, помещаемые в хранилище, получат значение рейтинга по умолчанию - 0. Обратите внимание, что уже существующие в хранилище объекты не будут автоматически изменены и не получат новые свойства.

Обновление существующих объектов

Хранилище платформы App Engine не требует того, чтобы все объекты одной модели имели одинаковый набор типов свойств. После обновления структуры моделей и добавлении к ней новых свойств, существующие объекты будут использовать старый набор. В некоторых ситуациях это не доставляет проблем, так как вам не требуется выполнять лишнюю работу. Когда может понадобиться после изменения структуры провести дополнительные действия и обновить существующие объекты, для того чтобы они получили новые свойства? Одна из таких ситуаций возникает, если вы захотите выполнять запросы, которые будут задействовать добавленные свойства. В нашем случае с моделью Picture, запросы по значениям "Самое популярное" или "Менее популярное" не вернут никаких результатов, так как объекты (пока) не имеют новых свойств с их рейтингом. Чтобы исправить это, необходимо обновить существующие в хранилище объекты.

Концептуально, процедура обновления очень проста. Необходимо написать новый обработчик запросов, который будет загружать каждый объект, присваивать новому свойству какое-то значение и сохранять изменения. Однако существуют две проблемы, которые мы должны принять во внимание:

  • Количество результатов, возвращаемых на запрос, ограничено 1000 объектами. Если в хранилище содержатся более 1000 объектов, то понадобится осуществить несколько последовательных запросов для того, чтобы получить их все.
  • Запросы не могут выполняться продолжительное время и в этом случае будут прерваны системой. Если имеется значительное количество объектов, то обработчик запроса возможно не сумеет произвести обновление их всех за один запрос.
Решением этих двух проблем является создание обработчика, который занимается обновлением небольшой порции данных за каждый запрос. Путем выполнения нескольких таких запросов, мы сможем обработать все существующие объекты вне зависимости от поставленных системных ограничений. Для того, чтобы сделать это проще, мы будем обновлять за один раз единственный объект, таким образом это сведется к алгоритму:
  1. Загружаем один объект
  2. Устанавливаем значение свойству (если свойство имеет значение по умолчанию, то этот шаг можно пропустить)
  3. Сохраняем объект
  4. Используем мета тэг с обновлением страницы для отправки браузера к URL с обработчиком следующего объекта

Небольшое предостережение: при написании запроса, который будет получать данные порциями, избегайте использования параметра OFFSET (который не будет правильно работать с большими объемами данных), вместо чего ограничивайте результаты запроса с помощью конструкции WHERE с условием. Если данные содержат свойство с уникальным значением для каждого объекта, это будет сделать довольно просто. К примеру, свойство name модели Picture должно содержать уникальные значения и мы будем использовать выражение WHERE с условием, основывающимся на значении name.

Пример кода:

# Обработчик запроса URL /update_datastore
def get(self):
  name = self.request.get('name', None)
  if name is None:
    # Это первый запрос, извлекаем первый объект из хранилища.
    pic = models.Picture.gql('ORDER BY name DESC').get()
    name = pic.name  q = models.Picture.gql('WHERE name <= :1 ORDER BY name DESC', name)
  pics = q.fetch(limit=2)
  current_pic = pics[0]
  if len(pics) == 2:
    next_name = pics[1].name
    next_url = '/update_datastore?name=%s' % urllib.quote(next_name)
  else:
    next_name = 'FINISHED'
    next_url = '/'  # Процесс закончен, переходим к основной странице.
  # В нашем примере для свойств num_votes и avg_rating используются значения
  # по умолчанию - 0, поэтому объект может без их изменения сохранен методом put().
  current_pic.put()  context = {
    'current_name': name,
    'next_name': next_name,
    'next_url': next_url,
  }
  self.response.out.write(template.render('update_datastore.html', context))
    

Ниже представлен шаблон, который показывает текущий обновляемый объект и перенаправляет браузер с помощью мета-тэга к следующему объекту:

<html>
<head>
  <meta http-equiv="refresh" content="0;url="/>
</head>
<body>
  <h3>Обновление хранилища</h3>
  <ul>
    <li>Обновлено: </li>
    <li>Следующий: </li>
  </ul>
</body>
</html>
    

Если обновляемые объекты не имеют свойства с уникальным значением, приведенный выше пример не будет работать (он застопорится на том месте, где совпадают значения двух объектов). Необходимо будет дополнить этот код для того, чтобы он работал с объектами, которые могут содержать одинаковые значения. Общая концепция останется та же, однако с помощью конструкции WHERE нужно будет ограничивать количество получаемых результатов в ответ на запрос и производить работу с порциями, вызывая метод put() для каждого объекта.

Очистка удаленных свойств в хранилище

Если вы произведете удаление свойства из модели, то увидите, что существующие объекты сохранят прежний набор свойств. Это будет видно в Административной консоли, что данные сохранят свое присутствие в хранилище. Для того, чтобы принудительно выполнить очистку старых данных, потребуется запустить цикл, который будет последовательно перебирать все объекты и удалять из них устаревшее содержимое.

  1. Убедитесь, что вы удалили соответствующие свойства из определения модели.
  2. Если класс этой модели является потомком класса db.Model, временно сделайте его потомком класса db.Expando. (экземпляры класса db.Model не могут быть динамически модифицированы, что необходимо нам будет в следующем шаге)
  3. Выполните цикл для каждого существующего объекта (как описано далее). Для каждого объекта используйте операцию delattr для удаления старого свойства, после чего сохраните изменения.
  4. Если модель была ранее потомком класса db.Model, не забудьте переключить ее обратно после процедуры обновления данных.

На будущее

Этот метод циклического обновления данных объектов на сегодняшний день является неким "костылем". Однако, в данный момент мы работаем над решением, которое позволит автоматизировать этот процесс. После того, как оно станет доступным, появится более действенный метод для изменения структуры данных без повышенной нагрузки на сервер, которую оказывает вышеописанный метод. Для того, чтобы оставаться в курсе новых возможностей для разработчиков, не забудьте подписаться на блог App Engine.