Меня зовут Рустам, и я техлид в компании Distillery. Мы занимаемся разработкой мобильных приложений и веб-сервисов. Хочу рассказать, как мы с коллегами решили немного поэкспериментировать с технологией GraphQL

Для начала о том, что такое GraphQL. Это язык запросов для API, который разработали в Facebook в 2012 году. Он позволяет клиентам запрашивать ограниченное множество данных, в которых они нуждаются. GraphQL использует строго типизированный протокол, и все операции с данными проверяются в соответствии со схемой. 

Это хороший вариант для проектов, в которых разным типам клиентов (например, мобильному приложению и сайту) нужны разные наборы данных. С GraphQL мы заранее описываем схему запроса и ответа, а клиент сам указывает, какие данные ему необходимы. 

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

В целом у GraphQL есть несколько сильных сторон:

  • Не нужно создавать несколько REST запросов: чтобы извлечь данные, достаточно ввести один запрос.

  • Не привязан к конкретной базе данных или механизму хранения.

  • Используется целая система встроенных типов данных (также при необходимости можно создать собственные типы).

Свой эксперимент я проводил на двух микросервисах, реализованных на Java c использованием Spring Boot фреймворка. Исходные коды вы можете посмотреть по этому адресу https://gitlab.com/distillery-playground/graphql-in-action.git.

Для генерации тестовых данных использовал интернет ресурс https://random-data-api.com.

Первый микросервис (graphql-user) служит для управления пользователями, второй (graphql-company) — для управления компаниями. Каждый из микросервисов имеет собственную БД PostgreSQL. Сущности пользователя и компании имеют следующую структуру:

@Data
@EqualsAndHashCode(of = { "id" })
@Entity(name = "_user")
@EntityListeners(AuditingEntityListener.class)
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;  
  private String uid;
  private String password;
  private String firstName;
  private String lastName;
  private String username;
  private String email;
  private String gender;
  private String phoneNumber;
  private String socialInsuranceNumber;
  private LocalDate birthday;
  private String country;  
  private String city;
  private String streetName;
  private String streetAddress;
  private String zipCode;
  private String cardNumber;  
  private Long companyId;
  @CreatedDate
  private Instant createdAt;
  @LastModifiedDate
  private Instant updatedAt;
}

@Data
@EqualsAndHashCode(of = { "id" })
@Entity(name = "company")
@EntityListeners(AuditingEntityListener.class)
public class Company {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String uid;
  private String businessName;
  private String suffix;
  private String industry;
  private String catchPhrase;  
  private String buzzword;
  private String bsCompanyStatement;
  private String employeeIdentificationNumber;
  private String dunsNumber;
  private String logo;
  private String type;
  private String phoneNumber;
  private String fullAddress;
  private Float latitude;
  private Float longitude;
  @CreatedDate
  private Instant createdAt;
  @LastModifiedDate
  private Instant updatedAt;
}

Сущность пользователя содержит привязку к компании, таким образом, при запросе информации о пользователе, нам необходимо обратиться в микросервис graphql-company для обогащения данными о компании. Взаимодействие реализовано через HTTP запросы:

@Component
@RequiredArgsConstructor
public class GraphqlCompanyClientImpl implements GraphqlCompanyClient {

  private final RestTemplate graphqlCompanyRestTemplate;
  private final GraphqlCompanyProperties properties;

  @Override
  @Retryable(interceptor = "graphqlCompanyRetryInterceptor")
  public CompanyDto getCompany(Long id) {
    try {
      var builder = UriComponentsBuilder.fromPath(properties.getCompanyUrl()).queryParam("id", id);
      return graphqlCompanyRestTemplate
          .exchange(builder.toUriString(), HttpMethod.GET, null, new ParameterizedTypeReference<CompanyDto>() {
          }).getBody();
    } catch (ResourceAccessException e) {
      throw new DownstreamException(Downstream.GRAPHQL_COMPANY, e);
    }
  }
}

Cервер GraphQL развернут в микросервисе управления пользователями. Для этого я использовал следующий набор зависимостей:

  • graphql-spring-boot-starter — используется для включения сервлета GraphQL, который будет доступен по пути /graphql. Он инициализирует GraphQLSchema бин.

  • graphql-java — используется для создания схем на языке GraphQL.

graphiql-spring-boot-starter – предоставляет пользовательский интерфейс, с помощью которого мы сможем тестировать наши запросы на GraphQL и просматривать определения запросов.

<!-- GraphQL dependencies -->
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>
    <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>
    <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>

GraphQL API построен на двух основных блоках: запросах (queries) и схеме (schema).

GraphQL поставляется с собственным языком для написания схем, который называется Schema Definition Language. Определение схемы состоит из всех функций API, доступных в конечной точке. Схема, используемая нашим GraphQL сервером, выглядит следующим образом:

scalar Date

