Как мне загрузить данные в соотношении "многие ко многим" с помощью пони-орма?

Вот мои сущности:

class Article(db.Entity):
    id = PrimaryKey(int, auto=True)
    creation_time = Required(datetime)
    last_modification_time = Optional(datetime, default=datetime.now)
    title = Required(str)
    contents = Required(str)
    authors = Set('Author')


class Author(db.Entity):
    id = PrimaryKey(int, auto=True)
    first_name = Required(str)
    last_name = Required(str)
    articles = Set(Article)

И вот код, который я использую, чтобы получить некоторые данные:

return left_join((article, author) for article in entities.Article
                 for author in article.authors).prefetch(entities.Author)[:]

Использую ли я метод предварительной выборки или нет, сгенерированный sql всегда выглядит одинаково:

SELECT DISTINCT "article"."id", "t-1"."author"
FROM "article" "article"
  LEFT JOIN "article_author" "t-1"
    ON "article"."id" = "t-1"."article"

И затем, когда я перебрал результаты, пони выдает еще один запрос (запросы):

SELECT "id", "creation_time", "last_modification_time", "title", "contents"
FROM "article"
WHERE "id" = %(p1)s

SELECT "id", "first_name", "last_name"
FROM "author"
WHERE "id" IN (%(p1)s, %(p2)s)

Для меня желаемое поведение было бы, если бы orm выдавал только один запрос, который бы загружал все необходимые данные. Так как мне этого добиться?

2 ответа

Решение

Автор PonyORM здесь. Мы не хотим загружать все эти объекты, используя только один запрос, потому что это неэффективно.

Единственное преимущество использования одного запроса для загрузки отношения "многие ко многим" - это уменьшение количества обращений к базе данных. Но если мы заменим три запроса одним, это не будет серьезным улучшением. Когда ваш сервер базы данных расположен рядом с вашим сервером приложений, эти обходы выполняются очень быстро, по сравнению с обработкой полученных данных в Python.

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

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

  2. Сервер базы данных обычно написан на скомпилированном языке, таком как C, и работает очень быстро. То же самое верно для сетевого уровня. Но код Python интерпретируется, и время, потребляемое кодом Python, (вопреки некоторым мнениям) обычно намного больше, чем время, которое проводится в базе данных. Вы можете увидеть тесты профилирования, которые были выполнены автором SQLAlchemy Майком Байером, после чего он пришел к выводу:

    Большое заблуждение, с которым я часто сталкиваюсь, заключается в том, что общение с базой данных занимает большую часть времени, проводимого в приложении Python, ориентированном на базу данных. Возможно, это распространенная мудрость в скомпилированных языках, таких как C или, может быть, даже в Java, но, как правило, не в Python. Python очень медленный, по сравнению с такими системами (...) Независимо от того, написан ли драйвер базы данных (DBAPI) на чистом Python или на C, это потребует значительных дополнительных затрат на уровне Python. Только для DBAPI это может быть на порядок медленнее.

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

    В качестве поддержки моих слов я могу указать на Django ORM. Этот ORM имеет два метода, которые можно использовать для оптимизации запросов. Первый, называемый select_related, загружает все связанные объекты в одном запросе, а недавно добавленный метод prefetch_related загружает объекты так, как это делает Pony по умолчанию. По словам пользователей Django, второй метод работает намного быстрее:

    В некоторых сценариях мы обнаружили улучшение скорости до 30%.

  3. База данных требуется для выполнения соединений, которые потребляют драгоценные ресурсы сервера базы данных.

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

    Когда база данных выполняет объединение, она должна тратить на это дополнительное время. Но для Pony не имеет значения, будет ли база данных объединяться или нет, потому что в любом случае объект будет связан внутри карты идентификации ORM. Таким образом, работа, выполняемая базой данных при выполнении объединения, является просто бесполезной тратой времени на базу данных. С другой стороны, используя шаблон карты идентичности, Pony может одинаково быстро связывать объекты независимо от того, представлены они в одной строке базы данных или нет.

Возвращаясь к количеству циклов, Pony имеет специальный механизм для устранения проблемы "N+1 запрос". Анти-паттерн "N+1 запрос" возникает, когда ORM отправляет сотни очень похожих запросов, каждый из которых загружает отдельный объект из базы данных. Многие ORM страдают от этой проблемы. Но Pony может обнаружить его и заменить повторяющиеся N запросов одним запросом, который загружает все необходимые объекты одновременно. Этот механизм очень эффективен и может значительно сократить количество поездок в оба конца. Но когда мы говорим о загрузке отношения "многие ко многим", здесь нет N запросов, есть только три запроса, которые более эффективны при отдельном выполнении, поэтому нет смысла пытаться выполнить один запрос вместо этого.

Подводя итог, я должен сказать, что производительность ORM очень важна для нас, разработчиков Pony ORM. И поэтому мы не хотим реализовывать загрузку отношения "многие ко многим" в одном запросе, поскольку это, безусловно, будет медленнее, чем наше текущее решение.

Таким образом, чтобы ответить на ваш вопрос, вы не можете загрузить обе стороны отношения "многие ко многим" в одном запросе. И я думаю, что это хорошо.

Это должно работать

python from pony.orm import select select((article, author) for article in Article if Article.authors == Authors.id)

Другие вопросы по тегам