Одной из проблем, с которой сталкиваются GAE-разработчики, привыкшие работать с реляционными СУБД и ORM, являются ссылки и отношения в App Engine. В данном руководстве рассматриваются два вопроса: во-первых, что вообще представляют из себя отношения в СУБД?; во-вторых, как ими пользоватья в GAE?
СУБД оперируют несколькими видами отношений — «один-к-одному», «один-ко-многим» и «многие-ко-многим». Не смотря на отличия в терминологии, отношения работают по тем же принципам, что и ссылки. Ссылка — это поле сущности, содержащее ключ другой сущности — например, если «питомец» ссылается на «владельца», то это означает, что сущность «питомец» имеет поле, содержащее ключ сущности «владелец».
Все виды отношений можно представить как ссылки. Тип «один-ко-многим» в самой простой форме — ссылка: каждый «питомец» имеет свего «владельца», поэтому «владелец» может иметь нескольких «питомцев», ссылающихся на него. При этом сам «владелец» не меняется — на него опираются отдельные «питомцы», назвавшие его своим хозяином.
Отношения «один-к-одному» — это «один-ко-многим» с дополнительным ограничением, что есть только один «питомец», ссылающийся на «владельца». Это ограничение можно усилить хранением перекрестных ссылок (поля-ссылки друг на друга в каждой сущности).
Отношения «многие-ко-многим» немного сложнее. Их можно реализовать несколькими способами, но все они сводятся к списку пар ссылок. Рассмотрим в качестве примера веб-страницу. Каждая из страниц имеет множество входящих и исходящих ссылок. Их можно представить списком пар вида (from_url, to_url). В реляционных СУБД подобные соответствия хранятся в отдельных таблицах, которые присоединяются в запросах для поиска связанных записей.
Теперь рассмотрим, как вышеописанные типы связей работают в App Engine. Вообще, зачастую бывает полезным избавиться от терминологии «один-ко-многим» и др. и рассматривать сущности с объектно-ориентированной точки зрения. Поставим вопрос иначе: как одна сущность должна ссылаться на другую, чтобы соответствовать вашей структуре данных?
Этот тип отношений легко реализуется в любой системе. Платформа App Engine обеспечивает хранение ключа стороны «один» в сущности со стороны «многие». В Питоне для этого используется поле-ссылка ReferenceProperty:
Чтобы найти «владельца» для «питомца», мы обращаемся к атрибуту pet.owner, и App Engine автоматически загружает сущность, на которую мы ссылаемся. Чтобы найти всех «питомцев», ссылающихся на конкретного «владельца», достаточно выполнить следующий запрос:
Аналогичный результат можно получить проще: ReferenceProperty автоматически создает свойство в классе Owner для быстрого и удобного доступа к связанным данным, так что извлечь список «питомцев» можно так:
По умолчанию, App Engine именует это свойство как имя поля + "_set", но вы можете задать свое собственное:
Другой способ моделирования отношения «один-ко-многим» — это привязка сущности к родителю. В момент создания сущности ей может быть назначен родитель. При этом ключ сущности-родителя становится частью ключа-потомка и не может быть изменен в будущем. Вот как это выглядит в нашем примере:
Далее мы нигде явно не указываем связь между сущностями — она следует из указания родителя на момент создания. Когда лучше использовать привязку к родителю (parent) вместо поля-ссылки (ReferenceProperty)? Это влияет на работу транзакций: в App Engine в каждой отдельной транзакции можно оперировать сущностями только одной группы, т.е. множеством сущностей с родителем из той же группы. Если требуется, чтобы в транзакцию не попадали связанные сущности, используйте поле-ссылку. Кроме того, помните, что сущность может иметь только одного непосредственного родителя, и его ключ не может быть изменен после создания.
Отношения «один-к-одному» являются частным случаем отношений «один-ко-многим». Они осуществляются хранением на стороне «один» поля-ссылки на другую сущность.
«Многие-ко-многим» наиболее сложны в реализации. Для App Engine есть несколько решений их построения. Наиболее очевидный подход — аналогичная реляционным БД таблица связей, которая содержит пары ключей для обеих сторон отношения. Для нашего примера с «питомцами/владельцами» она будт выглядеть так:
Достоинства этого способа в том, что в отношения можно добавить дополнительные свойства — например, при моделировании связей ссылок страницы вы можете добавить в отношение поле с текстом ссылки. Доступ к данным осуществляется поэтапно: находятся связанные пары, из которых потом извлекаются искомые сущности. В примере задействован паттерн пакетного извлечения сущностей из ссылок, описаный в этой статье*:
Извлечение сущностей в другом направлении (от «владельца» к «питомцам») осуществляется аналогично.
Другой подход заключается в том, что на одной стороне отношения хранится список ключей сущностей другой стороны. Это полезно, когда количество хранимых элементов заведемо ограничено (скажем, несколько сотен или меньше). С таким списком удобно производить пакетные операции. Например:
Из каждого «владельца» можно извлечь список его «питомцев»:
А чтобы найти всех «владельцев» для заданного «питомца», выполните такой запрос:
И наконец, наиболее производительным и гибким может оказться гибридный подход. На эту тему советую посмотреть замечательный доклад Бретта Слаткинса Разработка сложных масштабируемых приложений на App Engine.
* — имеется в виду паттерн, разработанный автором статьи для извлечения сущностей из ссылок без выполнения лишних запросов к хранилищу. Если кратко, то ссылочное поле не загружает сущность сразу, и при обращению к атрибуту или методу ссылки будет вполнен запрос. Чтобы минимизировать количество запросов, паттерн загружает сущности по ссылкам за один раз (прим. переводчика).
Виды отношений
СУБД оперируют несколькими видами отношений — «один-к-одному», «один-ко-многим» и «многие-ко-многим». Не смотря на отличия в терминологии, отношения работают по тем же принципам, что и ссылки. Ссылка — это поле сущности, содержащее ключ другой сущности — например, если «питомец» ссылается на «владельца», то это означает, что сущность «питомец» имеет поле, содержащее ключ сущности «владелец».
Все виды отношений можно представить как ссылки. Тип «один-ко-многим» в самой простой форме — ссылка: каждый «питомец» имеет свего «владельца», поэтому «владелец» может иметь нескольких «питомцев», ссылающихся на него. При этом сам «владелец» не меняется — на него опираются отдельные «питомцы», назвавшие его своим хозяином.
Отношения «один-к-одному» — это «один-ко-многим» с дополнительным ограничением, что есть только один «питомец», ссылающийся на «владельца». Это ограничение можно усилить хранением перекрестных ссылок (поля-ссылки друг на друга в каждой сущности).
Отношения «многие-ко-многим» немного сложнее. Их можно реализовать несколькими способами, но все они сводятся к списку пар ссылок. Рассмотрим в качестве примера веб-страницу. Каждая из страниц имеет множество входящих и исходящих ссылок. Их можно представить списком пар вида (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.
* — имеется в виду паттерн, разработанный автором статьи для извлечения сущностей из ссылок без выполнения лишних запросов к хранилищу. Если кратко, то ссылочное поле не загружает сущность сразу, и при обращению к атрибуту или методу ссылки будет вполнен запрос. Чтобы минимизировать количество запросов, паттерн загружает сущности по ссылкам за один раз (прим. переводчика).