Ejemplo GraphQL con Spring Boot y Netflix DGS

springboot-grpahql-dgs

springboot-grpahql-dgs


En esta nueva entrada, Ejemplo GraphQL con Spring Boot y Netflix DGS, veremos como construir una aplicación con Netflix DGS (Domain Graph Service) que nos facilitará el desarrollo con GraphQL.

¿Qué es Netflix Domain Graph Service?

Netflix Domain Graph Service (DGS), es un nuevo framework open source creado internamente en Netflix que simplifica y ayuda en la implementación de aplicaciones Spring Boot con GraphQL.

Entre las caraterísticas de DGS podemos destacar:

  • Es un sistema basado en anotaciones al estilo Spring Boot.
  • Se integra con Spring Security.
  • Gestión de errores.
  • Cliente GraphQL para Java.
  • Soporta WebSockets, SSE , subida de ficheros o GraphQL Federation.

Hands On

El ejemplo completo se encuentra en nuestro github.

Vamos manos a la obra para construir nuestro ejemplo GraphQL con Spring Boot y Netflix DGS.

Para nuestro ejemplo vamos a usar una base de datos relacional en memoria con H2, para simplificar nuestro ejemplo haciendo uso de JPA. Nuestro ejemplo consistirá en tres entidades muy básicas: Bank, User y Account Bank, en donde un usuario tiene una o N cuentas bancarias en un banco y un banco tiene 1 o N cuentas bancarias.

En nuestra aplicación haremos uso de Lombok para eliminar el boilerplate de Java.

Dependencias Maven para usar Netflix DGS

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

La anterior dependencia es la única necesaria para añadir Netflix DGS a nuestra aplicación. Obviamente serán necesarias añadir más dependencias para nuestra aplicación.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

Creación Schemas GraphQL

Al igual que hacemos con una base de datos relacional, vamos a hacer lo mismo con GraphQL, es decir, crear los schemas. Los schemas que creamos deben ir en la siguiente ruta /src/main/resources/schemas, y en esta ruta se encargará Netflix DGS de cargar los esquemas.

Para cada schema hemos creado un fichero diferente con extensión graphqls. En los cuales tenemos un tipo QueryResolver que tendrá dos búsquedas diferentes y un MutationResolver para poder crear el objeto en base de datos.

Tanto QueryResolver como MutationResolver deberán ser definidos como sustitución a Query y Mutation, abajo se podrá ver.

Para nuestro ejemplo vamos a crear 3 esquemas diferentes, account, user y bank:

type QueryResolver {
    users: [User]
    user(id: ID!): User!
}

type MutationResolver {
    createUser(user: UserInput!): User
}

input UserInput {
    firstName: String!
    lastName: String!
    address: String!
    country: String!
    city: String!
    age: Int
}

type User {
    id: ID!
    firstName: String!
    lastName: String!
    address: String!
    country: String!
    city: String!
    age: Int!
    bank: Bank
    accounts: [Account]
}

schema {
    query: QueryResolver
    mutation: MutationResolver
}


En el fichero anterior hemos visto la creación del usuario haciendo uso de un QueryResolver para las búsquedas y de un MutationResolver para las creaciones. Además verás que hemos redefinido, en el schema query y mutation por MutationResolver y QueryResolver, en el caso de no definirlo tendrá que ser llamado Query y Mutation.

extend type QueryResolver {
    banks: [Bank]
    bank(id: String!): Bank!
}

extend type MutationResolver {
    createBank(bank: BankInput!): Bank
}

input BankInput {
    name: String!
    country: String!
}

type Bank {
    id: ID!
    name: String!
    country: String!
    users: [User]
    accounts: [Account]
}

La principal diferencia con el schema anterior, es que en Bank, como en el siguiente schema, hacemos uso de extend (para QueryResolver y para MutationResolver) porque no podríamos tener más del mismo tipo y hay que hacer uso de extend.

extend type QueryResolver {
    accounts: [Account]
    account(id: ID!): Account!
}

extend type MutationResolver {
    createAccount(account: AccountInput!): Account
}

input AccountInput {
    name: String!
    alias: String!
    amount: Float
    userId: String!
    bankId: String!
}

type Account {
    id: ID!
    name: String!
    user: User!
    bank: Bank!
}

