Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Меня зовут Рустам, и я техлид в компании 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), которая позволяет принимать запросы от мобильных приложений и других клиентов на промежуточном слое и для каждого клиента формировать нужный набор данных.