GraphQL com Spring Boot e DGS

Por Gaspar Barancelli Junior em 15 de novembro de 2022

Introdução ao DGS (Domain Graph Service)

É um projeto open source desenvolvido pela Netflix, que visa facilitar a implementação de GraphQL em um aplicativo Spring Boot, onde a estrutura do DGS é baseada em anotações do Spring Boot.

Quando o projeto DGS começou?

O projeto DGS inicio em 2019 na Netflix, quando equipes internas começaram a desenvolver vários serviços de GraphQL. No final de 2020 a Netflix decidiu abrir o código fonte, para construir uma comunidade ao ser redor.

DGS está pronto para produção?

Sim! A Netflix usa a estrutura há mais de um ano e meio em diferentes partes da organização, inclusive em grande escala.

Demonstração

Crie um novo projeto Spring Boot

A estrutura do DGS como falamos anteriormente é baseada no Spring Boot, então vamos começar criando um novo aplicativo Spring Boot. Para isso vamos utilizar o Spring Initializr.

A única dependência necessária é a Spring Web.

1
Figure 1. Exemplo de configuração do novo projeto no Spring Initializr

Após efetuar o download do aplicativo, descompacte o arquivo e abra o projeto em sua IDE.

Adicionando a dependência do DGS

Adicione a seguinte dependência à configuração do Maven.

<dependency>
    <groupId>com.netflix.graphql.dgs</groupId>
    <artifactId>graphql-dgs-spring-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

O recurso de federação da biblioteca não se encontra no repositório central do Maven. Caso você queira utilizar este recurso, é necessário configurar o repositório jcenter no Maven. Para que todos os seus projetos possam realizar downloads no repositório jcenter, você deve criar um ou editar o arquivo settings.xml que fica no diretório .m2, esse é o diretório de artefatos do Maven, em todos os sistemas operacionais este diretório oculto fica no diretório raiz do usuário.

<settings xsi:schemaLocation='http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd'
xmlns='http://maven.apache.org/SETTINGS/1.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>


    <profiles>
        <profile>
            <repositories>
                <repository>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                    <id>central</id>
                    <name>bintray</name>
                    <url>https://jcenter.bintray.com</url>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                    <id>central</id>
                    <name>bintray-plugins</name>
                    <url>https://jcenter.bintray.com</url>
                </pluginRepository>
            </pluginRepositories>
            <id>bintray</id>
        </profile>
    </profiles>
    <activeProfiles>
        <activeProfile>bintray</activeProfile>
    </activeProfiles>
</settings>

Criando um esquema

As estruturas do DGS devem ser armazenadas no diretório resources/schema/schema.graphqls.

2
Figure 2. Exemplo dos diretórios onde devem ficar as estruturas do DGS

Abaixo segue os dados da estrutura.

scalar LocalDate

type Query {
    promocoes(dataFilter: LocalDate): [Promocao]
    produtos(descricaoFilter: String): [Produto]
}

type Promocao {
    descricao: String
    desconto: Float
    dataInicial: LocalDate
    dataFinal: LocalDate
}

type Produto {
    id: Int
    descricao: String
    valor: Float
}

Este esquema permite consultar uma lista de produtos e promoções, filtrando opcionalmente o produto por sua descrição e as promoções por data.

Implementar objetos utilizados na estrutura

Segue o código fonte das classes Java que tem como base as estruturas adicionadas no DGS.

import java.math.BigDecimal;

public class Produto {

    private final Long id;
    private final String descricao;
    private final BigDecimal valor;

    public Produto(Long id, String descricao, BigDecimal valor) {
        this.id = id;
        this.descricao = descricao;
        this.valor = valor;
    }

    public Long getId() {
        return id;
    }

    public String getDescricao() {
        return descricao;
    }

    public BigDecimal getValor() {
        return valor;
    }

}
import java.math.BigDecimal;
import java.time.LocalDate;

public class Promocao {

    private final String descricao;
    private final BigDecimal desconto;
    private final LocalDate dataInicial;
    private final LocalDate dataFinal;

    public Promocao(String descricao, BigDecimal desconto, LocalDate dataInicial, LocalDate dataFinal) {
        this.descricao = descricao;
        this.desconto = desconto;
        this.dataInicial = dataInicial;
        this.dataFinal = dataFinal;
    }

    public String getDescricao() {
        return descricao;
    }

    public BigDecimal getDesconto() {
        return desconto;
    }

    public LocalDate getDataInicial() {
        return dataInicial;
    }

    public LocalDate getDataFinal() {
        return dataFinal;
    }
}

Implementar serviços

Vamos simular o desenvolvimento de um serviço que obtenha os dados de produtos e promoções, e que também apliquem os filtros por descrição de produto e data de promoções.

import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
public class ProdutoService {

    private final List<Produto> produtos = List.of(
            new Produto(1L, "Cadeira", BigDecimal.valueOf(110)),
            new Produto(2L, "Mesa", BigDecimal.valueOf(999)),
            new Produto(3L, "Monitor", BigDecimal.valueOf(1200)),
            new Produto(4L, "Mouse", BigDecimal.valueOf(320.50)),
            new Produto(5L, "Teclado", BigDecimal.valueOf(275))
    );

    public List<Produto> get(String descricao) {
        if (Objects.isNull(descricao)) {
            return produtos;
        }

        return produtos.stream()
                .filter(s -> s.getDescricao().contains(descricao))
                .collect(Collectors.toList());
    }
}
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
public class PromocaoService {

    private final List<Promocao> promocoes = List.of(
            new Promocao("Dia das mães", BigDecimal.TEN, LocalDate.parse("2021-05-01"), LocalDate.parse("2021-05-09")),
            new Promocao("Natal", BigDecimal.valueOf(13), LocalDate.parse("2021-12-03"), LocalDate.parse("2021-12-25"))
    );

