Очень распространенный случай оптимизации запросов с решением, которое также легко на глазах

Проблема запроса n + 1 - одно из наиболее распространенных узких мест масштабируемости. Он включает в себя выборку списка ресурсов из базы данных, которая включает в себя другие связанные с ними ресурсы. Это означает, что нам, возможно, придется отдельно запрашивать связанные ресурсы. Итак, если у вас есть список из n родительских объектов, необходимо будет выполнить еще n запросов для получения связанных ресурсов. Давайте попробуем решить эту загадку O (n).

Если вы хорошо знакомы с Rails, сериализаторами активных моделей и уже имеете хорошее представление о том, в чем будет заключаться наша проблема, то, возможно, вы можете сразу перейти к коду здесь.

Конкретный пример

Предположим, вы загружаете массив объектов Post в конечной точке GET. Вы также хотите загрузить соответствующих авторов сообщений, встраивая объект author в каждый из объектов сообщения. Вот наивный способ сделать это:

Для каждого из n обрабатываемых объектов Post будет запущен запрос для получения соответствующего объекта User. Следовательно, мы выполним в общей сложности n + 1 запрос. Это ужасно. И вот как это исправить, нетерпеливо загрузив объект User:

Когда простое соединение невозможно

До сих пор для опытных не было ничего нового.

Но давайте усложним это. Предположим, что пользователи сайта не хранятся в той же RDMS, что и сообщения. Скорее, пользователи - это документы, хранящиеся в MongoDB (по какой-либо причине). Как изменить наш сериализатор Post, чтобы оптимально получать информацию о пользователе сейчас? Это будет возвращение к исходной точке:

Трудное положение, в котором наши пользователи теперь находятся в базе данных Mongo, можно заменить, скажем, вызовом сторонней HTTP-службы для выборки пользователей или их сохранения в совершенно другой RDMS. Наша основная проблема по-прежнему заключается в том, что невозможно "присоединить" хранилище данных пользователей к таблице сообщений и получить желаемый ответ в одном запросе.

Конечно, мы можем лучше. Мы можем получить весь ответ за два запроса:

  • Получить все сообщения без атрибута author (1 запрос SQL).
  • Получите всех соответствующих авторов, выполнив запрос с указанием идентификаторов пользователей, взятых из массива сообщений (1 запрос Mongo с предложением IN).

Введите пакетный загрузчик

Таким образом, наша первоначальная проблема оптимизации была сведена к как сделать этот код читаемым и поддерживаемым. Ребята из Universe придумали изящную жемчужину. Batch Loader мне очень помог.

gem 'batch-loader'

bundle install

Если вы знакомы с обещаниями JavaScript, подумайте о методе get_author_lazily как о возвращении обещания, которое оценивается позже. Думаю, это хорошая аналогия, поскольку BatchLoader использует ленивые объекты Ruby. По умолчанию BatchLoader кэширует загруженные значения, поэтому, чтобы ответы оставались актуальными, вы должны добавить это в свой config/application.rb:

config.middleware.use BatchLoader::Middleware

Вот и все! Мы решили расширенную версию проблемы n + 1 запросов, сохранив при этом чистый код и правильно используя сериализаторы активных моделей.

Использование AMS для вложенных ресурсов

Но есть одна проблема. Если у вас есть сериализатор User (активные сериализаторы моделей также работают с Mongoid), он не будет вызываться для лениво загружаемых объектов author. , в отличие от раньше. Чтобы исправить это, мы можем использовать блок Ruby и сериализовать объекты author до того, как они будут «назначены» сообщениям.

Вот весь код. Наслаждаться!

Ресурсы

Https://github.com/rails-api/active_model_serializers

Https://gist.github.com/UsamaAshraf/95b0c8d0d64ee193148342a931c0a423

Https://www.universe.com/about

Https://github.com/exAspArk/batch-loader