Привет, Хабр!

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

Настройка проекта / Зависимости

Добавьте следующие зависимости в ваш maven проект:

<dependency>
    <groupId>io.leangen.graphql</groupId>
    <artifactId>graphql-spqr-spring-boot-starter</artifactId>
    <version>0.0.6</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>

Чтобы понять основные подходы SpqrAutoConfigurationрегистрирует компонент для каждой из трех встроенных ResolverBuilderреализаций:

  • AnnotatedResolverBuilder- предоставляет только методы, аннотированные @GraphQLQuery@GraphQLMutationили@GraphQLSubscription

  • PublicResolverBuilder- предоставляет все publicметоды из исходного класса операций (возвращаемые методы voidсчитаются мутациями)

  • BeanResolverBuilder- предоставляет все геттеры как запросы и сеттеры как мутации (возвращаемые геттеры Publisher<T>считаются подписками)

Также возможно реализовать собственные, реализовав свой ResolverBuilder интерфейс.

Описание основных аннотаций SPQR

@GraphQLApi- аналог контроллера, в котором определены Query и Mutation.

@GraphQLQuery - QUERY, аналог read запросов

@GraphQLMutation- MUTATION, аналог create/delete/update запросов

Остальные аннотации будут описаны ниже.

Описание сущности

Сущность банковский счет имеет номер счета (numberAccount), валюту (currency) и текущий счет (balance).

@Getter
@Setter
@Entity
@Table(name = "bank_account")
@FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor
public class BankAccountEntity {
    @Id
    String numberAccount;
    
    Currency currency;

    BigDecimal balance;
}

Опишем dto для работы с этой сущностью:

@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BankAccountDto {
    @GraphQLId
    String numberAccount;

    @NotNull
    @GraphQLScalar
    @GraphQLInputField
    Currency currency;

    @NotNull
    @GraphQLInputField
    BigDecimal balance;
}

Здесь аннотация @GraphQLInputField говорит GraphQL, что текущее поле будет полем для обновления или ввода. Попрошу заметить, что у поля currency стоит аннотация @GraphQLScalar - один из способов реализовать сложный скаляр.

package ru.bank.web.dataFetcher;

import io.leangen.graphql.annotations.GraphQLArgument;
import io.leangen.graphql.annotations.GraphQLMutation;
import io.leangen.graphql.annotations.GraphQLNonNull;
import io.leangen.graphql.annotations.GraphQLQuery;
import io.leangen.graphql.spqr.spring.annotations.GraphQLApi;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
import ru.bank.business.service.BankAccountService;
import ru.bank.web.dto.BankAccountDto;
import ru.bank.web.dto.DispatchMoneyDto;

import java.util.List;

@RequiredArgsConstructor
@Component
@GraphQLApi
public class BankAccountDataFetcher {
    private final BankAccountService service;

    @GraphQLQuery(name = "getAllBalance")
    public List<BankAccountDto> getAll(@GraphQLArgument(name = "page")Pageable pageable) {
        return service.getAll(pageable);
    }

    @GraphQLMutation(name = "createBankAccount", description = "create a new bank account")
    public BankAccountDto createBankAccount(@GraphQLArgument(name = "createBankAccountInput") @GraphQLNonNull BankAccountDto bankAccount) {
        return service.create(bankAccount);
    }

    @GraphQLMutation(name = "dispatchMoney", description = "dispatch money")
    public void dispatchMoney(@GraphQLArgument(name = "createDispatchMoneyInput") @GraphQLNonNull DispatchMoneyDto dispatchMoneyDto) {
        service.dispatchMoney(dispatchMoneyDto);
    }

    @GraphQLMutation(name = "removeBankAccount", description = "remove bank account")
    public void removeBankAccount(@GraphQLNonNull String numberAccount) {
        service.remove(numberAccount);
    }
}

Параметр аннотации @GraphQLMutation name - указывает имя метода для GraphQL, а description - соответственно описание метода для graphiql

Отправка запросов

Отправка Query с пагинацией для получения данных по аккаунтам
Отправка Query с пагинацией для получения данных по аккаунтам
Отправка Mutation для удаления аккаунта по ключу
Отправка Mutation для удаления аккаунта по ключу
Отправка Mutation для создания нового аккаунта
Отправка Mutation для создания нового аккаунта

Остальной код можно посмотреть, перейдя по ссылке: https://github.com/gibkin/graphql-sqpr-spring-boot-starter

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


  1. js605451
    09.12.2021 04:34
    +4

    @GraphQLQuery(name = "getAllBalance")
    public List<BankAccountDto> getAll() {
      return service.getAll();
    }

    Ненене, погодите, а паджинация, фильтрация, сортировка - это все руками надо делать чтоли?


    1. gibsonen Автор
      09.12.2021 09:32

      Спасибо за замечание, поправлю статью в ближайшее время


    1. gibsonen Автор
      09.12.2021 14:28

      Поправил


      1. js605451
        09.12.2021 21:55

        Я имел в виду более прямую связь между GraphQL-фасадом и источником данных. Например программист определяет источник данных на уровне "select * from BankAccounts", а SPQR берёт на себя управление частями "where", "order by" и "skip/limit".

        Вот у вас написано:

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

        Что именно SPQR делает для того, чтобы "клиент смог выбрать именно то, что ему нужно"?


        1. gibsonen Автор
          10.12.2021 01:30

          Обновлю на выходных


        1. gibsonen Автор
          10.12.2021 01:53

          Добавлю работу с querydsl


  1. chilicoder
    09.12.2021 14:21

    Есть похожая библиотека на Java под названием Elide, которая позволяет следовать не только GraphQL, но и JSON-API спецификации. Не сравнивали?

    JSON-API очень удобная спецификация. Хорошо описывает не только чтение, но и запись. Вот пост с сравнением: https://dri.es/headless-cms-rest-vs-jsonapi-vs-graphql

    По своему опыту могу сказать, что следование JSON-API при взаимодействии FE<->BE сводит согласования только к согласованию реляционных моделей. Что очень позитивно сказывается на скорости интеграции.