type User {
	id: ID!,
	uid: String,
	password: String,
	firstName: String,
	lastName: String,
	username: String,
	email: String,
	gender: String,
	phoneNumber: String,
	socialInsuranceNumber: String,
	birthday: Date,
	country: String,
	city: String,
	streetName: String,
	streetAddress: String,
	zipCode: String,
	cardNumber: String,
	company: Company
}

type Company {
	id: ID!,
	uid: String,
	businessName: String,
	suffix: String,
	industry: String,
	catchPhrase: String,
	buzzword: String,
	bsCompanyStatement: String,
	employeeIdentificationNumber: String,
	dunsNumber: String,
	logo: String,
	type: String,
	phoneNumber: String,
	fullAddress: String,
	latitude: Float!,
	longitude: Float!
}

type Query {
	getUsers(isAddCompany: Boolean!):[User]
	getUser(id: ID!, isAddCompany: Boolean!):User
}

type Mutation {
	createUser(uid: String, password: String, firstName: String, lastName: String, username: String,
	email: String, gender: String, phoneNumber: String, socialInsuranceNumber: String, birthday: Date!,
	country: String, city: String, streetName: String, streetAddress: String, zipCode: String,
	cardNumber: String, companyId: ID!):User
	updateUser(id: ID!, uid: String, password: String, firstName: String, lastName: String, username: String,
	email: String, gender: String, phoneNumber: String, socialInsuranceNumber: String, birthday: Date!,
	country: String, city: String, streetName: String, streetAddress: String, zipCode: String,
	cardNumber: String, companyId: ID!):User	
}

В ней описаны пользовательские типы — User и Company, методы — getUser, getUsers, createUser и updateUser, которые будут доступны для вызова внешними клиентами. Также при описании пользовательских типов используем созданный нами скалярный тип Date, код которого приведен ниже.

@Component
public class DateScalarType extends GraphQLScalarType {
  
  private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");

  public DateScalarType() {
    super("Date", "Date", new Coercing<Object, Object>() {
      @Override
      public Object serialize(Object o) throws CoercingSerializeException {
        return ((LocalDate) o).format(formatter);
      }

      @Override
      public Object parseValue(Object o) throws CoercingParseValueException {
        return o;
      }

      @Override
      public Object parseLiteral(Object o) throws CoercingParseLiteralException {
        if (o == null) {
          return null;
        }
        return LocalDate.parse(((StringValue) o).getValue(), formatter);
      }
    });
  }
}

Схему необходимо разместить в ресурсах в папке с названием «graphql». Файл схемы может иметь произвольное название и должен включать расширение «graphqls». Все поля, указанные в возвращаемых типах, будут доступны для клиента.

Для обработки входящих запросов нам необходимо создать имплементации интерфейсов GraphQLMutationResolver и GraphQLQueryResolver. В имплементацию GraphQLMutationResolver добавлены методы модифицирующие данные, такие как создание и обновление сущностей, а в имплементацию GraphQLQueryResolver – методы чтения данных.

@Component
@RequiredArgsConstructor
public class UserMutation implements GraphQLMutationResolver {
  
  private final UserService userService;
  
  public UserDto createUser(String uid, String password, String firstName, String lastName, String username,
      String email, String gender, String phoneNumber, String socialInsuranceNumber, LocalDate birthday, String country,
      String city, String streetName, String streetAddress, String zipCode, String cardNumber, Long companyId) {
    return userService.createUser(uid, password, firstName, lastName, username,
        email, gender, phoneNumber, socialInsuranceNumber, birthday, country, city, streetName, streetAddress, zipCode,
        cardNumber, companyId);
  }
  
  public UserDto updateUser(Long id, String uid, String password, String firstName, String lastName, String username,
      String email, String gender, String phoneNumber, String socialInsuranceNumber, LocalDate birthday, String country,
      String city, String streetName, String streetAddress, String zipCode, String cardNumber, Long companyId) {
    return userService.updateUser(id, uid, password, firstName, lastName, username,
        email, gender, phoneNumber, socialInsuranceNumber, birthday, country, city, streetName, streetAddress, zipCode,
        cardNumber, companyId);
  }
}
@Component
@RequiredArgsConstructor
public class UserQuery implements GraphQLQueryResolver {

  private final UserService userService;

  public List<UserDto> getUsers(boolean isAddCompany) {
    return userService.getUsers(isAddCompany);
  }

  public Optional<UserDto> getUser(Long id, boolean isAddCompany) {
    return userService.getUser(id, isAddCompany);
  }
}

При запросе информации о пользователе через параметр можно указать необходимость добавления информации о компании.

Для тестирования функционала можно воспользоваться инструментом GraphiQL. Для этого необходимо перейти по адресу http://localhost:8080/graphiql.

 Сначала запросы в GraphQL (query) отправляют на сервер. Затем они возвращаются как ответ клиенту в формате JSON. Неважно, откуда поступают данные, их можно запросить конкретной командой.

