Skip to content
etienne-sf edited this page Jan 31, 2022 · 63 revisions

GraphQL Maven 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.

In server mode, the graphql-maven-plugin reads a graphqls schema, and generated the maximum of boilerplate code. That is, it generates:

  • When in a jar maven project, the main class to start a Spring Boot application
  • When in a war maven project, the servlet configuration to be embedded in a war package. It can then be deployed in any standard application server
  • 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.1.3.RELEASE</version>
	</parent>

	<groupId>com.graphql-java</groupId>
	<artifactId>mytest-of-graphql-maven-plugin</artifactId>
	<version>0.1.0-SNAPSHOT</version>

	<build>
		<plugins>
			<plugin>
				<groupId>com.graphql-java</groupId>
				<artifactId>graphql-maven-plugin</artifactId>
				<version>1.18.1</version>
				<executions>
					<execution>
						<goals>
							<goal>graphql</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<packageName>org.my.package</packageName>
					<mode>server</mode>
				</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 tests -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-api</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- Dependencies for GraphQL -->
		<dependency>
			<groupId>com.graphql-java-kickstart</groupId>
			<artifactId>graphql-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>com.graphql-java-kickstart</groupId>
			<artifactId>graphql-java-tools</artifactId>
		</dependency>
		<dependency>
			<!-- gives a GUI to test the GraphQL request on the generated server: http://localhost:8080/graphiql -->
			<groupId>com.graphql-java-kickstart</groupId>
			<artifactId>graphiql-spring-boot-starter</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- Other dependencies -->
		<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>
		<dependency>
			<groupId>org.dbunit</groupId>
			<artifactId>dbunit</artifactId>
		</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());
	}

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

	@Override
	public List<Topic> batchLoader(List<UUID> keys) {
		return topicRepository.findByIds(keys);
	}
}
Clone this wiki locally