Saltar al contenido

Construir un servidor GraphQL con Spring Boot

Empecemos por crear los depósitos. La Bota de Primavera hace muy fácil la creación de los repositorios de CRUD.

En el paquete de repositorio, crea la interfaz AuthorRepository:

Construir un servidor GraphQL con Spring Boot
Construir un servidor GraphQL con Spring Boot
1publicinterfaceAuthorRepositoryextendsCrudRepository<Author,Long;{}

java

Y la interfaz del Depósito de Libros:

1interfaz públicaLibroRepositorioextiendeCrudRepositorio<Libro, Largo
java

Spring Boot generará una implementación con métodos para encontrar, guardar, contar y borrar para las entidades de Autor y Libro.

De esta manera, implementar un GraphQLQueryResolver es sencillo. En el paquete del resolver, crea la clase Query:

123456789101112131415161718192021222324publicclassQueryimplmentsGraphQLQueryResolver{privateBookRepository bookRepository;privateAuthorRepository authorRepository;publicQuery(AuthorRepository authorRepository,BookRepository bookRepository){this.authorRepository = authorRepository;this. bookRepository = bookRepository;}publicIterable<Book;};findAllBooks(){return bookRepository.findAll();}publicIterable<Author;} findAllAuthors(){return authorRepository. findAll();}publiclongcountBooks(){return bookRepository.count();}publiclongcountAuthors(){return authorRepository.count();}}

java

Crear la clase de mutación requiere un poco más de trabajo, pero sigue siendo sencillo. Después de inyectar el autor y los depósitos de libros en el constructor:

123456789publicclassMutationimplmentsGraphQLMutationResolver{privateBookRepository bookRepository;privateAuthorRepository authorRepository;publicMutation(AuthorRepository authorRepository,BookRepository bookRepository){this.authorRepository = authorRepository;this.bookRepository = bookRepository;}}

java

Basándose en el esquema GraphQL, es necesario definir los métodos para crear nuevos autores y libros, tomando los parámetros declarados y guardando las entidades utilizando los repositorios correspondientes:

