Pull to refresh

Моделирование отношений в App Engine

Reading time5 min
Views1.2K
Original author: Nick Johnson
Одной из проблем, с которой сталкиваются GAE-разработчики, привыкшие работать с реляционными СУБД и ORM, являются ссылки и отношения в App Engine. В данном руководстве рассматриваются два вопроса: во-первых, что вообще представляют из себя отношения в СУБД?; во-вторых, как ими пользоватья в GAE?

Виды отношений



СУБД оперируют несколькими видами отношений — «один-к-одному», «один-ко-многим» и «многие-ко-многим». Не смотря на отличия в терминологии, отношения работают по тем же принципам, что и ссылки. Ссылка — это поле сущности, содержащее ключ другой сущности — например, если «питомец» ссылается на «владельца», то это означает, что сущность «питомец» имеет поле, содержащее ключ сущности «владелец».

Все виды отношений можно представить как ссылки. Тип «один-ко-многим» в самой простой форме — ссылка: каждый «питомец» имеет свего «владельца», поэтому «владелец» может иметь нескольких «питомцев», ссылающихся на него. При этом сам «владелец» не меняется — на него опираются отдельные «питомцы», назвавшие его своим хозяином.

Отношения «один-к-одному» — это «один-ко-многим» с дополнительным ограничением, что есть только один «питомец», ссылающийся на «владельца». Это ограничение можно усилить хранением перекрестных ссылок (поля-ссылки друг на друга в каждой сущности).

Отношения «многие-ко-многим» немного сложнее. Их можно реализовать несколькими способами, но все они сводятся к списку пар ссылок. Рассмотрим в качестве примера веб-страницу. Каждая из страниц имеет множество входящих и исходящих ссылок. Их можно представить списком пар вида (from_url, to_url). В реляционных СУБД подобные соответствия хранятся в отдельных таблицах, которые присоединяются в запросах для поиска связанных записей.

Теперь рассмотрим, как вышеописанные типы связей работают в App Engine. Вообще, зачастую бывает полезным избавиться от терминологии «один-ко-многим» и др. и рассматривать сущности с объектно-ориентированной точки зрения. Поставим вопрос иначе: как одна сущность должна ссылаться на другую, чтобы соответствовать вашей структуре данных?

Отношения в App Engine



Один-ко-многим



Этот тип отношений легко реализуется в любой системе. Платформа App Engine обеспечивает хранение ключа стороны «один» в сущности со стороны «многие». В Питоне для этого используется поле-ссылка ReferenceProperty:

class Owner(db.Model):
  name = db.StringProperty()

class Pet(db.Model):
  name = db.StringProperty()
  owner = db.ReferenceProperty(Owner)


Чтобы найти «владельца» для «питомца», мы обращаемся к атрибуту pet.owner, и App Engine автоматически загружает сущность, на которую мы ссылаемся. Чтобы найти всех «питомцев», ссылающихся на конкретного «владельца», достаточно выполнить следующий запрос:

pets = Pet.all().filter('owner =', owner).fetch(100)


Аналогичный результат можно получить проще: ReferenceProperty автоматически создает свойство в классе Owner для быстрого и удобного доступа к связанным данным, так что извлечь список «питомцев» можно так:

pets = Owner.owner_set.fetch(100)


По умолчанию, App Engine именует это свойство как имя поля + "_set", но вы можете задать свое собственное:

class Pet(db.Model):
  name = db.StringProperty()
  owner = db.ReferenceProperty(Owner, collection_name='pets')

pets = owner.pets.fetch(100)


Другой способ моделирования отношения «один-ко-многим» — это привязка сущности к родителю. В момент создания сущности ей может быть назначен родитель. При этом ключ сущности-родителя становится частью ключа-потомка и не может быть изменен в будущем. Вот как это выглядит в нашем примере:

class Owner(db.Model):
  name = db.StringProperty()

class Pet(db.Model):
  name = db.StringProperty()

bob = Owner(name='Bob')
felix = Pet(name='Felix', parent=bob)

owner_of_felix = felix.parent


Далее мы нигде явно не указываем связь между сущностями — она следует из указания родителя на момент создания. Когда лучше использовать привязку к родителю (parent) вместо поля-ссылки (ReferenceProperty)? Это влияет на работу транзакций: в App Engine в каждой отдельной транзакции можно оперировать сущностями только одной группы, т.е. множеством сущностей с родителем из той же группы. Если требуется, чтобы в транзакцию не попадали связанные сущности, используйте поле-ссылку. Кроме того, помните, что сущность может иметь только одного непосредственного родителя, и его ключ не может быть изменен после создания.

Один-к-одному



Отношения «один-к-одному» являются частным случаем отношений «один-ко-многим». Они осуществляются хранением на стороне «один» поля-ссылки на другую сущность.

Многие-ко-многим



«Многие-ко-многим» наиболее сложны в реализации. Для App Engine есть несколько решений их построения. Наиболее очевидный подход — аналогичная реляционным БД таблица связей, которая содержит пары ключей для обеих сторон отношения. Для нашего примера с «питомцами/владельцами» она будт выглядеть так:

class Owner(db.Model):
  name = db.StringProperty()

class Pet(db.Model):
  name = db.StringProperty()

class PetOwner(db.Model):
  pet = db.ReferenceProperty(Pet, collection_name='owners')
  owner = db.ReferenceProperty(Owner, collection_name='pets')


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

petowners = felix.owners.fetch(100)
prefetch_refprops(owners, 'owner')
owners = [x.owner for x in petowners]


Извлечение сущностей в другом направлении (от «владельца» к «питомцам») осуществляется аналогично.

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

class Pet(db.Model):
  name = db.StringProperty()

class Owner(db.Model):
  name = db.StringProperty()
  pets = db.ListProperty(db.Key)


Из каждого «владельца» можно извлечь список его «питомцев»:

pets = db.get(bob.pets)


А чтобы найти всех «владельцев» для заданного «питомца», выполните такой запрос:

owners = Owner.all().filter('pets =', felix).fetch(100)


И наконец, наиболее производительным и гибким может оказться гибридный подход. На эту тему советую посмотреть замечательный доклад Бретта Слаткинса Разработка сложных масштабируемых приложений на App Engine.

*имеется в виду паттерн, разработанный автором статьи для извлечения сущностей из ссылок без выполнения лишних запросов к хранилищу. Если кратко, то ссылочное поле не загружает сущность сразу, и при обращению к атрибуту или методу ссылки будет вполнен запрос. Чтобы минимизировать количество запросов, паттерн загружает сущности по ссылкам за один раз (прим. переводчика).
Tags:
Hubs:
+21
Comments7

Articles