Перевод статьи Dhaivat Pandya: GraphQL Concepts Visualized
GraphQL часто называют «унифицированным интерфейсом для доступа к данным из разных источников». Хотя это объяснение является точным, оно не раскрывает основополагающих идей, мотивов, лежащих в основе GraphQL, или даже то, почему оно называется «GraphQL». Вы можете видеть звезды и ночь, но совсем не «Звездную ночь».
Настоящее сердце GraphQL это то, что я считаю графом данных приложения. В этой статье я представлю граф данных приложения, расскажу о том, как запросы GraphQL работают с графом данных приложения и как мы можем кэшировать результаты запросов GraphQL, используя их древовидную структуру.
Многие данные в современных приложениях могут быть представлены с использованием графа узлов и ребер, где узлы представляют объекты, а ребра представляют отношения между этими объектами.
Например, мы создаем упорядоченную систему (каталог) для библиотек. Для простоты, в нашей системе есть множество книг и авторов, и у каждой из этих книг есть по крайней мере один автор. У авторов также есть соавторы, с которыми автор написал хотя бы одну книгу.
Если мы визуализируем отношения в виде графа, получится что-то примерно похожее на:
График представляет отношения между различными частями данных, которые у нас есть, и сущностями (например, Book и Author), которые мы пытаемся представить. Практически все приложения работают с такого рода графиком: они читают и пишут в него. Здесь GraphQL и обретает свое имя.
GraphQL позволяет извлекать деревья из графа данных приложения.
Поначалу это звучит довольно загадочно, но давайте разберемся, что это значит. По сути, дерево — это граф, который имеет начальную точку (корень, root) и свойство, согласно которому вы не можете пройти по узлам и вернуться к тому же узлу, то есть граф не имеет циклов.
Давайте рассмотрим пример запроса GraphQL и то, как он «извлекает дерево» из графа приложения. Вот запрос, который мы могли бы выполнить для графа данных, о котором мы только что говорили:
query {
book(isbn: "9780674430006") {
title
authors {
name
}
}
}
Как только сервер выполняет запрос, он возвращает результат:
{
book: {
title: "Capital in the Twenty First Century",
authors: [
{ name: "Thomas Piketty" },
{ name: "Arthur Goldhammer" },
]
}
}
Вот как это выглядит с точки зрения графика данных приложения:
Давайте разберемся, как фактически эта информация была извлечена из графа по запросу GraphQL.
GraphQL позволяет нам определить тип корневого запроса (мы будем называть его RootQuery), который определяет, где может начинаться запрос GraphQL при обходе графа данных приложения. В нашем примере мы начинаем с узла Book, который мы выбрали, используя его номер ISBN с полем запроса book (isbn:…). Затем запрос GraphQL обходит граф, следуя ребрам, отмеченным каждым из вложенных полей. Для нашего запроса он переходит от узла Book к узлу, содержащему заголовок книги через поле title в запросе. Он также получает узлы Author, следуя по ребрам Book, помеченными как authors, и получает также name каждого автора.
Чтобы увидеть, как создается дерево, нам просто нужно переместить узлы так, чтобы оно выглядело как единое целое:
Для каждой части информации, которую возвращает запрос, существует связанный путь запроса, который состоит из полей в запросе GraphQL, которые мы использовали для получения этой информации. Например, название книги «Capital» имеет следующий путь запроса:
RootQuery → book(isbn: “9780674430006”) → title
Поля в нашем запросе GraphQL (то есть book, authors, name) указывают, какие ребра следует использовать в графе данных приложения, чтобы получить желаемый результат. Вот где GraphQL обретает свое имя: GraphQL — это язык запросов, который обходит ваш граф данных для создания дерева результатов запроса.
Чтобы создать действительно быстрое и гибкое приложение, которое не тратит большую часть своей жизни, показывая пользователям прелоадер, мы хотим сократить количество обращений к серверу с помощью кеша. Оказывается, что древовидная структура GraphQL отлично подходит для кеширования на стороне клиента.
В качестве простого примера, предположим, что у вас есть код на вашей странице, который выбирает следующий запрос GraphQL:
query {
author(id: "8") {
name
}
}
Позже, другой раздел страницы снова запрашивает тот же запрос. Если нам не нужны самые новейшие данные, этот второй запрос может быть выполнен с помощью данных, которые у нас уже есть! Это означает, что кеш должен иметь возможность выполнять запросы, даже не отправляя их на сервер, что делает наше приложение быстрее. Но мы можем сделать намного лучше, чем просто кэшировать точные запросы, которые мы получили ранее.
Давайте рассмотрим подход Apollo Client к кэшированию результатов GraphQL. По сути, результаты запросов GraphQL являются деревьями с данными из вашего графа данных на стороне сервера. Мы хотим иметь возможность кэшировать эти деревья результатов, чтобы не запрашивать их с сервера каждый раз, когда они нам понадобятся снова. Для этого мы сделаем ключевое предположение:
Apollo Client предполагает, что каждый путь в графе данных вашего приложения, как указано в ваших запросах GraphQL, указывает на стабильный фрагмент информации.
Если это предположение не выполняется в некоторых случаях (например, когда информация, на которую указывает конкретный путь запроса, изменяется очень часто), мы можем помешать Apollo Client сделать это предположение с помощью идентификаторов объектов, которую мы представим позже. Но, в целом, это оказывается разумным предположением, когда дело доходит до кеширования.
Предположение «один и тот же путь означает один и тот же объект», введенное последним пунктом, невероятно полезно. Например, у нас есть эти два запроса, запускаемые один за другим:
query particularAuthor {
author(name: "Thomas Piketty") {
name
age
}
}
query authorAndBook {
book(isbn: "9780674430006") {
title
}
author(name: "Thomas Piketty") {
name
age
}
}
Просто посмотрев на запросы вы можете увидеть, что чтобы получить имя автора не нужно обращаться к серверу. Эта информация может быть найдена в кеше из результата предыдущего запроса.
Apollo Client использует этот тип логики для удаления частей запроса на основе данных, уже находящихся в кеше. Это возможно сделать из-за предположения о пути. Предполагается, что путь RootQuery → author (id: 6) → name принес бы одинаковую информацию в обоих запросах. Конечно, если это предположение не работает, вы можете полностью переопределить поведение, используя опцию forceFetch.
Это предположение работает очень хорошо потому, что путь запроса также включает аргументы, которые мы используем в GraphQL. Например…
RootQuery → author(id: 3) → name
отличается от
RootQuery → author(id: 6) → name
… поэтому Apollo Client не будет предполагать, что они представляют одну и ту же информацию, и пытаться объединить один с результатом другого.
Оказывается, можно сделать даже лучше, чем просто следовать по путям запросов от корня. Иногда вы можете получить доступ к одному и тому же объекту через два совершенно разных запроса.
Например, учитывая, что у каждого из наших авторов есть некоторый набор соавторов, мы можем получить доступ к некоторым объектам «Author» через это поле:
query {
author(name: "Arthur Goldhammer") {
coauthors {
name
id
}
}
}
Но мы также можем получить автора прямо из корня:
query {
author(id: "5") {
name
id
}
}
Предположим, что автор с именем «Arthur Goldhammer» и автор с id 5 являются соавторами какой-то книги. Тогда мы закончили бы тем, что сохранили ту же самую информацию (то есть информацию о «Thomas Piketty», авторе с id 5) дважды в нашем кеше.
Вот что было бы в нашей древовидной структуре кеша:
Проблема в том, что оба запроса ссылаются на одну и ту же часть информации в графе данных приложения, но у Apollo Client пока нет возможности узнать это. Чтобы решить эту проблему, Apollo Client использует второе ключевое предположение: идентификаторы объектов. По сути, вы можете указать уникальный идентификатор для любого объекта, который вы запрашиваете. И Apollo Client предполагает, что все объекты с одинаковым идентификатором объекта представляют одну и ту же часть информации.
Как только клиент Apollo узнает об этом, он может сделать кеш более приятным
Это означает, что идентификаторы объектов должны быть уникальными во всем приложении. Как следствие, вы не можете просто использовать свои идентификаторы напрямую, потому что тогда у вас может быть автор с id 5 и книга с id 5. Но это легко исправить: создать уникальный идентификатор объекта. Просто добавьте __typename, возвращенное GraphQL, к id, сгенерированному вашим бэкендом. Таким образом, автор с id 5 может иметь идентификатор объекта Author:5 или что-то подобное.
Продолжая два последних запроса, которые мы только что рассмотрели, давайте подумаем о том, что произойдет, если некоторые данные изменятся. Например, что если вы получите какой-то другой запрос и поймете, что автор с id 5 изменил свое имя? Что происходит с частями вашего пользовательского интерфейса, которые в настоящее время ссылаются на старое имя, которое имел автор с id 5?
Вот отличная новость: они будут обновляться автоматически. Это приводит нас к еще одной вещи, которую предоставляет Apollo Client: если значение какого-либо узла просматриваемого дерева запросов изменится, запрос будет обновлен с новым результатом.
Таким образом, в этом случае у нас есть два запроса, которые оба полагаются на автора с идентификатором объекта «Author: 5». Поскольку оба дерева запросов ссылаются на этого автора, любое обновление информации об авторе уведомит оба запроса:
Если вы используете react-apollo или angular2-apollo с Apollo Client, вам не нужно беспокоиться o настройке: ваши компоненты просто получат новые данные и обновят их автоматически. Если вы не используете интеграцию с фреймворками, основной метод watchQuery делает то же самое, предоставляя вам наблюдаемую информацию, которая обновляется при каждом изменении хранилища.
Иногда для вашего приложения нет смысла иметь идентификаторы объектов для всего, или вы, возможно, не хотите иметь дело с ними непосредственно в своем коде, но нуждаетесь в определенных кусочках информации в кеше для обновления. Вот почему мы предоставляем удобные, но мощные API, такие как updateQueries или fetchMore, которые позволяют включать новую информацию в эти деревья запросов с очень детальным контролем.
Основа любого приложения — граф данных приложения. Когда-то, когда нам приходилось накатывать наши собственные HTTP-запросы на конечные точки REST, чтобы записывать информацию в этот граф приложений и читать из него, кеширование на клиенте было невероятно трудным, потому что выборка данных была очень специфичной для приложения. GraphQL, с другой стороны, дает нам много информации, которую мы можем использовать для автоматического кеширования.
Если вы понимаете пять простых концепций, вы можете понять, как реактивность и кеширование, работают в Apollo Client.
Вот они:
- Запросы GraphQL представляют собой способ получить деревья из графа данных вашего приложения. Мы называем эти деревья результатами запроса.
- Apollo Client кеширует деревья результатов запроса. Для этого делается два предположения:
- Один и тот же путь, один и тот же объект — один и тот же путь запроса обычно приводит к одной и той же части информации.
- Идентификаторы объекта, когда пути недостаточно. Если двум результатам присваивается один и тот же идентификатор объекта, они представляют один и тот же узел / фрагмент информации.
- Если какой-либо узел кеша, включенный в дерево результатов запроса, обновляется, Apollo Client обновит запрос.
Вышесказанное — это все, что вам нужно знать, чтобы быть экспертом в Apollo Client и GraphQL кэшировании. Слишком много для одного поста? Не беспокойтесь — мы будем публиковать больше концептуальной информации, когда это возможно, чтобы каждый мог понять цель, стоящую за GraphQL, где он получил свое имя и как четко рассуждать о любом аспекте кэширования результатов GraphQL.
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.
Если вам понравилась статья, внизу можно поддержать автора хлопками 👏🏻 Спасибо за прочтение!