Протестировать функционал также можно через Postman:

curl --location --request POST 'http://localhost:8080/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query {\r\n    getUsers(isAddCompany: true) {\r\n        id,\r\n        uid,\r\n        password,\r\n        firstName,\r\n        lastName,\r\n        username,\r\n        birthday,\r\n        company {\r\n            id,\r\n            uid,\r\n            businessName,\r\n            phoneNumber,\r\n            fullAddress\r\n        }\r\n    }\r\n}","variables":{}}'

Примеры запросов для тестирования функционала в GraphiQL:

1. Создание пользователя.

mutation {
  createUser(
    uid: "8aa68c80-4635-461f-8707-0bf5b7691119",
    password: "qwerty",
    firstName: "James",
  	lastName: "Bond",
    username: "bond007",
    email: "james.bond@distillery.com",
    gender: "Male",
    phoneNumber: "+77777777777",
    socialInsuranceNumber: null,
 		birthday: "11-11-1991",
    country: "Russia",
    city: "Moscow",
    streetName: "1 May",
    streetAddress: "777",
    zipCode: null,
    cardNumber: null,
  	companyId: 10)
  {id}
}

2. Изменение пользователя.

mutation {
  updateUser(
    id: 8,
    uid: "8aa68c80-4635-461f-8707-0bf5b7691119",
    password: "123",
    firstName: "James",
  	lastName: "Bond",
    username: "bond007",
    email: "james.bond@distillery.com",
    gender: "Male",
    phoneNumber: "+77777777777",
    socialInsuranceNumber: null,
 		birthday: "11-11-1991",
    country: "Russia",
    city: "Moscow",
    streetName: "1 May",
    streetAddress: "777",
    zipCode: null,
    cardNumber: null,
  	companyId: 10)
  {
    id,
    uid,
    password,
    firstName,
  	lastName,
    username,
    email,
    gender,
    phoneNumber,
    socialInsuranceNumber,
 		birthday,
    country,
    city,
    streetName,
    streetAddress,
    zipCode,
    cardNumber,
  	company {
      id,
      uid,
      businessName,
      phoneNumber,
      fullAddress
    }
  }
}

3. Запрос информации о пользователе с определенным id.

query {
  getUser(id: 3876, isAddCompany: true) {
    id,
    uid,
    password,
    firstName,
    lastName,
    username,
    birthday,
    company {
      id,
      uid,
      businessName,
      phoneNumber,
      fullAddress
    }
  }
}

GraphQL подходит для работы с приложении с микросервисной архитектурой. С другой стороны, есть более привычная альтернатива — REST запросы, а также технология BFF (Backend-for-Frontend), которая позволяет принимать запросы от мобильных приложений и других клиентов на промежуточном слое и для каждого клиента формировать нужный набор данных.

Комментарии (10)


  1. alexesDev
    28.03.2022 12:00

    GraphQL апи можно программно склеивать друг с другом через graphql-tools как душе угодно. Поэтому оно подходит для микросервисов. У REST и других подходов нет этой фишки.


  1. debagger
    28.03.2022 12:04

    Так зачем использовать GraphQL? Прочитал статью - так и не понял...


  1. jakobz
    28.03.2022 12:27

    А что происходит, если от бизнеса приходит задача: хотим табличку всех пользователей, с пейджингом, и сортировкой по колонке company business name?


    1. alexesDev
      28.03.2022 12:29
      +1

      Есть инструменты типа https://www.graphile.org/postgraphile/

      Такие простые запросы прогать на беке нет смысла. Базовое апи, которое покроет ваш кейс тул даст написать.


      1. jakobz
        29.03.2022 08:19

        Задачка с подвохом. По тексту, люди и кампании лежат в разных микросервисах и разных базах. А запрос должен включать обе базы сразу.


        1. alexesDev
          29.03.2022 15:32

          Это тоже не проблема. GraphQL дает писать resolver для каждой "вершины". Два resolver обращаются в две разные базы и тп. Можно два апи написать с клеить их через graphql-tools и тп. Много рабочих подходов (не костылей).


  1. aml
    28.03.2022 21:49

    Высовывать наружу API, через который можно сервер уложить, это опасное дело.


    1. eee
      30.03.2022 03:06

      Если правильно настроить complexity guard, то нет


  1. makar_crypt
    29.03.2022 09:12

    Я правильно разобрался в GraphQL или это ошибка и не доработка в вашем коде, что когда делаете запрос с подгрузкой 1 к N (у объекта поле это массив с ID'шниками), происходит N запросов вида getbyID ? Этоже просто абсурд для высоконагруженных приложений.


    1. eee
      30.03.2022 03:06

      Для оптимизации таких вещей есть dataloader-ы