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

Original author: Nick Johnson
  • Translation
Одной из проблем, с которой сталкиваются 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.

*имеется в виду паттерн, разработанный автором статьи для извлечения сущностей из ссылок без выполнения лишних запросов к хранилищу. Если кратко, то ссылочное поле не загружает сущность сразу, и при обращению к атрибуту или методу ссылки будет вполнен запрос. Чтобы минимизировать количество запросов, паттерн загружает сущности по ссылкам за один раз (прим. переводчика).
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 7

    +1
    >Другой подход заключается в том, что на одной стороне отношения хранится список ключей сущностей другой стороны.
    >Это полезно, когда количество хранимых элементов заведемо ограничен

    то же самое касается и отношений child-parent. если повесить много чайлдов, при чем активно обновляющихся, на одного парента — будет ой.
      0
      > pets = Owner.owner_set.fetch(100)
      должно быть
      pets = Owner.pet_set.fetch(100)
      у Nick'а опечатка
        0
        спасибо, полезно
          0
          Подскажите пожалуйста — возможна ли а GAE реализация
          запроса с упорядоченными элементами?
          как аналог например такого запроса SQL: SELECT * FROM table ORDER BY id DESC LIMIT 10
          При условии что в таблице более 1 мл записей
            0
            см. чуть ниже, комментарий не туда вставился
            0
            Запрос может быть выполнен только по индексу, и никак иначе. А индекс всегда сортирован в прямом или обратном порядке, в зависимости от настроек этого индекса. Запрос по не индексированным полям невозможен.
              –1
              Жутко не понравилось. Не нужно учить тому, как перенести sql схему работы с данными в gae. Не реляционная бд там и подход этот ни к чему хорошему не приведёт.

              Ожидал разъяснение того, что бд гаи по сути key-value. Ожидал паттерны реализации отношений в рамках key-value.

              Only users with full accounts can post comments. Log in, please.