-
Notifications
You must be signed in to change notification settings - Fork 48
server
- GraphQL Maven and Gradle Plugin (server mode usage)
- A war or a Spring Boot application
- The exposed URL
- The pom.xml file
- The server implementation
- More information about Data Loaders and Batch Loading
This project is a maven plugin, which makes it easy to work in Java with graphQL in a schema first approach. The graphql-gradle-plugin-project contains the gradle version of this plugin. It is basically a wrapper of the plugin logic into a Gradle plugin.
In server mode, the plugin reads a graphqls schema, and generated the maximum of boilerplate code, so that you don't have to write (and maintain)it yourself. That is, it generates:
- When in a jar project, the main class to start a Spring Boot application
- When in a war project, the servlet configuration to be embedded in a war package. It can then be deployed in any standard application server
- The maven plugin detects the maven packaging, and automatically activate the war mode
- The gradle can't discover the packaging mode alone. Since 2.5, a packaging parameter has been added. It accepts the "jar" and "war" values.
- Almost all the Spring components to wire the whole stuff
- The interfaces (DataFetchersDelegate) for the classes that are specific to the application context (see below)
- The POJOs to manipulate the GraphQL objects defined in the GraphQL schema.
- These POJOs are annotated with JPA annotations. This allows to link them to almost any database
- You can customize these annotations, with the Schema Personalization file (see below for details)
- (in a near future) It will be possible to define your own code template, to generate exactly the code you want
Please note that the generated code uses dataloader to greatly improve the server's performances. See https://github.com/graphql-java/java-dataloader.
Once all this is generated, you'll have to implement the DataFetchersDelegate interfaces. The DataFetchersDelegate implementation is the only work that remains on your side. They are the link between the GraphQL schema and your data storage. See below for more details.
Depending on your use case, you can set the maven packaging to jar or war, in your pom. This changes the generated code. But your specific code is exactly the same. That is: you can change the packaging at any time, and it will still produce a ready-to-go product without any other modification from you.
Below you'll find:
- A sample pom to start with
- The explanation about the DataFetchersDelegate interfaces you'll have to implement.
The parameters for the server are stored in the application.properties file.
You can have a look at this file, in the given server samples. It's a standard spring boot configuration file, so you'll find all the needed information on the net.
If you want to expose your Spring Boot app in https, take a look at the doc on the net. All parameters will go in the application.properties. For instance, Thomas Vitale provides a doc for that.
The important parameter is: server.port (for instance server.port = 8180), which determines the app port, when running as a spring boot app, that is, when it's packaged as a jar.
The path depends on the way the GraphQL is run:
-
If packaged as a jar: the path is /graphql
-
If packaged as a war: the paths is /{WebAppContext}/graphql
Create a new Maven Project, with this pom, for instance :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
</parent>
<groupId>com.graphql-java</groupId>
<artifactId>mytest-of-graphql-maven-plugin</artifactId>
<version>0.1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- The graphql plugin's version -->
<graphql-maven-plugin.version>2.8</graphql-maven-plugin.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-maven-plugin</artifactId>
<version>${graphql-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>generateServerCode</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>org.my.package</packageName>
<!-- The parameters below change the 1.x default behavior. They are set to respect the behavior of the future 2.x versions -->
<copyRuntimeSources>false</copyRuntimeSources>
<generateBatchLoaderEnvironment>true</generateBatchLoaderEnvironment>
<separateUtilityClasses>true</separateUtilityClasses>
<skipGenerationIfSchemaHasNotChanged>true</skipGenerationIfSchemaHasNotChanged>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<extensions>
<!-- Adding these extensions prevents the error below, with JDK 9 and higher: -->
<!-- NoSuchMethodError: 'java.lang.String javax.annotation.Resource.lookup()' -->
<extension>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</extension>
<extension>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</extension>
</extensions>
</build>
<dependencies>
<!-- Dependencies for GraphQL -->
<dependency>
<groupId>com.graphql-java-generator</groupId>
<artifactId>graphql-java-server-runtime</artifactId>
<version>${graphql-maven-plugin.version}</version>
</dependency>
...
</dependencies>
</project>
Take care of the two parameters of the Maven Plugin that have been set there:
- mode: as client is the default mode, you must define the mode as server, here, to generate the server code.
- packageName: Very important. This package is where the main code is defined. The Spring container is started from this package. The implementations of the DataFetchersDelegate (see below) will be searched in this package, and in its subpackages. So this package must be a package that is the same, or that contains the packages where you define your implementations of DataFetchersDelegate
Then do a first build :
mvn clean install
The build will complain about the DataFetchersDelegate you need to define.
The short story is this one:
- The code generated by the GraphQL maven plugin directly maps to the entity, thanks to Spring Data JPA's magic.
- You still needs to implement the DataFetchersDelegate interfaces, to manage the access to your data modele (see the samples and below, to see how to do this).
A longer story is this one:
The generated code can not be automatically adapted to all and every data model that exists, and even less all combinations between local and distant data that you may have on server side. So the generated code is only the basis for what's most common to all implementations.
Then, it's up to you to map the generated POJOs to your own data model.
In usual cases, this mapping is actually declaring the Spring Data Repositories, and call them from your implementation of the calling the DataFetchersDelegate interfaces, that have been generated by the graphql-java-generator plugin.
==> You can see such an example in the forum server sample. This sample is embedded into the plugin project, and is used as an integration test.
If the GraphQL schema is really different from the data model, then you may have to implement the relevant logic to fit your data model into the GraphQL model.
==> You can see such an example in the StarWars server sample. This sample is embedded into the plugin project, and is used as an integration test.
Write your implementation for the DataFetchersDelegate:
Basically, the plugin generates one DataFetchersDelegate interfaces for each object in the GraphQL schema, whether they are regular objects or query/mutation/subscription objects.
These DataFetchersDelegate interfaces contains one method for each Data Fetcher that must be implemented, and a batchLoader method used by the DataLoader:
- For regular objects, there is one method par attribute that is another object or a list. This method will be called each time the GraphQL engine needs to read such an attribute, that is, each time it needs to go across a relationship from one object to a linked one. This method will have these parameters:
- DataFetchingEnvironment: This is the GraphQL context. It can be used to retrieve data about the request.
- (Optional) DataLoader: the data loader that will retrieve the data, asynchronously, merging all loading into one database call. Big performance improvement!
- Please note that the cache for the DataLoader is managed per request.
- Source Object: The POJO that is the parent object from which this DataFetcher will fetch an attribute. Typically, you can get its id, to read the data from a join in a database. See the provided samples for more details.
- Parameters: The list of each parameters for the field to be fetched, as defined in the GraphQL schema. The parameter is null if not provided (only possible if this parameter is not mandatory)
- For query/mutation/subscription objects, there is one method for each attribute, as each attribute is actually a query, a mutation or a subscription.
- DataFetchingEnvironment: This is the GraphQL context. It can be used to retrieve data about the request.
- Parameters: The list of each parameters for this query/mutation/subscription, as defined in the GraphQL schema. The parameter is null if not provided (only possible if this parameter is not mandatory)
The methods implemented by a DataFetchersDelegate are the actual DataFetchers. They can return (also see the sample below):
- CompletableFuture<Xxx>: this is when the data can be retrieved asynchronously through a DataLoader. In this case, the DataLoader collects all the Id that must be read from the datastore. This id list is de-duplicated (an id will be loaded once), and all the remaining ids are loaded in unique call to the datastore, thanks to the DataLoader that calls the relevant batchLoader method.
- Xxx (not a CompletableFuture): the request will be executed for each object, and must return the expected Xxx data.
The DataFetchersDelegate implementation must be a Spring Bean (marked by the @Component spring framework annotation): Spring will magically discover them during the app or war startup: Spring is fantastic! :)
The only constraint you must respect is that these DataFetchersDelegate implementations are in the same package or a sub-package of the target package of the generated code. This package is:
- defined in the pom, in the package configuration item of the graphql-java-generator plugin,
- or, if you have not defined it in the page, the default package name is com.generated.graphql.
So your DataFetchersDelegate implementation class will look like the sample below. Rather simple, isn't it!
package com.graphql_java_generator.samples.forum.server.specific_code;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Resource;
import org.dataloader.DataLoader;
import org.springframework.stereotype.Component;
import com.graphql_java_generator.samples.forum.server.GraphQLUtil;
import com.graphql_java_generator.samples.forum.server.Member;
import com.graphql_java_generator.samples.forum.server.Post;
import com.graphql_java_generator.samples.forum.server.Topic;
import com.graphql_java_generator.samples.forum.server.TopicDataFetchersDelegate;
import com.graphql_java_generator.samples.forum.server.jpa.MemberRepository;
import com.graphql_java_generator.samples.forum.server.jpa.PostRepository;
import com.graphql_java_generator.samples.forum.server.jpa.TopicRepository;
import graphql.schema.DataFetchingEnvironment;
@Component
public class DataFetchersDelegateTopicImpl implements DataFetchersDelegateTopic {
@Resource
MemberRepository memberRepository;
@Resource
PostRepository postRepository;
@Resource
TopicRepository topicRepository;
@Resource
GraphQLUtil graphQLUtil;
@Override
public CompletableFuture<Member> author(DataFetchingEnvironment dataFetchingEnvironment,
DataLoader<UUID, Member> dataLoader, Topic source) {
return dataLoader.load(source.getAuthorId());
// Use the dataLoader.loadMany for CompletableFuture<List<Xxx>> (that is: if the GraphQL field is a list)
}
@Override
public List<Post> posts(DataFetchingEnvironment dataFetchingEnvironment, Topic source, String since) {
if (since == null)
return graphQLUtil.iterableToList(postRepository.findByTopicId(source.getId()));
else
return graphQLUtil.iterableToList(postRepository.findByTopicIdAndSince(source.getId(), since));
}
// Since 1.18.4, two methods are available for batch loading: batchLoader and unorderedReturnBatchLoader
// The unorderedReturnBatchLoader is generally easier to implement
// (see below for more details)
// The javadoc in the generated code for the DataFetchersDelegate interface gives all the needed information
//
// For version until 1.18.3, only the batchLoader is available. This method MUST sort the returned list in the
// same order as the given keys list (and contain null values, for keys that don't match a value)
@Override
public List<Topic> unorderedReturnBatchLoader(List<UUID> keys) {
return topicRepository.findByIds(keys);
}
}
The use of DataLoader is a very important optimization, on server side. You'll find the details in the java dataloader github page and in the graphql-java site.
Based on this, the DataFetchersDelegate implementations will:
- In each method that returns a
CompletableFuture
: add the keys to be loaded, into the Data Loader (see the sample, here above) - Implement one of
batchLoader
orunorderedReturnBatchLoader
methods
These methods use DataLoader to minimize the number of requests to execute against the data storage backend. The DataLoader that will group and execute the asynchronous read request is provided as a method parameter.
So all this subject is only about GraphQL fields whose type are objects with an id:
- Scalar fields are managed directly
- Fields whose type is a GraphQL object that don't have an idea can't be loaded through a DataLoader
Until release 1.18.3, these DataLoader and batch loader methods are generated only for GraphQL field that are not lists.
Starting from release 1.18.4, these methods can be generated (or not), dependending on:
- The
generateDataLoaderForLists
plugin parameter. The default value is false.- If false: the data loaders are used only for fields that don't return a list. That is: the behavior is the same as previous releases. In other words, for fields which type is a sub-object with an id, two methods are generated in the DataFetchersDelegate: one which returns a {@link CompletableFuture}, and one which returns a none {@link CompletableFuture} result (that is used by the generated code only if no data loader is available).
- If true: the above behavior is extended to GRaphQL fields that are lists. If the field's type is a type with an id (whether it is a list or not), then a method which returns a {@link CompletableFuture} is generated.
- The
generateDataLoaderForLists
directive can be added to a specific GraphQL field, which type is a list of GraphQL objects, which have an id. The generated code will then make use of a Data Loader for the DataFetcherDelegate that matches this field.- As this is not a standard directive, the Directive must be defined in your GraphQL schema, before being associated to a GraphQL field
DataLoader
have four methods to add ids to the data loader:
-
dataLoader.load(id)
allows to add one id to the data loader. The associated value will be batch loaded and added later to the response- See the
author
method, in the above sample
- See the
-
dataLoader.load(id, context)
does as the above method. But it associate a context to this id. This context is associated to this id, and is available when executing the batch loading, as shown below. -
dataLoader.loadMany(ids)
allows to add a list of ids to the data loader. The associated values will be batch loaded and added later to the response- See the below sample
-
dataLoader.loadMany(ids, contexts)
allows to add a list of ids to the data loader. A list of contexts is provided. The provided lists must be in the same order (for instance, the third id is associated with the third context).
Here is a sample of the use of the loadMany
method:
@Override
public CompletableFuture<List<AllFieldCasesWithIdSubtype>> listWithIdSubTypes(
DataFetchingEnvironment dataFetchingEnvironment, DataLoader<UUID, AllFieldCasesWithIdSubtype> dataLoader,
Boolean uppercaseName, String textToAppendToTheForname) {
List<UUID> uuids = getIdsList();
// We store the parameter that'll allow the datafetcher to return a AllFieldCasesWithIdSubtype that respects
// what the GraphQL request expects
List<Object> keyContexts = new ArrayList<>();
KeyContext kc = new KeyContext(uppercaseName, textToAppendToTheForname);
for (int i = 0; i < uuids.size(); i += 1) {
// In this sample, each key has the same context, but they may be different
keyContexts.add(kc);
}
return dataLoader.loadMany(uuids, keyContexts);
}
For object that can be loaded by a DataLoader, the generated DataFetcherDelegate
interfaces have two methods for batch loading:
-
batchLoader
: it's the historical method. The implementation for this method MUST return the loaded items in the exact same order as the provided key list. If you override this method, it's up to you to sort the returned list, and to fill the gaps (that is: insert null values when a key has no matching value)- New since 1.18.4: a default implementation is provided, that calls the
unorderedReturnBatchLoader
method, then sort the list according to the provided keys list, and fills the gaps (insert null when a key has no matching value). - If your backend has issues returning values in the proper order AND insure you that a value for key is returned (null if there is no value matching a given key), then you'd better call the
unorderedReturnBatchLoader
method.
- New since 1.18.4: a default implementation is provided, that calls the
-
unorderedReturnBatchLoader
is the method you should override, unless you are sure that your implementation respect the above rule.- The default implementation of
batchLoader
calls this method, then makes sure that the returned list is valid:- The values are sorted in the same order as the keys list (to be sure that they are properly dispatched in the server response)
- If a key has not matching value, a null value is inserted at this key's position
- If the returned list contains a value with another key (wwich would be a bug), it will be ignored
- The default implementation of
If none of batchLoader
and unorderedReturnBatchLoader
is implemented, then a NullPointerException will be thrown at runtime, with a proper error message.
Here is a sample of the implementation of the unorderedReturnBatchLoader
method:
@Component
public class DataFetchersDelegatePostImpl implements DataFetchersDelegatePost {
// A Spring data repository that access the Post entity
@Resource
PostRepository postRepository;
@Override
public List<Post> unorderedReturnBatchLoader(List<Long> keys, BatchLoaderEnvironment env) {
return postRepository.findByIds(keys);
}
...
Some other useful methods
...
}
Creating a first app (non spring)
Connect to more than one GraphQL servers
Easily execute GraphQL requests with GraphQL Repositories
Access to an OAuth2 GraphQL server
How to personalize the client app
Howto personalize the generated code
Client migration from 1.x to 2.x
Implement an OAuth2 GraphQL server
Howto personalize the generated code
Server migration from 1.x to 2.x