Con la creación de estos ficheros vemos una de las principales diferencias con Rest y que hace de Graphql un lenguaje tan atractivo, por ejemplo para consultar un objeto de tipo usuario habría que crear un endpoint y para consultar todos sería necesario otro endpoint totalmente diferente. Estos dos problemas nos los resuelve GraphQL sin necesidad de nuevos endpoints, tan solo con la definición en su esquema. Lo veremos con algún ejemplo después.

Todos aquellos campos que llevan ! son campos obligatorios en la query.

Todos aquellos campos que llevan ! son campos obligatorios en la query.

Creación de las Entidades en Spring Boot

A continuación vamos a crear las entidades que se encargarán de hacer un mapping con nuestra base de datos relacional y nuestro esquema GraphQL.

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class User {

    @Id
    @GeneratedValue
    private UUID id;

    private String firstName;

    private String lastName;

    private int age;

    private String address;

    private String country;

    private String city;

    @OneToMany(mappedBy = "user")
    private Set<Account> accounts;

    @ManyToOne()
    private Bank bank;
}

Vamos a continuar con el banco, que tendrá una relación OneTOMany con user y account.

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class Bank {

    @Id
    @GeneratedValue
    private UUID id;

    private String name;

    private String country;

    @OneToMany(mappedBy = "bank")
    private Set<User> users;

    @OneToMany(mappedBy = "bank")
    private Set<Account> accounts;
}

Y creamos finalmente el objeto account que tendrá un objeto de tipo User y otro de tipo Bank

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Account {

    @Id
    @GeneratedValue
    private UUID id;

    private String name;

    private String alias;

    private BigDecimal amount;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    private Bank bank;
}

Creación de los objetos Mutation

Como hemos comentado antes, hemos creado un tipo MutationResolver por schema, por lo que necesitamos crear un nuevo objeto con lo que le vamos a pasar para crearlo. Es decir, será el objeto con el que hará el binding cuando se hace la petición y la invocación, además tendrá el nombre que le hemos puesto en el schema.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class UserInput {

    private String firstName;

    private String lastName;

    private int age;

    private String address;

    private String country;

    private String city;
}

En la clase UserInput, hemos creado los campos con lo que se podrá crear un nuevo usuario.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class BankInput {

    private String name;

    private String country;
}

La clase BankInput, únicamente tendrá dos campos. Los ids serán creados al crear un nuevo registro.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class AccountInput {

    private String name;

    private String alias;

    private BigDecimal amount;

    private UUID userId;

    private UUID bankId;
}

Finalmente creamos, AccountInput que tendrá dos campos userId y bankId, de tipo UUID que serán las relaciones con sus correspondientes tablas en BBDD.

Creación de los Repositorios con Spring Boot

Para la capa de persistencia o repository vamos a hacer uso de JpaRepository, con lo que nos dará todas las facilidades de JPA para poder guardar y consultar nuestros objetos de Bases de Datos.

Por ejemplo para Account sería de la siguiente manera:

public interface AccountRepository extends JpaRepository<Account, UUID> {
}

Una vez hemos completado todas las entidades los schemas , los input y la capa de persistencia, viene la magia que aporta Netflix.

DGS de Netflix con Spring Boot

Netflix ha creado su framework DGS basado en anotaciones que sean soportadas por Spring Boot. A continuación os mostramos las anotaciones básicas a tener en cuenta para poder tener una aplicacón funcionando con GrpahQL y Spring Boot.

  • @DgsComponent: Esta anotación es la responsable de indicar que una clase va a realizar queries. La clase deberá llevar esta anotación en la definición.
  • @DgsData, cada método que lleve la lógica para realizar una query deberá ser anotado con @DgsData. En donde añadiremos el parent type, que es el tipo del schema y el campo que iba definido en el schema.
  • @InputArgument, para definir el argumento que se pasa por parámetro en la query.
  • @DgsQuery, es una abreviación de @DgsData cuando el parentType es query.
  • @DgsMutation, es una abreviación de @DgsData cuando el parentType es Mutation.

Otro tema a tener en cuenta al trabajar con DGS es poder crear un contexto propio implementando la clase DgsCustomContextBuilder. Lo puedes utilizar para logs o almacenar estados o guardar alguna información accediendo através de la clase DataFetchingEnvironment. En la clase del ejemplo UserQuery se puede ver su uso.

Query con DGS de Netflix en Spring Boot

A continuación veremos una clase de query con DGS de netflix haciendo uso de alguna de las anotaciones que hemos visto anteriormente:

@DgsComponent
@RequiredArgsConstructor
public class UserQuery {

    private final UserRepository userRepository;

