Делаем GraphQL API на PHP и MySQL. Часть 1: Установка, схема и запросы

  • Tutorial
image

В последнее время я все чаще и чаще слышу про GraphQL. И в интернете уже можно найти немало статей о том как сделать свой GraphQL сервер. Но почти во всех этих статьях в качестве бэкенда используется Node.js.

Я ничего не имею против Node.js и сам с удовольствием использую его, но все-таки большую часть проектов я делаю на PHP. К тому же хостинг с PHP и MySQL гораздо дешевле и доступнее чем хостинг с Node.js. Поэтому мне кажется не справедливым тот факт, что об использовании GraphQL на PHP в интернете практически нет ни слова.

В данной статье я хочу рассказать о том, как сделать свой GraphQL сервер на PHP с помощью библиотеки graphql-php и как с его помощью реализовать простое API для получения данных из MySQL.

Я решил отказаться от использования какого-либо конкретного PHP фреймворка в данной статье, но после ее прочтения вам не составит особого труда применить эти знания в своем приложении. Тем более для некоторых фреймворков уже есть свои библиотеки основанные на graphql-php, которые облегчат вашу работу.

Подготовка


В данной статье я не буду делать фронтенд, поэтому для удобного тестирования запросов к GraphQL серверу рекомендую установить GraphiQL-расширение для браузера.

Для Chrome это могут быть:


Также понадобится создать таблицы в БД и заполнить их тестовым набором данных.

В таблице «users» будем хранить список пользователей:

Таблица users

А в таблице «friendships» связи типа «многие-ко-многим», которые будут обозначать дружбу между пользователями:

Таблица friendships

Дамп базы данных, как и весь остальной код, можно взять из репозитория данной статьи на Github.

Hello, GraphQL!


Для начала необходимо установить graphql-php в наш проект. Можно сделать это с помощью composer:

composer require webonyx/graphql-php

Теперь, по традиции напишем «Hello, World».

Для этого в корне создадим файл graphql.php, который будет служить конечной точкой (endpoint) нашего GraphQL сервера.

В нем подключим автозагрузчик composer:

require_once __DIR__ . '/vendor/autoload.php';

Подключим GraphQL:

use GraphQL\GraphQL;

Чтобы заставить GraphQL выполнить запрос необходимо передать ему сам запрос и схему данных.

Для получения запроса напишем следующий код:

$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];

Чтобы создать схему сначала подключим GraphQL\Schema:

use GraphQL\Schema;

Конструктор схемы принимает массив, в котором должен быть указан корневой тип данных Query, который служит для чтения данных вашего API, поэтому сначала создадим этот тип.

Примечание
Также схема может содержать необязательный корневой тип данных Mutation, который предоставляет API для записи данных, но в рамках данной статьи мы его рассматривать не будем.

В простейшем случае тип данных Query должен быть экземпляром класса ObjectType, а его поля должны быть простых типов (например int или string), поэтому подключим классы предоставляющие эти типы данных в GraphQL:

use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;

И создадим тип данных Query:

$queryType = new ObjectType([
    'name' => 'Query',
    'fields' => [
        'hello' => [
            'type' => Type::string(),
            'description' => 'Возвращает приветствие',
            'resolve' => function () {
                return 'Привет, GraphQL!';
            }
        ]
    ]
]);

Как можно заметить тип данных обязательно должен содержать имя (name) и массив полей (fields), а также можно указать необязательное описание (description).

Примечание
Также тип данных может содержать свойства «interfaces», «isTypeOf» и «resolveField», но в рамках данной статьи мы их рассматривать не будем.

Поля типа данных также должны иметь обязательные свойства «name» и «type». Если свойство «name» не задано, то в качестве имени используется ключ поля (в данном случае «hello»). Также в нашем примере у поля «hello» заданы необязательные свойства «description» — описание и «resolve» — функция возвращающая результат. В этом случае функция «resolve» просто возвращает строку "Привет, GraphQL!", но в более сложной ситуации она может получать какую-либо информацию по API или из БД и обрабатывать ее.

