Введение в GraphQL: что это за язык и как использовать его под Android

Всем привет! Меня зовут Мария Агеева, я Android-разработчик. Около 2 лет работаю с GraphQL. Хотела бы поделиться опытом работы с этой технологией и, возможно, заинтересовать в ее использовании тех из вас, кто еще с ней не знаком, собирается ее использовать или только начал интеграцию GraphQL в проект. Также в статье будет кратко описана работа с GraphQL для платформы Android.

Что такое GraphQL

Прежде всего рассмотрим, что же такое GraphQL. По определению с официального сайта, GraphQL — это язык запросов и манипулирования данными для API, а также среда для выполнения этих запросов. Язык был разработан в 2012 году в Facebook для внутренних нужд компании, в 2015-м вышел в открытый доступ, а с 7 ноября 2018 года работу над ним ведет не Facebook, а GraphQL Foundation. Конечно, проект развивался достаточно активно с 2012 года, но особую популярность заработал после того, как получил статус open source.

Даже если ранее вы никогда не слышали о GraphQL, велик шанс, что вы знаете или использовали продукты, которые написаны с его применением. Во-первых, это, естественно, социальная сеть Facebook. Кроме нее, с GraphQL работают при разработке таких продуктов, как Airbnb, GitHub, Pinterest, Shopify, New York Times и многих других.

Вернемся к созданию языка. То, что он был создан именно в Facebook для проекта с большим объемом разнородных данных, говорит о том, что при работе над подобным продуктом могут возникать ситуации с ограничениями REST-архитектуры. Например, получение профиля пользователя, его постов и комментариев изначально не кажется сложной задачей. Но если учесть объем данных в системе и предположить, что все эти данные хранятся в разных базах данных (например, MySQL и MongoDB), становится понятно, что для этого понадобится создать несколько REST-endpoint’ов. Ну а если представить, насколько велик объем данных и разнородны источники данных, то становится понятно, почему понадобилось разрабатывать новый подход к работе с API. В основе этого подхода лежит следующий принцип: лучше иметь один «умный» endpoint, который будет способен работать со сложными запросами и возвращать данные именно в той форме и в том объеме, которые необходимы клиенту.

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

Как можно заметить, клиенту при работе с GraphQL API совершенно не важно, откуда поступают данные, которые он запрашивает. Он просто делает запрос в нужном ему объеме, а сервер GraphQL возвращает результат. Поэтому можно представить, что схема — это контракт между API и клиентом, так как, прежде чем клиент выполнит какой-либо запрос, этот запрос валидируется в соответствии со схемой данного API.

Взаимосвязь клиента и сервера при работе с GraphQL

Тут я хотела бы остановиться на том, как, собственно, устроена работа клиента и сервера при использовании GraphQL. Так как я не back-end-разработчик, то расскажу только вкратце о работе с серверной частью, не вдаваясь в подробности.

Схема взаимодействия клиента и сервера GraphQL

Работу с GraphQL поддерживает сейчас большое количество платформ: веб, Android, iOS и многие другие. GraphQL-клиент отправляет запрос на получение данных или на их изменение, составленный в соответствии со схемой, на GraphQL-сервер. GraphQL-сервер, в свою очередь, представляет собой HTTP-сервер, с которым связана схема GraphQL. То есть имеется в виду, что через эту схему «пропускаются» все запросы, полученные от клиента, и возвращаемые ответы.

Сервер GraphQL не может знать, что делать с запросом, если ему не «объяснить» это с помощью специальных функций. Благодаря им GraphQL понимает, как получить данные для запрашиваемых полей. Эти функции связаны с соответствующими полями и называются распознавателями, или резолверами (resolvers). После этого клиенту возвращается ответ, который отражает запрашиваемую с клиента структуру данных, обычно в формате JSON. И, повторюсь, тут можно работать с абсолютно различными источниками данных: базы данных (реляционные/NoSQL), результаты веб-поиска, Docker и т. д.

Сравнение GraphQL API и REST API

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

  • Они предоставляют единый интерфейс для работы с удаленными данными.
  • Чаще всего на запрос возвращается ответ в формате JSON.
  • Оба подхода позволяют дифференцировать запросы на чтение и на запись.

Но у GraphQL есть и много преимуществ по сравнению с REST:

  • Он клиентоориентированный, то есть позволяет клиенту получить ту информацию и именно в том объеме, которые ему нужны — не больше и не меньше.
  • В отличие от REST, необходим только один endpoint для работы.
  • GraphQL — сильно типизированный язык, что позволяет заранее оценить правильность запроса до этапа выполнения программы.
  • GraphQL предоставляет возможность комбинировать запросы.
  • Запросы к GraphQL API всегда возвращают ожидаемый результат, который соответствует схеме данных этого GraphQL API.
  • Использование GraphQL позволяет уменьшить количество данных, передаваемых клиенту, так как клиент запрашивает только необходимые ему данные. То есть если при использовании REST конечная точка определяет то, какие данные и в какой форме получит клиент, то при использовании GraphQL клиент не должен запрашивать лишние данные, тем самым уменьшая объем ответа сервера.

На практике использование GraphQL увеличивает независимость front-end- и back-end-разработчиков. После согласования схемы front-end-разработчики больше не будут просить создать новые endpoint’ы или добавить новые параметры для имеющихся: back-end-разработчики один раз описывают схему данных, а front-end-специалист создает запросы и комбинирует данные так, как это необходимо.

Система типов

В основе любого GraphQL API лежит описание типов, с которыми можно работать и которые он может вернуть — как было сказано ранее, схема. Так как сервисы GraphQL могут быть написаны на многих языках, то был разработан универсальный GraphQL schema language. Рассмотрим основные типы данных, которые он поддерживает.

Объектные

Наиболее базовые типы GraphQL — объектные типы, которые представляют собой объект и набор полей, описывающих его.

Все примеры в этой статье я буду приводить с использованием Star Wars API.

Например:

type Planet {
    id: ID!
    diameter: Int
    name: String!
    population: Float
    residents: [Person!]
}

Скалярные

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

  • Int — 32-битное целое число со знаком;
  • Float — число двойной точности с плавающей точкой со знаком;
  • String — строка в UTF-8;
  • Boolean — логический тип (true или false);
  • ID — специальный скалярный тип, представляющий собой уникальный идентификатор, который используется чаще всего для получения объекта или как ключ в кеше. Значения типа ID сериализуются так же, как и String, но тот факт, что для идентификаторов был выделен отдельный тип данных, говорит о том, что он должен использоваться не для отображения клиенту, а только в программах.

Во многих имплементациях сервисов GraphQL есть возможность создавать и свои собственные скалярные типы.

Хотелось бы еще отметить, что в GraphQL можно добавить и так называемые модификаторы типов (type modifiers), которые влияют на валидацию полей. Очень распространен non-null модификатор, который гарантирует, что данное значение никогда не будет null (иначе получим ошибку выполнения). Обозначается как !, например:

    id: ID!

Другой распространенный модификатор — List. Можно пометить тип как List, в таком случае ожидается, что в этом месте будет возвращен массив из таких значений. Обозначается как [], например:

    films: [Film!]

Модификаторы можно комбинировать и использовать на любом уровне вложенности.

Аргументы

Представляют собой набор пар «ключ — значение», которые привязаны к определенному полю. Они передаются на сервер и влияют на то, как будут получены данные для определенного поля. Аргументы могут быть и литералами, и переменными. Их можно применять на любых полях вне зависимости от уровня их вложенности. Они обязательно должны быть именованными, а также могут быть обязательными или опциональными (если аргументы опциональные, то их значение должно быть задано по умолчанию). По типу данных значения аргументов могут быть скалярными или специальными объектными input-типами.

Например, тут к полю Film привязан аргумент id (в данном примере литерал типа ID):

query {
    Film(id:"1234abcd") {
        id
        title
        characters {
            name
        }
    }
}

Перечисления

Специальный скалярный тип, который ограничен определенным набором значений. Пример:

enum HAIR_COLOR {
    BLACK
    BLONDE
    BROWN
    GREY
}

Также в GraphQL есть 2 абстрактных типа: union и interface. Они могут использоваться только как возвратный тип, то есть можно лишь получить данные такого типа, но не передать в запрос в качестве аргумента.

Интерфейсы

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

interface Character {
    id: ID!
    name: String!
}
type Droid implements Character {
    id: ID!
    name: String!
    function: String
}
type Human implements Character {
    id: ID!
    name: String!
    starships: [Starship]
}

Другой абстрактный тип, не включает обязательные поля. Может применяться там, где необходимо использовать семейство из типов, у которых нет общих полей, например при реализации поиска или сложных подписок. Пример:

union SearchResult = Human | Starship | Film
... on Human {
    name
    height
}
... on Starship {
    model
    capacity
    manufacturer
}
... on Film {
    title
    episode
}

Можно задать вопрос: зачем использовать 2 абстрактных типа? Потому что у этих типов принципиально разное применение. Union-типы используются в тех местах схемы, где можно сказать, что тут можно вернуть один из перечисленных типов. В свою очередь, интерфейсы используются там, где о типе можно сказать, что он реализует этот контракт.

Query, mutations, subscriptions

Cуществуют специальные типы, определяющие тип операции, которую клиент хочет выполнить, например получение данных или их изменение. Эти типы являются точками входа для API. Любой GraphQL API должен обязательно иметь хотя бы один query, но mutations и subscriptions необязательны. Стоит отметить, что, несмотря на свой особый статус, эти специальные типы такие же, как и другие объектные типы GraphQL. Рассмотрим каждый из этих типов подробнее.

Query. Cпециальный тип, который по конвенции представляет собой запрос на получение данных (можно сказать, что это аналог GET в REST). Именно этот тип данных обязателен для любого GraphQL API. Рассмотрим простой пример:

query GetAllFilms {
    allFilms {
        id
        title
        episode
    }
}

Это простой запрос на получение информации обо всех фильмах (как понятно из названия). Уже на этом этапе видно важное преимущество использования GraphQL: мы получаем только те данные, которые нам нужны в приложении (в данном случае id, название и номер эпизода фильма). Таким образом, на практике не приходится на клиенте получать все данные независимо от того, нужны они или нет, и выполнять порой достаточно сложную фильтрацию, если можно получить лишь нужные данные и далее работать только с ними. Для дебага и логирования также полезно именовать запросы (при работе с некоторыми технологиями, например при разработке для Android, анонимные query не поддерживаются в принципе).

Mutations. Cпециальный тип, который, в отличие от query, используется для изменения данных (можно сказать, что это аналог POST). Да, конечно, это рекомендация, и на практике не запрещено использовать для изменения данных и query, но все же так делать не стоит. Ведь основное отличие query от mutation — их поведение при получении результатов запросов: при выполнении query обработка полей результата будет выполняться параллельно, так как они не изменяют данные. В свою очередь, в mutations это выполняется последовательно, чтобы обеспечить целостность данных. Пример:

mutation DeleteEpisode {
    deleteFilm(id: “123456”) {
        title
    }
}

В примере можно заметить, что отличий в синтаксисе нет, запрос начинается с названия операции (mutation) и также является именованным.

Subscriptions. Cамый новый специальный тип данных, который позволяет реализовать систему publisher/subscriber. То есть это способ отправки данных клиенту, который «слушает» эти данные в реальном времени. В отличие от предыдущих 2 специальных типов, тут ответ сервера приходит не единоразово, а каждый раз при срабатывании определенных условий на сервере при условии, что клиент «слушает» эти данные. Можно сказать, что это аналог push-нотификаций, которые работают в одной системе со всеми другими типами запросов — query и mutations. Большой плюс данного подхода — то, что клиент и в этом случае может с помощью аргументов определить, при каком условии результат с сервера должен приходить на клиент. Пример:

subscription WaitForNewFilm {
    Film(filter:{ action:CREATED }) {
        id
        title
        episode
        characters {                
            name
        }
    }
}

Обратите внимание, что в примере ожидаются только данные о созданном фильме. Если же произойдет изменение имеющейся информации о фильме, его удаление или какое-то другое событие, клиент эти данные не получит.

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

Introspection query и GraphQL Playground

Для получения информации обо всех типах, которые поддерживает данный GraphQL API, можно выполнить так называемый introspection query. По умолчанию на дебаг-версии сервера этот запрос доступен, на production, естественно, должен быть отключен. Также работу с API упрощает использование GraphQL Playground — это графическая интерактивная среда разработки, базирующаяся на GraphiQL, полноценной IDE для работы с GraphQL. При разработке и использовании Apollo Server, распространенного open source — сервера для разработки с использованием GraphQL, Playground становится доступным на том же endpoint, что и сервер (например, localhost:4000/playground). И, как и в случае с introspection query, он доступен только в дебаг-версии.

Работа с GraphQL API при разработке для Android

В заключительной части своей статьи я хотела бы кратко описать работу с GraphQL для Android-разработки. Наиболее распространенный сервис для работы с GraphQL — Apollo GraphQL Server. Это open source — проект, который активно развивается Apollo GraphQL. Версии его существуют для многих платформ, в том числе для веб, iOS и Android. И именно о последнем пойдет речь далее.
Apollo GraphQL клиент для Android поддерживает многие из основных функций на данном этапе, в том числе:

  • queries, mutations, subscriptions;
  • нормализованный кеш;
  • загрузку файлов;
  • собственные скалярные типы.

В основе его работы — кодогенерация сильно типизированных моделей по схеме GraphQL. По умолчанию поддерживается кодогенерация на Java, но можно в качестве экспериментальной фичи использовать Kotlin-кодогенерацию.
Для облегчения работы с GraphQL в Android Studio хорошо подходит плагин JS GraphQL. Основные его плюсы:

  • подсветка синтаксиса, автодополнение, форматирование кода;
  • возможность выполнения introspection query в Android Studio;
  • поддержка выполнения query, mutations в Android Studio и др.;
  • go-to-definition для GraphQL-файлов.

Рассмотрим, как же подключить поддержку GraphQL в проект. Последняя версия клиента на момент написания статьи — 1.4.4.

В build.gradle уровня приложения необходимо добавить следующие строки для работы с Gradle-плагином Apollo:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
       classpath("com.apollographql.apollo:apollo-gradle-plugin:x.y.z")
    }
}

В build.gradle уровня модуля добавляем следующее:

apply plugin: 'com.apollographql.apollo'
repositories {
    jcenter()
}
dependencies {
    implementation("com.apollographql.apollo:apollo-runtime:x.y.z")
// If not already on your classpath, you might need the jetbrains annotations
    compileOnly("org.jetbrains:annotations:13.0")
    testCompileOnly("org.jetbrains:annotations:13.0")
}

Далее необходимо создать директорию в проекте (модуле), например src/main/graphql/com/my_package/, в которую следует добавить файл schema.json — схему GraphQL в формате JSON, полученную в результате выполнения introspection query того API, который будет использоваться в проекте.

В этой же директории создаем файлы с запросами в формате .graphql, например queries.graphql. Единственный нюанс при создании запросов — запросы (query, mutations, subscriptions) должны быть именованными.

По умолчанию, как упоминалось выше, используется кодогенерация моделей на Java. Если же нужно включить Kotlin-кодогенерацию, добавляем следующее:

apollo {
    generateKotlinModels.set(true) 
}

И затем выполняем команду./gradlew generateApolloSources, чтобы модели по запросам из директории src/main/graphql/com/my_package/ были сгенерированы. При создании моделей на основе GraphQL-типов будут сгенерированы Java-классы (или Kotlin-классы), типы полей классов соответствуют скалярным типам GraphQL. При использовании типа ID в GraphQL в сгенерированном классе используется тип String. При необходимости также можно добавить собственные скалярные типы и определить то, как они преобразуются (их маппинг).
Для работы с сервером (например, для выполнения запросов и их конфигурации, настройки subscriptions) и работы с кешем используется класс ApolloClient. Пример его создания и конфигурации:

val apolloClient : ApolloClient = ApolloClient.builder()
    .serverUrl(ENDPOINT)
     .subscriptionTransportFactory(
         WebSocketSubscriptionTransport.Factory(SUBSCRIPTIONS_ENDPOINT, okHttp))
    .okHttpClient(okHttp)
    .build()

Теперь рассмотрим, как же можно выполнить основные типы операций GraphQL. Клиент Apollo GraphQL поддерживает и стандартное выполнение операций с использованием callback-функций, и RxJava2 и coroutines, для чего предполагается подключение отдельных зависимостей Gradle.

Предположим, что мы хотим выполнить 2 запроса с одним и тем же типом данных GraphQL Film и выбором одних и тех же полей в нем:

query GetFilmNotOptimized($id:ID!) {
     Film(id:$id) {
        id
        title
        director
        episodeId
        planets {
            id
            name
            diameter
            population
            climate
        }
    }
}
query GetFilmsNotOptimized {
    allFilms {
        id
        title
        director
        episodeId
        planets {
            id
            name
            diameter
            population
            climate
        }
    }
}

Вот как это будет выглядеть в проекте. Первый запрос:

apolloClient.query(
    GetFilmNotOptimizedQuery.builder()
        .id(id)
        .build()
).enqueue(object : ApolloCall.Callback<GetFilmNotOptimizedQuery.Data>() {

    override fun onFailure(e: ApolloException) {
        /* Response to exception */
    }

    override fun onResponse(response: Response<GetFilmNotOptimizedQuery.Data>) {
        val filmType = response.data()?.Film()?.__typename()
        val filmClassName = response.data()?.Film()?.javaClass?.simpleName
        Log.i("ApolloStarWars", """GetFilmNotOptimizedQuery: film type = $filmType, film Java class = $filmClassName""")
       //result: GetFilmNotOptimizedQuery: film type = Film, film Java class = Film
	}
})

И второй запрос:

apolloClient.query(
    GetFilmsNotOptimizedQuery()
).enqueue(object : ApolloCall.Callback<GetFilmsNotOptimizedQuery.Data>() {
    override fun onFailure(e: ApolloException) {
        /* Response to exception */
    }
    override fun onResponse(response: Response<GetFilmsNotOptimizedQuery.Data>) {
        val filmType = response.data()?.allFilms()?.first()?.__typename()
        val filmClassName = response.data()?.allFilms()?.first()?.javaClass?.simpleName
        Log.i("ApolloStarWars", """GetFilmsNotOptimizedQuery: films type = $filmType, film Java class = $filmClassName""")
       //GetFilmsNotOptimizedQuery: films type = Film, film Java class = AllFilm
    }
})

Тут можно заметить, что Apollo GraphQL генерирует 2 разных Java-типа на один и тот же GraphQL-тип даже при условии того, что выбраны одни и те же поля. В таком случае можно воспользоваться фрагментами GraphQL — переиспользуемыми структурами, которые позволяют определять наборы полей, основанные на каком-либо GraphQL-типе, и включать их в запросы. Например, предыдущие запросы можно оптимизировать, используя такие фрагменты:

fragment PlanetFragment on Planet {
    id
    name
    diameter
    population
    climate
}
fragment FilmFragment on Film {
    id
    title
    director
    episodeId
    planets {
        ...PlanetFragment
    }
}

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

query GetFilm($id:ID!) {
   Film(id:$id) {
       ...FilmFragment
   }
}

Это позволит в итоге получить один и тот же Java-тип как результат выполнения различных запросов, который будет сгенерирован однократно на каждый GraphQL-фрагмент.

Поддержка RxJava2 и Kotlin Coroutines

И в заключение кратко о поддержке RxJava2 и Coroutines.

Для подключения в проект поддержки RxJava2 необходимо добавить следующий импорт в build.gradle уровня модуля:

implementation 'com.apollographql.apollo:apollo-rx2-support:x.y.z'

Он включает в себя набор extension-функций (Kotlin) и класс Rx2Apollo (Java) для конвертации ApolloCall в Observable. Вариант использования:

 apolloClient.rxQuery(
      GetFilmsQuery()
  ).singleElement()
   .toSingle()
   .map { response ->
       return@map response.data()?.allFilms()?.map { film ->
           film.fragments().filmFragment().toFilm()
       }?: listOf()
   }

Аналогично для поддержки Coroutines добавляем следующий импорт:

implementation 'com.apollographql.apollo:apollo-coroutines-support:x.y.z'

Тут включены extension-функции для конвертации ApolloCall во Flow, в Deferred и Channel.

apolloClient.query(
   GetFilmsQuery()
).toDeferred()
.await()
.data()?.allFilms()?.map { film ->
   film.fragments().filmFragment().toFilm()
}?: listOf()

Выводы

Подводя итог, хотелось бы отметить, что GraphQL — это концепция создания API, которая обеспечивает слабую связность клиента и сервера. Очевидно, что с появлением этой технологии совершенно не обязательно полностью отказываться от использования REST-архитектуры.

Более того, с моей точки зрения, если структура данных в проекте несложная, обязательный переход на GraphQL не имеет смысла, все зависит только от желания разработчиков. Но этот подход действительно упрощает работу с большими разнородными данными. Главное — помнить о том, что GraphQL не предполагает переработки всего того, что было раньше, и в итоге каждый должен выбирать тот подход, который ему ближе. Но я считаю, что GraphQL — определенно та технология, с которой стоит ознакомиться.

Мне, как Android-разработчику, очень нравится использование этой технологии по многим причинам. Это и то, что в итоге клиентское приложение работает со сгенерированными классами Java или Kotlin, и то, что нет необходимости фильтровать данные на клиенте, и возможность использовать не только запросы на получение данных и их изменение, но и subscriptions (можно сказать, аналоги push-нотификаций) в одной системе без необходимости использования и настройки сторонних сервисов и т. д.

Чтобы вы могли получить больше информации по этой теме, а также узнать о расширенных функциях (например, о работе с кешем), привожу список полезных ссылок:

Похожие статьи:
Даніїл Куриленко та його дружина Юлія Петрова загинули 24 червня. Частина російської ракети влучила в помешкання подружжя...
Компания Motorola и оператор Verizon, кроме смартфона Droid Turbo 2, представила на американском рынке и модель Droid Maxx 2. Фактически, это...
Меня зовут Станислав Малкин, я технический специалист. В разработке уже более 10 лет, а в данный момент DevOps в небольшом...
Компания MEIZU представила свой новый флагман - MEIZU PRO 5. По словам создателей, это решение для самых требовательных...
Універсальний космічний конектор для заправки супутників. Саме з цим проєктом українська команда FireWay здобула...
Яндекс.Метрика