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

Моделирование связей между объектами

Рейф Кэплэн, инженер по программному обеспечению
Июнь 2008

Введение

Уверен, что Руководство для начинающих содержит описание всего того, что нужно для начала работы с данными хранилища, однако оно не затрагивает вопросы построения схем данных, которые обычно используются в реальных приложениях. Неважно, являетесь ли вы новичком в разработке веб-приложений или имеете богатый опыт работы с SQL запросами в базах данных, эта статья будет полезна всем, кто хочет выйти на новый уровень понимания работы с данными App Engine.

Почему так важно уделять время построению связей между объектами?

Представьте, что вы разрабатываете новое убойное приложение, которое содержит адресную книгу и позволяет посетителям хранить в ней свои контакты. Для каждого такого контакта, необходимо сохранить имя человека, его день рождения (который важно не забыть!), адрес, номер телефона и компанию, где он работает.

Когда пользователь хочет добавить сведения об адресе, он вводит эту информацию в форму, которая передает ее в модель приложения, подобную этой:

class Contact(db.Model):  # Основные данные
  name = db.StringProperty()
  birth_day = db.DateProperty()  # Информация об адресе
  address = db.PostalAddressProperty()  # Информация о телефоне
  phone_number = db.PhoneNumberProperty()
 
  # Данные о компании
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()

Все замечательно, ваши пользователи приступят к работе с адресной книгой и начнется процесс наполнения хранилища новыми данными. Через некоторое время после публикации приложения вы слышите от пользователей, что им недостаточно одного поля с номером телефона. К примеру, звучат пожелания о необходимости добавления возможности сохранения рабочего телефонного номера в дополнении к домашнему. Вы думаете: "Нет проблем, сейчас добавим поле для хранения рабочего телефона в нашу модель". И дополняете структуру следующим образом:

  # Информация о телефоне
  phone_number = db.PhoneNumberProperty()
  work_phone_number = db.PhoneNumberProperty()

Обновляем форму новыми полями и мы снова в деле! Вскоре после обновления приложения на сервере опять получаем кучу претензий. Все хотят видеть еще больше полей для описания разных номеров телефонов. Кто-то желает, чтобы мы добавили поле для номера факса, кому-то необходимо указывать номер мобильного телефона. Некоторые вообще хотят вносить несколько мобильных (есть же современные люди, которые обвешиваются мобильниками)! Конечно, вы можете добавить дополнительные поля для факса, второго мобильного, или даже двух. А что будем делать, если человек имеет три мобильника? А если десять? Что будем делать, когда человечество изобретет такой телефон, о котором мы не могли бы догадаться при разработке приложения?

Возникает вывод: необходимо использовать отношения между моделями.

Отношения один-к-многим

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

class Contact(db.Model):  # Основные данные
  name = db.StringProperty()
  birth_day = db.DateProperty()  # Информация об адресе
  address = db.PostalAddressProperty()  # Оригинальное свойство phone_number было заменено автоматически
  # созданным, имеющим название 'phone_numbers'.  # Данные о компании
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()
class PhoneNumber(db.Model):
  contact = db.ReferenceProperty(Contact,
                                 collection_name='phone_numbers')
  phone_type = db.StringProperty(
    choices=('home', 'work', 'fax', 'mobile', 'other'))
  number = db.PhoneNumberProperty()

Всю основную работу в этой схеме делает свойство contact. Определяя его как ReferenceProperty, вы создаете новое свойство, которое принимает исключительно ссылку на объект типа Contact. Каждый раз, когда вы определяете ссылочное свойство, оно автоматически создает дополнительный атрибут, являющийся списком дочерних объектов базовой модели. По умолчанию он имеет имя <имя-класса>_set. В нашем рассматриваемом случае будет создат атрибут с именем Contact.phonenumber_set. Однако, для последующего использования будет удобно, если мы присвоим ему свое имя phone_numbers. Вы можете перекрыть определение имени по умолчанию с использованием параметра collection_name в конструкторе класса ReferenceProperty.

Само создание взаимосвязей между контактом и одним из соответствующих ему номеров телефонов выполняется достаточно просто. Допустим, у нас есть контакт под именем "Scott", который имеет домашний и мобильный телефон. Вы создаете новый объект следующим способом:

scott = Contact(name='Scott')
scott.put()
PhoneNumber(contact=scott,
            phone_type='home',
            number='(650) 555 - 2200').put()
PhoneNumber(contact=scott,
            phone_type='mobile',
            number='(650) 555 - 2201').put()

Так как ReferenceProperty создаст специальный атрибут в модели Contact, будет очень просто произвести загрузку всех номеров телефонов заданного человека. К примеру, если необходимо это сделать, то воспользуемся следующим кодом:

print 'Content-Type: text/html'
print
for phone in scott.phone_numbers: 
  print '%s: %s' % (phone.phone_type, phone.number)

Он выведет результат:

home: (650) 555 - 2200
mobile: (650) 555 - 2201

Примечание: Вывод номеров телефонов может происходить в любом порядке, так как мы не задали соответствующую сортировку.

Виртуальный атрибут phone_numbers является экземпляром класса Query, который выполняет запрос к коллекции объектов, ассоциированных с данными класса Contact. К примеру, если существует необходимость получить все домашние номера телефонов, то это можно сделать выражением:

scott.phone_numbers.filter('phone_type =', 'home')

Когда Скот потеряет свой телефон, достаточно будет просто удалить соответствующий ему объект. После удаления такого экземпляра класса PhoneNumber, он более не будет попадать в результаты выборки:

jack.phone_numbers.filter('phone_type =', 'home').get().delete()

Отношения многие-к-многим

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

class Group(db.Model):  name = db.StringProperty()
  description = db.TextProperty()

Теперь вы можете создать новое свойство типа ReferenceProperty в модели Contact, которое будет относиться к его группе. Однако, такой алгоритм позволит контакту участвовать только в одной группе. К примеру, если кто-нибудь захочет указать, что его коллега также является другом, то не сможет этого сделать. В этом случае необходимо использовать отношения многие-к-многим.

Списки ключей

Самым простым способом сделать это - создать список ключей соответствующих объектов:

    class Contact(db.Model):
      # Пользователь, которому принадлежит этот контакт.
      owner = db.UserProperty()      # Основные данные
      name = db.StringProperty()
      birth_day = db.DateProperty()      # Информация об адресе
      address = db.PostalAddressProperty()      # Данные о компании
      company_title = db.StringProperty()
      company_name = db.StringProperty()
      company_description = db.StringProperty()
      company_address = db.PostalAddressProperty()      # Относится к группе
      groups = db.ListProperty(db.Key)

Добавление и удаление контакта из группы сведется к работе со списком ключей:

friends = Group.gql("WHERE name = 'friends'").get()
mary = Contact.gql("WHERE name = 'Mary'").get()
if friends.key() not in mary.groups:
  mary.groups.append(friends.key())
  mary.put()

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

class Group(db.Model):
  name = db.StringProperty()
  description = db.TextProperty()  @property
  def members(self):
    return Contact.gql("WHERE groups = :1", self.key())

При определении отношения многие-к-многим таким способом существует несколько ограничений. Во-первых, вы должны явно производить загрузку всех значений списка объектов Key. Другое наиболее важное замечание: что из-за архитектурных особенностей вы должны всячески избегать хранения больших списков ключей в свойстве типа ListProperty. Это значит, что необходимо грамотно выбирать тип объекта, со стороны которого будет моделироваться связь с другими таким образом, чтобы список содержал как можно меньше значений. К примеру описанному выше, для свойства со списком выбрана модель Contact, так как обычно контакт не принадлежит большому количеству групп, однако сама группа в больших массивах данных наоборот может содержать тысячи зависимых объектов.

Модели отношений

Одним из пользователей вашего приложения является продвинутый менеджер отдела продаж, который знает кучу людей, работающих в одной компании. Он расстроен необходимостью при заведении каждого контакта вводить снова и снова полную информацию об этой организации. Нельзя ли ввести ее один раз, а потом указывать что конкретный человек принадлежит к соответствующей организации? Что может быть проще, думаем мы, достаточно будет создать отношение один-к-многим между моделями Contact и Company, но корни проблемы оказываются зарыты гораздо глубже. Некоторые его клиенты работают в нескольких компаниях и занимают там различные должности. Что же делать?

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

class Contact(db.Model):
  # Пользователь, которому принадлежит этот контакт.
  owner = db.UserProperty()  # Основные данные
  name = db.StringProperty()
  birth_day = db.DateProperty()  # Информация об адресе
  address = db.PostalAddressProperty()  # Оригинальные свойства с информацией о компании были
  # заменены автоматически созданным атрибутом 'companies'.   # Отношение к группе контактов
  groups = db.ListProperty(db.Key)class Company(db.Model):
  name = db.StringProperty()
  description = db.StringProperty()
  company_address = db.PostalAddressProperty()class ContactCompany(db.Model):
  contact = db.ReferenceProperty(Contact,
                                 required=True,
                                 collection_name='companies')
  company = db.ReferenceProperty(Company,
                                 required=True,
                                 collection_name='contacts')
  title = db.StringProperty()

Добавление сотрудника в компанию осуществляется при помощи создания нового объекта ContactCompany:

mary = Contact.gql("name = 'Mary'").get()
google = Company.gql("name = 'Google'").get()
ContactCompany(contact=mary,
               company=google,
               title='Engineer').put()

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

Заключение

Платформа App Engine позволяет осуществлять построение простых в использовании отношений между объектами, которые будут соответствовать реальным взаимосвязям внутри данных. Используйте свойство типа ReferenceProperty при необходимости ассоциировать произвольное количество повторяющихся типов данных в одном объекте. Используйте списки ключей при необходимости сопоставления множества объектов с другим множеством. Вы увидите, что эти два подхода являются базовыми для определения сложных схем данных в реальных приложениях.