Примечание
Также поля могут содержать свойство args, которое будет рассмотрено в статье позже, и свойство deprecationReason, которое в данной статье не рассматривается.

Таким образом, мы создали корневой тип данных «Query», который содержит всего одно поле «hello», возвращающее простую строку текста. Давайте добавим его в схему данных:

$schema = new Schema([
    'query' => $queryType
]);

А затем выполним запрос GraphQL для получения результата:

$result = GraphQL::execute($schema, $query);

Остается только вывести результат в виде JSON и наше приложение готово:

header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);

Обернем код в блок try-catch, для обработки ошибок и в итоге код файла graphql.php будет выглядеть так:

graphql.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use GraphQL\GraphQL;
use GraphQL\Schema;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;

try {
    // Получение запроса
    $rawInput = file_get_contents('php://input');
    $input = json_decode($rawInput, true);
    $query = $input['query'];

    // Содание типа данных "Запрос"
    $queryType = new ObjectType([
        'name' => 'Query',
        'fields' => [
            'hello' => [
                'type' => Type::string(),
                'description' => 'Возвращает приветствие',
                'resolve' => function () {
                    return 'Привет, GraphQL!';
                }
            ]
        ]
    ]);

    // Создание схемы
    $schema = new Schema([
        'query' => $queryType
    ]);

    // Выполнение запроса
    $result = GraphQL::execute($schema, $query);
} catch (\Exception $e) {
    $result = [
        'error' => [
            'message' => $e->getMessage()
        ]
    ];
}

// Вывод результата
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);


Проверим работу GraphQL. Для этого запустим расширение для GraphiQL, установим endpoint (в моем случае это «localhost/graphql.php») и выполним запрос:

Проверка работы GraphQL

Вывод пользователей из БД


Теперь усложним задачу. Выведем список пользователей из базы данных MySQL.

Для этого нам понадобится создать еще один тип данных класса ObjectType. Чтобы не нагромождать код в graphql.php, вынесем все типы данных в отдельные файлы. А чтобы у нас была возможность использовать типы данных внутри самих себя, оформим их в виде классов. Например, чтобы в типе данных «user» можно было добавить поле «friends», которое будет являться массивом пользователей такого же типа «user».

Когда мы оформляем тип данных в виде класса, то не обязательно указывать у него свойство «name», потому что оно по умолчанию берется из названия класса (например у класса QueryType будет имя Query).

Теперь корневой тип данных Query, который был в graphql.php:

$queryType = new ObjectType([
    'name' => 'Query',
    'fields' => [
        'hello' => [
            'type' => Type::string(),
            'description' => 'Возвращает приветствие',
            'resolve' => function () {
                return 'Привет, GraphQL!';
            }
        ]
    ]
]);

Будет находиться в отдельном файле QueryType.php и выглядеть так:

class QueryType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'fields' => [
                'hello' => [
                    'type' => Type::string(),
                    'description' => 'Возвращает приветствие',
                    'resolve' => function () {
                        return 'Привет, GraphQL!';
                    }
                ]
            ]
        ];
        parent::__construct($config);
    }
}

А чтобы в дальнейшем избежать бесконечной рекурсии при определении типов, в свойстве «fields» лучше всегда указывать не массив полей, а анонимную функцию, возвращающую массив полей:

class QueryType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'fields' =>  function() {
                return [
                    'hello' => [
                        'type' => Type::string(),
                        'description' => 'Возвращает приветствие',
                        'resolve' => function () {
                            return 'Привет, GraphQL!';
                        }
                    ]
                ];
            }
        ];
        parent::__construct($config);
    }
}

При разработке проекта может появиться очень много типов данных, поэтому для них лучше создать отдельный реестр, который будет служить фабрикой для всех типов данных, в том числе и базовых, используемых в проекте. Давайте создадим папку App, а в ней файл, Types.php, который как раз и будет тем самым реестром для всех типов данных проекта.

Также в папке App создадим подпапку Type, в которой будем хранить все наши типы данных и перенесем в нее QueryType.php.

Теперь добавим пространство имен и заполним реестр Types.php необходимыми типами:

App/Types.php
<?php

namespace App;