    private final CustomContextBuilder contextBuilder;


    @DgsData(parentType = "QueryResolver", field = "users")
    public Iterable<User> findAll(DgsDataFetchingEnvironment dfe) {

        var users = (List<User>) userRepository.findAll();

        contextBuilder.customContext(users, null, null).build();

        return users;
    }

    @DgsData(parentType = "QueryResolver", field = "user")
    public User findById(@InputArgument("id") String id, DataFetchingEnvironment dfe) {

        CustomContext customContext = DgsContext.getCustomContext(dfe);

        var users = customContext.getUsers();


        if (null != users) {
            var user = users.stream().filter(u -> u.getId().equals(UUID.fromString(id))).findFirst();

            return user.orElseGet(() -> userRepository.findById(UUID.fromString(id)).orElseThrow(DgsEntityNotFoundException::new));

        } else {

            return userRepository.findById(UUID.fromString(id)).orElseThrow(DgsEntityNotFoundException::new);
        }

    }
}

A destacar de esta clase sería el uso CustomContextBuilder, que es una clase que ha creado un contexto propio en el que se añaden los diferentes objetos devueltos para luego poder acceder directamente.

Mutation con DGS de Netflix en Spring Boot

A continuación veremos la classe UserMutation que se encarga de guardar un nuevo usuario en la base de datos.

Como se puede ver en la anotación @DgsData, primero indicamos que el parentType es MutationResolver y el campo es createUser, que coincide con la definición en el schema.

@DgsComponent
@RequiredArgsConstructor
public class UserMutation {

    private final UserRepository userRepository;

    @DgsData(parentType = "MutationResolver", field = "createUser")
    public User createUser(@InputArgument("user") UserInput user) {
        return userRepository.save(new User(null, user.getFirstName(), user.getLastName(), user.getAge(), user.getAddress(), user.getCountry(), user.getCity(), null, null));
    }

}

Probando aplicación DGS de netflix con Spring Boot

Una vez hemos terminado la aplicación y tenemos todos implementado es momento de lanzar unas cuantas queries.

Ejecutamos nuestra aplicación con:

mvn spring-boot:run

Accedemos a la página http://localhost:8080/graphiql, y aparecerá una interfaz donde podemos lanzar nuestras queries.

Vamos a ver unos ejemplos:

Creación de un bank:

mutation CREATE {
   createBank(bank:
      {name: "Santander", country:"Spain"}) {
        id
        name
        country
      }   
}
Mutation y Respuesta con GraphQL |Ejemplo GraphQL con Spring Boot y Netflix DGS
Mutation y Respuesta con GraphQL

Obtener todos los banks:

{
 banks{
  id
  name
  country
 }
}
 

Consulta de un bank por id:

{
 bank(id:"db440101-ff15-435c-a475-7319118e9131"){
  id
  name
  country
 
 }
}
Consulta en GrapQL por Id | Ejemplo de GraphQL con Spring Boot y Netflix DGS
Consulta en GrapQL por Id

Con GraphQL podemos mostrar los campos que queramos, siempre y cuando se encuentren en el objeto, por ejemplo con el caso anterior podríamos solo mostrar el id de la siguiente manera:

{
 bank(id:"db440101-ff15-435c-a475-7319118e9131"){
  id
  }
}

Con estos ejemplos hemos podido ver como con la definición en el Schema de users y user, podemos obtener los datos que necesitamos, sin necesidad de crear nuevos endpoints o más lógica como sería necesario con Rest. Si necesitamos, por ejemplo alguna información más, habría que añadir en el schema ese nuevo método.

Conclusión sobre Ejemplo GraphQL con Spring Boot y Netflix DGS

El framework de Netflix sobre Domain Graph Service (DGS), ha contribuido a facilitar en gran medida los desarrollos con GraphQL con Spring Boot, siendo capaz de construir cualquier API de una manera bastante más sencilla.

El uso de GraphQL es cada vez más frecuente ya que se posiciona como una alternativa al extendido uso de Rest, debido a su adaptabilidad y flexibilidad entre otras características.

Si quieres te puedes bajar el ejemplo completo de nuestro github.

Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com y te ayudaremos encantados!


No te olvides de seguirnos en nuestras redes sociales Facebook o Twitter para estar actualizado.


1 pensamiento sobre “Ejemplo GraphQL con Spring Boot y Netflix DGS

Deja una respuesta

Tu dirección de correo electrónico no será publicada.