» » Делаем GraphQL API на PHP и MySQL. Часть 3: Решение проблемы N+1 запросов

 

Делаем GraphQL API на PHP и MySQL. Часть 3: Решение проблемы N+1 запросов

Автор: admin от 25-05-2017, 19:55, посмотрело: 611

Делаем GraphQL API на PHP и MySQL. Часть 3: Решение проблемы N+1 запросов

В этой, третьей по счету, статье о создании GraphQL сервера с graphql-php я расскажу о том как бороться с проблемой N+1 запросов.

Предисловие


Я продолжу изменять код, полученный по окончании предыдущей статьи. Также его можно посмотреть в репозитории статьи на Github. Если вы еще не читали предыдущие статьи, то рекомендую ознакомиться с ними прежде чем продолжать.

Также я закомментирую две строки в graphql.php, добавляющие ограничение на максимальную глубину и сложность запроса, так как при разработке они не очень нужны, а могут лишь вызывать проблемы отладки:

// DocumentValidator::addRule('QueryComplexity', new QueryComplexity(6));
// DocumentValidator::addRule('QueryDepth', new QueryDepth(1));


Проблема N+1 запросов


Проблема


Легче всего объяснить в чем заключается проблема N+1 запросов на примере. Допустим вам надо запросить список статей и их авторов. Не долго думая можно сделать это так:

$articles = DB::table('articles')->get();
foreach ($articles as &$article) {
    $article->author = DB::table('users')->where('id', $article->author_id)->first();
}

Как правило DB::table('articles')->get() в итоге посылает в базу данных один, примерно такой, запрос:

SELECT * FROM articles;

И затем в цикле отправляется еще N запросов в БД:

SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
SELECT * FROM users WHERE id = 4;
SELECT * FROM users WHERE id = 5;
...
SELECT * FROM users WHERE id = N;

Где N — количество полученных в первом запросе статей.

Например мы выполняем один запрос, который возвращает нам 100 статей, и затем для каждой статьи мы выполняем еще по одному запросу автора. В сумме получается 100+1=101 запрос. Это является лишней нагрузкой на сервер БД и называется проблемой N+1 запросов.

Решение


Самый распространенный метод решения этой проблемы — это группировка запросов.

Если переписать тот же пример используя группировку запросов, то мы получим примерно такой код:

$articles = DB::table('articles')->get();
$authors_ids = get_authors_ids($articles);
$authors = DB::table('users')->whereIn('id', $authors_ids)->get();
foreach ($articles as &$article) {
    $article->author = search_author_by_id($authors, $article->author_id);
}

То есть мы делаем следующее:


  1. Запрашиваем массив статей

  2. Запоминаем id всех авторов данных статей

  3. Запрашиваем массив пользователей по этим id

  4. Вставляем авторов в статьи из массива пользователей


При этом, сколько бы статей мы ни запросили, в БД отправится всего два запроса:

SELECT * FROM articles;
SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5, ..., N);


Проблема N+1 запросов в GraphQL


Теперь давайте вернемся к нашему GraphQL серверу в том состоянии, котором он находится после предыдущей статьи, и обратим внимание на то, как реализован запрос количества друзей пользователя.

Если мы запрашиваем список пользователей с указанием количества друзей каждого, то сначала GraphQL сервер запросит все записи из таблицы пользователей:

'allUsers' => [
    'type' => Types::listOf(Types::user()),
    'description' => 'Список пользователей',
    'resolve' => function () {
        return DB::select('SELECT * from users');
    }
]

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

'countFriends' => [
    'type' => Types::int(),
    'description' => 'Количество друзей пользователя',
    'resolve' => function ($root) {
        return DB::affectingStatement("SELECT u.* FROM friendships f JOIN users u ON u.id = f.friend_id WHERE f.user_id = {$root->id}");
    }
]

Как раз тут и проявляется проблема N+1 запросов.

Чтобы решить эту проблему методом группировки запросов graphql-php предлагает нам отлаживать выполнение ресолверов полей.

Идея проста: вместо результата, функция «resolve» поля должна возвращать объект класса GraphQLDeferred, в конструктор которого передается функция для получения того самого результата.

То есть теперь мы можем подключить класс Deferred:

use GraphQLDeferred;

И отложить выполнение, переписав ресолвер поля «countFriends» следующим образом:

'countFriends' => [
    'type' => Types::int(),
    'description' => 'Количество друзей пользователя',
    'resolve' => function ($root) {
        return new Deferred(function () use ($root) {
            return DB::affectingStatement("SELECT u.* FROM friendships f JOIN users u ON u.id = f.friend_id WHERE f.user_id = {$root->id}");
        });
    }
]

Но просто отложив выполнение запроса мы не решим проблему N+1. Поэтому нам надо создать буфер, который будет накапливать в себе id всех пользователей, для которых надо запросить количество друзей, и в дальнейшем сможет вернуть результаты по всем пользователям.

Для этого я создам небольшой класс, который будет иметь три простых статических метода:


  • add — Добавление id пользователя в буфер

  • load — Загрузка количества друзей из БД для всех пользователей в буфере

  • get — Получение количества друзей пользователя из буфера


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


Теперь подключим наш буфер в UserType.php:

use AppBuffer;

И снова перепишем ресолвер для поля «countFriends»:

'countFriends' => [
    'type' => Types::int(),
    'description' => 'Количество друзей пользователя',
    'resolve' => function ($root) {
        // Добавляем id пользователя в буфер
        Buffer::add($root->id);
        return new Deferred(function () use ($root) {
            // Загружаем результаты в буфер из БД (если они еще не были загружены)
            Buffer::load();
            // Получаем количество друзей пользователя из буфера
            return Buffer::get($root->id);
        });
    }
],

Готово. Теперь при выполнении запроса:

Делаем GraphQL API на PHP и MySQL. Часть 3: Решение проблемы N+1 запросов


Количество друзей для всех пользователей будет получаться из базы данных только один раз. Причем запрос данных о количестве друзей будет выполняться всего один раз даже при таком запросе GraphQL:

Делаем GraphQL API на PHP и MySQL. Часть 3: Решение проблемы N+1 запросов


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

Заключение


На этом все. Предлагайте свои варианты решения подобных проблем и задавайте вопросы если они возникнут.

Исходный код из статьи на Github.

Другие части данной статьи:


  • Установка, схема и запросы

  • Мутации, переменные, валидация и безопасность

  • Решение проблемы N+1 запросов


  • Источник: Хабрахабр

    Теги: PHP, MySQL, GraphQL

    Категория: Программирование

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *