Количество просмотров65
18 января 2022

GraphQL в мобильной разработке

GraphQL — это специальный язык запросов и манипулирование данными для API, а также среда для выполнения этих запросов. Технология появилась в 2012 году и была разработана компанией Facebook для личных целей. В 2015 году стала открытой для сторонних пользователей, а в 2019 была создана GraphQL Foundation — нейтральная структура на базе Linux Foundation, занимающаяся развитием GraphQL.

Основной особенностью данной технологии является использование в API особой декларативной структуры запроса, в которой указываются только те поля данных, которые нужны в ответе.

query HeroNameAndFriends {
            hero {
             name
            friends {
            name
}
}
}

Пример запроса GraphQL.

И в ответе гарантировано придут только запрошенные поля и ничего больше.

Давайте посмотрим, чем это отличается от REST API и как работает.

На рисунке внизу представлена схема взаимодействия GraphQL клиента с сервером. Со стороны клиента мы отсылаем запрос с нужными нам полями на интересующий нас endpoint. Далее на стороне сервера происходит выборка данных через специальную схему данных. Определение полей в схеме GraphQL состоит из двух частей: объявлений типов и распознавателей.

Причем схема строится уже поверх БД, в которой хранятся данные, что несколько отличается от того, к чему мы привыкли.

Запросы и ответы тоже отличаются не только содержанием. С одной стороны, это такой же json, как в REST. Оба API:

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

Но если REST нам отдаст всю структуру json, если только бекенд не озаботился и не подготовил специальную выдачу по конкретному endpoint, то GraphQL поможет нам сэкономить трафик. Кроме того, что мы можем запросить только те поля, которые нам нужны, мы можем агрегировать несколько запросов в один, если это позволяет наша схема:

image

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

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

Еще одной особенностью является то, что не все операции REST поддерживаются GraphQL. По сути, мы делаем всегда GET или POST запрос JSON.

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

  • Query — запрос без изменения данных.
  • Mutation — запрос на создание/обновление/удаление данных (как POST/ PUT/PATCH/DELETE).
  • Subscription — специальная операция, которая позволяет слушать изменения в реальном времени.

Теория, конечно, интересно, но давайте побыстрее перейдем к практике.

Итак, перед нами стоит задача — настроить свой собственный GraphQL сервер. Одними из самых популярных решений являются:

  1. Развертывание Apollo-Server.
  2. Использование облачных решений типа Amplify от Amazon.
  3. Использование облачных решений Hasura.

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

Итак, заходим на Hasura и логинимся/создаем аккаунт по одной из доступных опций.

Мы сразу попадаем на специальную консоль, где будут наши проекты:

Чтобы добавить новый проект, нажмем New Project. В появившемся меню выберем план оплаты и регион:

В большинстве случаев (для любительского использования) нам достаточно бесплатного проекта.

После создания проекта нам доступны его настройки:

Больше всего нас интересует наш endpoint и секретный ключ админа. Вы всегда можете скопировать их из консоли.

Перейдем к консоли проекта и настроим наше API. Нажимаем Launch Console.

Перед нами открывается меню:

На первой вкладке мы видим настройки входной информации для нашего API.

На второй вкладке информация о настройке REST API на основе нашего GraphQL.

Для того, чтобы наше API заработало, идем на вкладку Data и перейдем в менеджер данных.

Нам нужно подключить хранилище базу данных:

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

Простым, быстрым и дешевым решениям для нас будет использование Heroky Database:

После создания в левом меню появится база данных по умолчанию (PostgreSQL), в которой мы можем создавать свои таблицы:

Создадим три таблицы для нашей задачи:

  • пользователи;
  • посты;
  • комментарии к постам.

Тут ничего сложного. Поля будут преимущественно текстовые, кроме ключей, для них у нас uuid. Лайки к постам (массив айдишников) можно хранить как поле массива или как десериализованную строку (для удобства).

Добавим таблицы с помощью меню:

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

При желании можно добавить и внешние ключи, а также правила.

После создания таблицы мы можем как изменить наши поля:

Так и просмотреть наши данные, либо добавить записи вручную:

После того, как все таблицы созданы, мы можем перейти к работе с API.

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

Перед началом работы не забудьте прописать специальный заголовок и использовать в качестве значений ключ администратора над Explorer:

Этот же заголовок мы будем использовать при обращении из приложения.

В левой части нам доступны наши таблицы со структурами полей, а также шаблоны запросов для агрегации или выборки по первичному ключу. Нажав на любой шаблон, мы можем начать набирать свой запрос. Например, такой для запроса пользователей:


query Users($usersId: uuid) {
  users(where: {user_id: {_eq: $usersId}}) {
    user_email
    user_id
    user_name
    likes
  }
}

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

Запустим запрос и проверим, что он работает:


{
  "data": {
    "users": [
      {
        "user_email": "anna@mailinator.com",
        "user_id": "f06b74ff-0cc7-4411-a6df-dcfc8c41e831",
        "user_name": "Anna",
        "likes": null
      },
      {
        "user_email": "test@test.com",
        "user_id": "244c68b6-dcdb-49a8-8d1f-86431558df32",
        "user_name": "Test",
        "likes": null
      },
      {
        "user_email": "2345@mailinator.com",
        "user_id": "99bc4b7e-c663-4a94-be81-f07aae940a06",
        "user_name": "test2",
        "likes": null
      },
      {
        "user_email": "newbie@mailinator.com",
        "user_id": "28b430be-7ce1-475f-8530-6b80759e814b",
        "user_name": "New user",
        "likes": null
      }
    ]
  }
}

Добавить новый запрос или mutation мы можем с помощью нижнего левого меню:

Для примера используем mutation создания нового юзера:

Обратите внимание, что у mutation возникают свои шаблоны. Нам потребуется insert_users_one:


mutation CreateUser($userId: uuid, $userName: String, $userEmail: String, $userPassword: String) {
  insert_users_one(object: {user_name: $userName, user_id: $userId, user_email: $userEmail, password: $userPassword}) {
    user_id
    user_name
    user_email
  }
}

Такие операции требуют возврата. Укажем поля пользователя в качестве ответа. В Explorer вы можете передавать сразу значений по умолчанию, чтобы проверить работу API.

Добавим оставшиеся запросы. Мы используем Mutation для создания, редактирования и удаления поста. Также через Mutation мы изменяем лайки, добавляем комментарии. Для запросов ленты постов, списка комментариев поста мы используем Query.

Для обновления данных (постов) мы используем шаблон update_posts, указывая в фильтре idнужного поста. Аналогично для удаления – delete_posts c фильтром.

//Запрос списка постов
query Posts {
  posts {
       user_name
      user_id
      post_text
      post_id
      post_date
      likes
      image_link
  }
}
 
//Комментарий к посту
query CommentsForPost($postId: uuid) {
  comment(where: {post_id: {_eq: $postId}}) {
     comment_id
    comment_text
    user_id
  }
}
 
//Заведение нового поста
mutation CreatePost($postId: uuid, $likes: String, $postText: String, $userId: uuid, $userName: String, $postDate: date, $image: String) {
  insert_posts_one(object: {user_name: $userName, user_id: $userId, post_text: $postText, post_id: $postId, post_date: $postDate, image_link: $image, likes: $likes}) {
   ... post
  }
}
 
//Изменение лайка
mutation ChangePostLike($postId: uuid, $likes: String) {
  update_posts(where: {post_id: {_eq: $postId}}, _set: {likes: $likes}) {
    returning {
     ... post
    }
  }
}
 
//Редактирование поста
mutation ChangePost($postId: uuid, $postText: String, $image: String) {
  update_posts(where: {post_id: {_eq: $postId}}, _set: {post_text: $postText, image_link: $image}) {
    returning {
     ... post
    }
  }
}
 
//Удаление поста
mutation DeletePost($postId: uuid) {
  delete_posts(where: {post_id: {_eq: $postId}}) {
    returning {
     post_id
    }
  }
}
 
//Запрос поста для юзера – комбинированный запрос
query GetPost($postId: uuid, $userId: uuid) {
  posts(where: {post_id: {_eq: $postId}}) {
      user_name
      user_id
      post_text
      post_id
      post_date
      likes
      image_link
  }
  users(where: {user_id: {_eq: $userId }}) {
       user_id
    user_name
    user_email
  }
  comment(where: {post_id: {_eq: $postId}}) {
      comment_id
    comment_text
    user_id
  }
}
 
//Запрос поста
  query GetPostById($postId: uuid) {
  posts(where: {post_id: {_eq: $postId}}) {
      user_name
      user_id
      post_text
      post_id
      post_date
      likes
      image_link
  }
  }
 
//Создание нового пользователя
mutation NewUser($userId: uuid, $userName: String, $userEmail: String, $userPassword: String) {
insert_users_one(object: {user_id: $userId, user_name: $userName, user_email: $userEmail, user_password: $userPassword}) {
... user
}
}
 
//Пользователь по id
query GetUser($userId: uuid) {
    users(where: {user_id: {_eq: $userId}}) {
       user_id
    user_name
    user_email
}
}
 
//Вход – пользователь по email и паролю
query LoginUser($userEmail: String, $userPassword: String) {
    users(where: {user_email: {_eq: $userEmail}, user_password: {_eq: $userPassword}}) {
       user_id
    user_name
    user_email
}
}

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

Также мы используем в некоторых таблицах особую агрегационную структуру – фрагменты для объединения полей ответа.


//Фрагмент для постов
fragment post on posts {
   user_name
      user_id
      post_text
      post_id
      post_date
      likes
      image_link
}
 
//Фрагмент для комментариев
fragment comment on comment {
  comment_id
    comment_text
    user_id
}
 
//Фрагмент для пользователей
fragment user on users {
   user_id
    user_name
    user_email
}

Запросы можно скопировать куда-нибудь прежде, чем мы пропишем их у себя. В самом проекте они не сохраняются.

Готово. Теперь нам нужно подключить наше API к приложениям. Для обоих приложений мы будем использовать Apollo Client версии для Android или iOS. Это самое простое и популярное решение для работы с GraphQL в мобильном приложении:

Также нам нужно скачать схему. Сделаем это через командную строку или терминал рабочей машины. Будем использовать apollo-codegen, который нужно установить через npm:

npm i apollo-codegen

Следом используем следующую команду:

apollo-codegen download-schema "<host>.hasura.app/v1/graphql" --output schema.json --header "x-hasura-admin-secret: <key>"

Нужно указать правильный endpoint вашего проекта и правильный ключ для заголовка.

В том каталоге, где вы вызывали команду, появится файл schema.json. Его мы и будем подключать к нашему проекту в следующих частях.