123456789101112131415161718192021222324publicclassMutationimplmentsGraphQLMutationResolver{// ...publicAuthornewAuthor(String firstName,String lastName){Author author =newAuthor(); author.setFirstName(firstName); author.setLastName(lastName); authorRepository. save(author);return author;}publicBooknewBook(String title,String isbn,Integer pageCount,Long authorId){Book book =newBook(); book.setAuthor(newAuthor(authorId)); book. setTitle(title); book.setIsbn(isbn); book.setPageCount(pageCount !=null? pageCount :0); bookRepository.save(book);return book;}}

java

Fíjese que el tipo ID de GraphQL puede ser convertido a los tipos de Java String, Integer y Long y los nombres de los parámetros del método y del esquema no tienen por qué coincidir. Sin embargo, hay que tener en cuenta el número de parámetros, su tipo, y si son opcionales o no.

A continuación, aquí está el método de eliminación:

12345678publicclassMutationimplmentsGraphQLMutationResolver{// ...publicbooleandeleteBook(Long id){ bookRepository.delete(id);returntrue;}}

java

Y finalmente, el método para actualizar el número de páginas del libro:

123456789101112publicclassMutationimplmentsGraphQLMutationResolver{// ...publicBookupdateBookPageCount(Integer pageCount,Long id){Libro libro = libroRepositorio.findOne(id); book.setPageCount(pageCount); bookRepository.save(book);return book;}}

java

Sin embargo, hay algo en este método que no es del todo correcto.

¿Y si la identificación pasada como argumento es inválida?

El método arrojará un NullPointerException cuando el libro a actualizar no se encuentre. Esto se traducirá en un Error interno del servidor en el lado del cliente.

Pero el asunto es que, por defecto, cualquier excepción no manejada en el lado del servidor llegará al cliente como un genérico Error interno del servidor .

Echa un vistazo al gestor de errores del Servlet GraphQL por defecto:

123456789101112131415161718192021222324252627public classDefaultGraphQLErrorHandlerimplmentsGraphQLErrorHandler{// ... @OverridepublicList<GraphQLError>processErrors(List<GraphQLError> errores){finalList<GraphQLError> clientErrors =filterGraphQLErrors(errors);if(clientErrors.size()< errors.size()){// Algunos errores fueron filtrados para ocultar la implementación - poner un error genérico en su lugar. clientErrors.add(newGenericGraphQLError("Internal Server Error(s) whileecuting query")); errors.stream().filter(error ->!isClientError(error)).forEach(error -§;{if(error instanceofThrowable){ log. error("Error ejecutando la consulta!",(Throwable) error);}else{ log.error("Error ejecutando la consulta ({}): {}", error.getClass().getSimpleName(), error.getMessage();}});}regresar ClientErrors;}// ...}

java

Sólo se procesan los errores del cliente (por ejemplo, cuando se escribe mal el nombre de un campo). El resto de los errores se tratan como un error genérico y luego se registran.

Si quieres que el cliente reciba el mensaje correcto, lo primero que tienes que hacer es crear una excepción personalizada que implemente GraphQLError. Por ejemplo, una clase BookNotFoundException:

123456789101112131415161718192021222324public classBookNotFoundExceptionextendsRuntimeExceptionimplmentsGraphQLError{privateMap<String,Object > extensions =newHashMap<>();publicBookNotFoundException(String message,Long invalidBookId){super(message); extensions. put("invalidBookId", invalidBookId);}@OverridepublicList<SourceLocation;};getLocations(){returnnull;}@OverridepublicMap<String,Object;};getExtensions(){return extensions;}@OverridepublicErrorTypegetErrorType(){returnErrorType.DataFetchingException;}}

java

GraphQLError proporciona un campo llamado extensiones para pasar datos adicionales al objeto de error enviado al cliente. En este caso, lo usaremos para pasar el ID del libro inválido.

De esta manera, en la actualización del método BookPageCountmethod, si el libro no puede ser encontrado, tiramos esta excepción:

123456789101112131415publicclassMutationimplmentsGraphQLMutationResolver{// ...publicBookupdateBookPageCount(Página enteraCount,Long id){Libro libro = libroRepositorio. findOne(id);if(book ==null){thrownewBookNotFoundException("El libro a actualizar no fue encontrado", id);} book.setPageCount(pageCount); bookRepository.save(book);return book;}}

java

Usando una excepción como un GraphQLError, el cliente recibirá el rastro completo de la pila además del mensaje de excepción.

Si no quieres este comportamiento, puedes crear una clase adaptadora para ocultar la excepción (que puede ser envuelta en una clase ExceptionWhileDataFetching):

1234567891011121314151617181920212223242526272829303132333435363738publicclassGraphQLErrorAdapterimplmentsGraphQLError{privateGraphQLError error;publicGraphQLErrorAdapter(GraphQLError error){esto. error = error;}@OverridepublicMap<String,Object >getExtensions(){retorno error.getExtensions();}@OverridepublicList<SourceLocation(;getLocations(){retorno error. getLocations();}@OverridepublicErrorTypegetErrorType(){return error.getErrorType();}@OverridepublicList<Object>getPath(){return error.getPath();}@OverridepublicMap<String,Object>toSpecification(){return error. toSpecification();}@OverridepublicStringgetMessage(){return(error instanceofExceptionWhileDataFetching)?((ExceptionWhileDataFetching) error).getException().getMessage(): error.getMessage();}}

java

Así que ahora lo único que falta es redefinir el manejador de errores por defecto de GraphQL.

Puede crear una clase Spring @Configuration o usar la clase principal de la aplicación anotada con @SpringBootApplication para crear un frijol de tipo GraphQLErrorHandler para reemplazar la implementación del manejador de errores por defecto:

123456789101112131415161718192021222324252627282930313233@SpringBootApplicationpublicclassDemoGraphQlApplication{publicstaticvoidmain(String[] args){SpringApplication.run(DemoGraphQlApplication. class, args);}@BeanpublicGraphQLErrorHandlererrorHandler(){returnnewGraphQLErrorHandler(){@OverridepublicList<GraphQLError>processErrors(List<GraphQLError> errors){List<GraphQLError> clientErrors = errors.stream(). filter(this::isClientError).collect(Collectors.toList());List<GraphQLError> serverErrors = errors.stream().filter(e ->!isClientError(e)).map(GraphQLErrorAdapter::new).collect(Collectors. toList());List<GraphQLError> e =newArrayList<>(); e.addAll(clientErrors); e.addAll(serverErrors);return e;}protectedbooleanisClientError(GraphQLError error){return!(error instanceofExceptionWhileDataFetching|| error instanceofThrowable);}};}}

java

Si no te importa mostrar el rastro de la pila de excepciones a tus clientes, puedes devolver la lista de errores que el método processErrors recibe como argumento.

Pero en este caso, lo hacemos. Así que primero recogemos los errores del cliente tal como están, luego los del servidor, convirtiéndolos al tipo de adaptador (GraphQLErrorAdapter), y finalmente devolvemos una lista de todos los errores recogidos.

Ya que estamos en ello, declaremos también como frijoles de primavera a los resolutores de los tipos Libro, Consulta y Mutación:

12345678910111213141516171819@SpringBootApplicationpublicclassDemoGraphQlApplication{/// ... @BeanpublicBookResolverauthorResolver(AuthorRepository authorRepository){returnnewBookResolver(authorRepository);}@BeanpublicQuery(AuthorRepository authorRepository,BookRepository bookRepository){returnnewQuery(authorRepository, bookRepository); }@BeanpublicMutationmutation(AuthorRepository authorRepository,BookRepository bookRepository){returnnewMutation(authorRepository, bookRepository);}}

java

Y si quieres, con un frijol CommandLineRunner, puedes insertar algunos datos en la base de datos:

1234567891011121314@SpringBootApplicationpublicclassDemoGraphQlApplication{// ...@BeanpublicCommandLineRunnerdemo(AuthorRepository authorRepository,BookRepository bookRepository){return(args)- &gtAuthor author =newAuthor("Herbert", "Schildt"); authorRepository. save(autor); bookRepository.save(newBook("Java: Una guía para principiantes, sexta edición", "0071809252",728, autor));};}}

java

Y eso es todo. Probemos la aplicación.