use App\Type\QueryType;
use GraphQL\Type\Definition\Type;

class Types
{
    private static $query;

    public static function query()
    {
        return self::$query ?: (self::$query = new QueryType());
    }

    public static function string()
    {
        return Type::string();
    }
}


Пока в нашем реестре будет всего 2 типа данных: 1 простой (string) и 1 составной (query).

Теперь во всех остальных файлах вместо:

use GraphQL\Type\Definition\Type;

Подключим наш реестр типов:

use App\Types;

И заменим все ранее указанные типы, на типы из реестра.

В QueryType.php вместо:

Type::string()

Будет:

Types::string()

А схема в graphql.php теперь будет выглядеть так:

$schema = new Schema([
    'query' => Types::query()
]);

Чтобы получить пользователей из базы данных, необходимо обеспечить интерфейс доступа к ней. Получать данные из базы можно любым способом. В каждом фреймворке для этого есть свои инструменты. Для данной статьи я написал простейший интерфейс который может подключаться к MySQL базе данных и выполнять в ней запросы. Так как это не относится к GraphQL, то я не буду объяснять как реализованы методы в данном классе, а просто приведу его код:

App/DB.php
<?php

namespace App;

class DB
{
    private static $pdo;
    
    public static function init($config)
    {
        self::$pdo = new PDO("mysql:host={$config['host']};dbname={$config['database']}", $config['username'], $config['password']);
        self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
    }
    
    public static function selectOne($query)
    {
        $records = self::select($query);
        return array_shift($records);
    }
    
    public static function select($query)
    {
        $statement = self::$pdo->query($query);
        return $statement->fetchAll();
    }
    
    public static function affectingStatement($query)
    {
        $statement = self::$pdo->query($query);
        return $statement->rowCount();
    }
}


В файле graphql.php добавим код для инициализации подключения к БД:

// Настройки подключения к БД
$config = [
    'host' => 'localhost',
    'database' => 'gql',
    'username' => 'root',
    'password' => 'root'
];

// Инициализация соединения с БД
DB::init($config);

Теперь в папке Type создадим тип данных User, который будет отображать данные о пользователе. Код файла UserType.php будет таким:

App/Type/UserType.php
<?php

namespace App\Type;

use App\DB;
use App\Types;
use GraphQL\Type\Definition\ObjectType;

class UserType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'description' => 'Пользователь',
            'fields' => function() {
                return [
                    'id' => [
                        'type' => Types::string(),
                        'description' => 'Идентификатор пользователя'
                    ],
                    'name' => [
                        'type' => Types::string(),
                        'description' => 'Имя пользователя'
                    ],
                    'email' => [
                        'type' => Types::string(),
                        'description' => 'E-mail пользователя'
                    ],
                    'friends' => [
                        'type' => Types::listOf(Types::user()),
                        'description' => 'Друзья пользователя',
                        'resolve' => function ($root) {
                            return DB::select("SELECT u.* from friendships f JOIN users u ON u.id = f.friend_id WHERE f.user_id = {$root->id}");
                        }
                    ],
                    '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}");
                        }
                    ]
                ];
            }
        ];
        parent::__construct($config);
    }
}


Значение полей можно понять из их свойства «description». Свойства «id», «name», «email» и «countFriends» имеют простые типы, а свойство «friends» является списком друзей – таких же пользователей, поэтому имеет тип:

Types::listOf(Types::user())

Необходимо также добавить в наш реестр пару базовых типов, которые мы раньше не использовали:

public static function int()
{
    return Type::int();
}

public static function listOf($type)
{
    return Type::listOf($type);
}

И только, что созданный нами тип User:

private static $user;

public static function user()
{
    return self::$user ?: (self::$user = new UserType());
}

Возвращаемые значения (resolve) для свойств «friends» и «countFriends» берутся из базы данных. Анонимная функция в «resolve» первым аргументом получает значение текущего поля ($root), из которого можно узнать id пользователя для вставки его в запрос списка друзей.

Примечание
Я не уделял внимание экранированию запросов, потому как этот код написан в учебных целях, но на реальном проекте так делать, конечно, нельзя.

В завершении изменим код QueryType.php так, чтобы в API были поля для получения информации о конкретном пользователе по его идентификатору (поле «user»), а также для получения списка всех пользователей (поле «allUsers»):

App/Type/QueryType.php
<?php

namespace App\Type;

use App\DB;
use App\Types;
use GraphQL\Type\Definition\ObjectType;

class QueryType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'fields' => function() {
                return [
                    'user' => [
                        'type' => Types::user(),
                        'description' => 'Возвращает пользователя по id',
                        'args' => [
                            'id' => Types::int()
                        ],
                        'resolve' => function ($root, $args) {
                            return DB::selectOne("SELECT * from users WHERE id = {$args['id']}");
                        }
                    ],
                    'allUsers' => [
                        'type' => Types::listOf(Types::user()),
                        'description' => 'Список пользователей',
                        'resolve' => function () {
                            return DB::select('SELECT * from users');
                        }
                    ]
                ];
            }
        ];
        parent::__construct($config);
    }
}


Тут чтобы узнать идентификатор пользователя, данные которого необходимо получить, у поля «user» мы используем свойство «args», в котором содержится массив аргументов. Массив «args» передается в анонимную функцию «resolve» вторым аргументом, используя который мы узнаем id целевого пользователя.

Примечание
У аргументов могут быть свои свойства, но в этом случае я использую упрощенную форму записи массива аргументов, при которой ключи массива являются именами, а значения – типами аргументов:
'args' => [
    'id' => Types::int()
]

Вместо:
'args' => [
    'id' => [
        'type' => Types::int()
    ]
]


Теперь можно запустить сервер GraphQL и проверить его работу таким запросом:

Проверка работы GraphQL: Получение данных пользователя

Или таким:

Проверка работы GraphQL: Получение данных пользователя

Или любым другим.

Заключение


На этом все. Читайте документацию. Задавайте вопросы в комментариях. Критикуйте.

Также рекомендую почитать исходный код с комментариями на Github.