    public List<Promocao> get(LocalDate data) {
        if (Objects.isNull(data)) {
            return promocoes;
        }

        return promocoes.stream()
                .filter(s -> s.getDataInicial().compareTo(data) <= 0 && s.getDataFinal().compareTo(data) >= 0)
                .collect(Collectors.toList());
    }

}

Implementar um buscador de dados

Os buscadores de dados são responsáveis por devolver dados para uma consulta.

import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;

import java.util.List;

@DgsComponent
public class ProdutosDatafetcher {

    private final ProdutoService produtoService;

    public ProdutosDatafetcher(ProdutoService produtoService) {
        this.produtoService = produtoService;
    }

    @DgsData(parentType = "Query", field = "produtos")
    public List<Produto> produtos(@InputArgument("descricaoFilter") String descricao) {
        return produtoService.get(descricao);
    }

}
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;

import java.time.LocalDate;
import java.util.List;

@DgsComponent
public class PromocaoDatafetcher {

    private final PromocaoService promocaoService;

    public PromocaoDatafetcher(PromocaoService promocaoService) {
        this.promocaoService = promocaoService;
    }

    @DgsData(parentType = "Query", field = "promocoes")
    public List<Promocao> promocoes(@InputArgument("dataFilter") LocalDate data) {
        return promocaoService.get(data);
    }

}

Implementar conversores

Como utilizamos o objeto LocalDate na classe de promoções e também na definição da estrutura do DGS, precisamos desenvolver um conversos de objetos, para que o GraphQL saiba converter um objeto do tipo LocalDate para String e de String para LocalDate.

Não se desespere, isto é muito simples de ser desenvolvido. Basta criar uma classe que implemente Coercing e que esteja anotada com @DgsScalar.

import com.netflix.graphql.dgs.DgsScalar;
import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@DgsScalar(name="LocalDate")
public class DateTimeScalar implements Coercing<LocalDate, String> {

    @Override
    public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
        if (dataFetcherResult instanceof LocalDate) {
            return ((LocalDate) dataFetcherResult).format(DateTimeFormatter.ISO_DATE);
        } else {
            throw new CoercingSerializeException("Not a valid DateTime");
        }
    }

    @Override
    public LocalDate parseValue(Object input) throws CoercingParseValueException {
        return LocalDate.parse(input.toString(), DateTimeFormatter.ISO_DATE);
    }

    @Override
    public LocalDate parseLiteral(Object input) throws CoercingParseLiteralException {
        if (input instanceof StringValue) {
            return LocalDate.parse(((StringValue) input).getValue(), DateTimeFormatter.ISO_DATE);
        }

        throw new CoercingParseLiteralException("Value is not a valid ISO date time");
    }

}

Este é todo o código necessário, o aplicativo está pronto para ser testado.

Testar aplicação

A biblioteca disponibiliza o endpoint graphiql, para que possamos realizar as consultas utilizando o GraphQL.

Como não alteramos a porta de padrão do nosso servidor de aplicação, podemos acessar o seguinte endereço para executar as consultas.

http://localhost:8080/graphiql
3
Figure 3. Página de consultas do GraphQL

Para consultarmos somente a descrição dos produtos, podemos executar a seguinte consulta.

{
    produtos {
        descricao
    }
}
{
  "data": {
    "produtos": [
      {
        "descricao": "Cadeira"
      },
      {
        "descricao": "Mesa"
      },
      {
        "descricao": "Monitor"
      },
      {
        "descricao": "Mouse"
      },
      {
        "descricao": "Teclado"
      }
    ]
  }
}

Podemos retorna os dados das consultas de produtos e promoções.

{
    produtos {
        id
        descricao
    },
    promocoes {
      descricao
      desconto
    }
}
{
  "data": {
    "produtos": [
      {
        "id": 1,
        "descricao": "Cadeira"
      },
      {
        "id": 2,
        "descricao": "Mesa"
      },
      {
        "id": 3,
        "descricao": "Monitor"
      },
      {
        "id": 4,
        "descricao": "Mouse"
      },
      {
        "id": 5,
        "descricao": "Teclado"
      }
    ],
    "promocoes": [
      {
        "descricao": "Dia das mães",
        "desconto": 10
      },
      {
        "descricao": "Natal",
        "desconto": 13
      }
    ]
  }
}

Observe que adicionamos nas consultas apenas os campos que desejamos que sejam retornados.

Como último exemplo, vamos filtrar os produtos e promoções.

{
    produtos(descricaoFilter: "M") {
        id
        descricao
    },
    promocoes(dataFilter: "2021-05-04") {
      descricao
      desconto
      dataFinal
    }
}
{
  "data": {
    "produtos": [
      {
        "id": 2,
        "descricao": "Mesa"
      },
      {
        "id": 3,
        "descricao": "Monitor"
      },
      {
        "id": 4,
        "descricao": "Mouse"
      }
    ],
    "promocoes": [
      {
        "descricao": "Dia das mães",
        "desconto": 10,
        "dataFinal": "2021-05-09"
      }
    ]
  }
}

Conclusão

Neste post desenvolvemos um serviço GraphQL utilizando a biblioteca DGS da Netflix num projeto Spring Boot.

O código fonte da aplicação desenvolvida se encontra neste repositório no github.

// Compartilhe esse Post

💫
🔥 NOVO APP

Domine o Inglês em 30 dias!

Inteligência Artificial + Repetição Espaçada • Método cientificamente comprovado

✅ Grátis para começar 🚀 Resultados rápidos
×