Skip to content
etienne-sf edited this page Aug 28, 2024 · 63 revisions

GraphQL Maven and Gradle Plugin (server mode usage)

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.

A war or a Spring Boot application

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 exposed URL

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

The pom.xml file

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>

The server implementation

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);
	}
}

More information about Data Loaders and Batch Loading

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 or unorderedReturnBatchLoader methods

CompletableFuture 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
  • 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);
	}

Implement one of batchLoader or unorderedReturnBatchLoader methods

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.
  • 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

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
...
}
Clone this wiki locally