Другие части данной статьи:
  1. Установка, схема и запросы
  2. Мутации, переменные, валидация и безопасность
  3. Решение проблемы N+1 запросов
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 40
  • +1
    Однозначно пригодится! В закладки
    • –10
      NodeJS сможет с лёгкостью отправить одновременно несколько запросов на связанные сущьности. Например, если надо вытянуть данные человека, его друзей, его посты, сообщения. Запросы на друзей, сообщения и посты можно отправить параллельно. В PHP, насколько я знаю, такой возможности нет (за исключением особых извращений)
      • +4
        Ну во первых в этой статье я не говорю что надо использовать PHP, а не Node.js. Я просто рассказываю как использовать GraphQL, если вы используете PHP. А про преимущества и недостатки этих языков можно спорить бесконечно.
        Во вторых на PHP конечно нельзя параллельно выполнить подобные запросы. Но во многих случаях можно написать более сложные запросы к БД, которые за один раз получат всю или почти всю необходимую информацию, но в данной статье я не стал усложнять код, потому что это бы усложнило его понимание.
        • –6
          Я к пытался сказать, что PHP плох, NodeJS хорош. Каждый язык — просто инструмент. И каждый язык имеет свои сильные и слабые стороны. Как по мне профит от использования GraphQL не в последнюю (хотя и не единственную) очередь заключается в возможности распараллеливания запросов, когда за различные сущьности отвечают различные сервисы.
          • +1
            Вот уж в отправке асинхронных запросов к БД в PHP никаких проблем нет. Но как правило даже этого ненужно.
      • 0

        https://github.com/Youshido/GraphQL советую присмотреться. Реализует почти все из протокола, есть подход как инлайн конфигурации, так и OO. По опыту использования советую

        • 0
          Спасибо. Я слышал и про эту библиотеку, но так как она не полностью реализует спецификацию, то решил пользоваться graphql-php. Но соглашусь — по стилю эта реализация мне тоже нравится больше. Обязательно рассмотрю её.
          • 0

            пробовал, ни та ни другая реализация graphql под php не понравились. В целом Youshido/GraphQL понравилась больше чем webonyx/graphql-php, однако под последнюю проще писать обертки и фасады упрощающие дела. В целом я еще недостаточно набаловался с обеими.

          • 0
            А что по поводу безопасности самого graphql?
            • 0

              а что не так с безопасностью?

              • +1
                Ну во первых можно намеренно слать самые тяжелые запросы, даже те, которых на фронтенде нет. Во вторых при вытагивании ресурсов было бы неплохо еще всякие политики безопасности иметь, например не показывать удаленных пользователей, сообщения находящиеся в определенных разделах и так далее.
                • +1

                  https://habrahabr.ru/post/326986/#comment_10183788
                  Любая спецификация api не защищает от вас от вашей реализации.

                  • +1
                    Это решается точно так же, как и в других API. Или в моделях, если graphql — это просто обертка, или через middleware
                    • –2
                      Похоже не дошли ещё до безопасности в GraphQL.

                      Типа свежая хипстерская технология.

                      Имхо.
                      • +2
                        Вот я удивляюсь. Под каждой статьёй про GraphQL обязательно кто нибудь спросит про безопасность. Но ещё сильнее меня удивляет то, что после комментариев, в которых говорится: «безопасность вашего приложения зависит от вас, а не от GraphQL» — вы делаете вывод, что безопасности в GraphQL нет.
                        • +1

                          рассмотрим любую спецификацию любого api, например rest. Что должен получить клиент при запросе GET /api/orders? Все 100000 заказов, если в конкретной реализации разработчик не позаботился о том, чтобы данный запрос был с дефолтными page=1&limit=20 (условно). Точно также работает и graphql. Это спецификации доступа к данным, а не про безопасность.

                          • 0
                            если в конкретной реализации разработчик не позаботился о том,
                            и кстати в этот момент это API перестаёт быть REST и становится «просто ещё одно кастомное API на URL и HTTP»
                            • 0

                              не совсем. рест вообще не спека, а крайне общая парадигма, в своей основе имеющая понятия ресурсов и методов доступа к ним. О пагинированности листингов оно не говорит.

                              • 0
                                в этот момент это API перестаёт быть REST

                                Я вам больше скажу. Если у вас клиент не использует HATEOAS — у вас не REST. Мобилки не умеют в гипертекст. А сделать так чтобы у нас все на клиенте декларировалось гипертекстом — получится недо-браузер и десяток человеко-лет разработки. Это никому не нужно.


                                Так что сойдемся на rest-like и норм.

                            • 0

                              стандарты типа jsonapi вообще никаких решений в этом ключе не предоставляют и никто не замарачивается.

                        • 0

                          graphql никак не решает проблемы безопасности. Это лишь более-менее стандартизированный прикладной протокол передачи данных. Аутентификацию/авторизацию вы уже сами делайте. Точно так же как graphql не решает вопросов пагинации, фильтраци и т.д.


                          Что дает graphql, и это по настоящему мощная штука, это полный контроль за тем как ваша API используется. Вы можете анализировать запросы, смотреть что чуваки мол чаще запрашивают глубокие короткие графы, что позволит вам улучшить ваше API и сделать его удобнее для клиента. Так же это упростит эволюционный подход к разработке API (решение проблемы версионизации API — не версионизировать API).

                          • 0

                            проникся? помню, в том году ты еще был скептически настроен.

                            • 0

                              Ну в целом оно решает мои проблемы. Причем неплохо. Тут больше агрессивный маркетинг напрягал.

                              • 0
                                А я вот все смотрю на него, смотрю. Домашние проекты даже некоторые делал с GraphQL, но вот в реальных проектах пока не решаюсь.
                                • 0

                                  Я уперся в то что с мобильными клиентами все не так просто. Ну то есть есть всякие Apollo но мне лично не очень нравится такой подход. Под андроид есть неплохой примерчик с кодогенерацией, но я не мобильщик и все жду пока наши ребята потыкают.

                        • 0
                          Скажите, пожалуйста, а nested attacks как-то предполагаются?
                          {
                            authors {
                              firstName
                              posts {
                                title
                                author {
                                  firstName
                                  posts{
                                    title
                                    author {
                                      firstName
                                      posts {
                                        title
                                        [n author]
                                          [n post]
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                          
                          • 0
                            Насколько я знаю в реализациях GraphQL на PHP пока нет готового решения этой проблемы, но думаю в будущем она появится. А пока можно просто накладывать определённые ограничения на вложенности или время выполнения запроса. Или же создать что-то вроде реестра доступных запросов и тогда, если запроса полученного сервером не будет в реестре, то он не будет выполняться. Хороший ответ на подобный вопрос есть тут: http://stackoverflow.com/questions/37337466/how-do-you-prevent-nested-attack-on-graphql-apollo-server
                            • 0
                              Спасибо, именно оттуда я и взял этот запрос, как можно заметить. Просто я из лучших побуждений хочу, чтобы как можно больше решений, использующих GraphQL, сразу обращали внимание на подобную опасность.
                            • 0

                              Их решают ограничивая сложность запроса или же уровнем вложенности.


                              https://github.com/webonyx/graphql-php/pull/32

                              • 0
                                Ответ на ваш вопрос в следующей статье.
                              • 0
                                SPARQL напоминает очень сильно
                              • 0
                                Возможно я чего-то не понимаю, или этот подход не для меня, поэтому ответьте пожалуйста на вопрос:

                                Сейчас, грубо говоря, мы подгоняем хранение данных под то, как они будут использоваться. Т.е. данные в базах могут быть специально нормализованы под какой-то высоконагруженный endpoint и от этого будет профит. Но ведь получается сам подход GraphQL этому противоречит? А как с таким подходом борются с нагрузками?
                                • 0
                                  Сейчас, грубо говоря, мы подгоняем хранение данных под то, как они будут использоваться

                                  это объяснимый, но нехороший способ решать проблемы.


                                  Т.е. данные в базах могут быть специально нормализованы под какой-то высоконагруженный endpoint и от этого будет профит.

                                  обычно все-таки при использовании в качестве бэка sql-решений имеют нормализованную базу и заточенную под фронт денормализованное быстрое хранилище.


                                  Но ведь получается сам подход GraphQL этому противоречит?

                                  GraphQL не предъявляет никаких требований к хранилищу. Используйте любое на какое хватит ума и опыта. В ресолверах могут быть тяжелые запросы с джойнами к sql-хранилищу либо быстрые запросы к redis/mongo/elasticsearch/solr.

                                  • 0
                                    мы подгоняем хранение данных под то, как они будут использоваться. Т.е. данные в базах могут быть специально нормализованы под какой-то высоконагруженный endpoint и от этого будет профит.

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


                                    Но ведь получается сам подход GraphQL этому противоречит?

                                    нисколько. Это по сути агрегатор. Фэйсбук его по сути создавал решать простую проблему. У вас есть 5 серверов и нам надо сделать 5 запросов что бы забрать данные на один скрин. В итоге нам проще сделать своего рода фасад над нашим сервером который будет агрегировать данные так как их хочет видеть клиент. Будете вы делать для филда запрос по HTTP или в базу — это уже деталь реализации.

                                  • 0

                                    я правильно понимаю, что на выборку из 50 пользователей будет сгенерировано еще +50 запросов на получение кол-ва друзей для каждого? Это можно как-то одним пакетным запросом через WHERE IN решить?

                                    • 0
                                      В данном примере — да. Для каждого пользователя будет ещё отдельный запрос друзей, но как я уже писал ранее — это я сделал чтобы максимально упростить пример. Вы же можете написать любой запрос к базе данных и потом полученные результаты обработать в ресолвере как вам надо.
                                      • 0

                                        Покажите тогда пример как эту ситуацию можно обыграть через +1 запрос с WHERE IN

                                        • +1
                                          Конкретно про решение проблемы n+1 можете почитать здесь:
                                          http://webonyx.github.io/graphql-php/data-fetching/#solving-n1-problem

                                          Это документация к библиотеке graphql-php и в ней этот случай описан. Если все равно возникнут вопросы — задавайте.

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