This Tutorial describes how-to create a GraphQL client application, with the graphql-maven-plugin and the graphql Gradle plugin.
The GraphQL plugin helps both on the server and on the client side. You'll find the tutorials for the server side on the Maven server tutorial and on the Gradle server tutorial
This plugin allows a schema first approach.
This approach is the best approach for APIs: it allows to precisely control the Interface Contract. This contract is the heart of all connected systems.
This tutorial won't describe how to create a GraphQL schema. There are plenty of resources on the net for that, starting with the official GraphQL site.
This sample is based on the Forum schema, available here
This schema contains:
- A custom scalar definition: Date.
- This allows to define new type to define objet's field. We'll have to provide it's implementation to read and write Date fields.
- A schema object. This declaration is optional. It allows to define query/mutation/subscription specific names. This schema declares:
- QueryType as a query.
- MutationType as a mutation
- SubscriptionType as a subscription
- These types are declared below, as any regular object. Their definition is that of standard Object, but their meaning is very different. These fields are respectively the queries, mutations and subscriptions that you can execute, as a client GraphQL schema that connects to a GraphQL server that implements this schema.
- Four regular GraphQL objects: Member, Board, Topic, Post
- These are the objects defined in the Object model that can queried (with queries or subscriptions), or inserted/updated (with mutations)
- One enumeration: MemberType
- Enumeration are a kind of scalar, that allows only a defined list of values. MemberType is one of ADMIN, MODERATOR or STANDARD.
- Three input types: TopicPostInput, TopicInput and PostInput
- These are objects that are not in the Object model. They may not be returned by queries, mutations or subscriptions. As their name means, they can only be used as field parameters. And regular objects maynot be use as field parameters.
This schema is stored in the /src/main/resources/ project folder for convenience.
It could be also be used in another folder, like /src/main/graphql/ . In this case, the schema is not stored in the packaged jar (which is Ok for the Client mode), and you have to use the plugin schemaFileFolder parameter, to indicate where to find this schema.
As a Maven or a Gradle plugin, you have to add the plugin in the build:
- For Maven, you add it in the build section of your pom (here is the full pom):
- For Gradle, you declare the plugin, then configure it (here is the full build.gradle)
Let's first have a look at the Maven pom.xml file:
<build>
...
<plugin>
<groupId>com.graphql-java-generator</groupId>
<artifactId>graphql-maven-plugin</artifactId>
<version>${graphql-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>generateClientCode</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>org.forum.client</packageName>
<customScalars>
<customScalar>
<graphQLTypeName>Date</graphQLTypeName>
<javaType>java.util.Date</javaType>
<graphQLScalarTypeStaticField>com.graphql_java_generator.customscalars.GraphQLScalarTypeDate.Date</graphQLScalarTypeStaticField>
</customScalar>
</customScalars>
<!-- The parameters below change the 1.x default behavior to respect the future 2.x behavior -->
<copyRuntimeSources>false</copyRuntimeSources>
<generateDeprecatedRequestResponse>false</generateDeprecatedRequestResponse>
<separateUtilityClasses>true</separateUtilityClasses>
</configuration>
</plugin>
...
</plugins>
</build>
<dependencies>
<!-- Dependencies for GraphQL -->
<dependency>
<groupId>com.graphql-java-generator</groupId>
<artifactId>graphql-java-runtime</artifactId>
<version>${graphql-maven-plugin.version}</version>
</dependency>
</dependencies>
Then the Gradle build.properties and build.gradle files:
Define once the plugin version in the build.properties file:
graphQLpluginVersion = 1.14.1
plugins {
id "com.graphql_java_generator.graphql-gradle-plugin" version "${graphQLpluginVersion}"
id 'java'
}
repositories {
jcenter()
mavenCentral()
}
dependencies {
// THE VERSION MUST BE THE SAME AS THE PLUGIN's ONE
implementation "com.graphql-java-generator:graphql-java-client-dependencies:${graphQLpluginVersion}"
}
// The line below makes the GraphQL plugin be executed before Java compiles, so that all sources are generated on time
compileJava.dependsOn generateClientCode
// The line below adds the generated sources as a java source folder in the IDE
sourceSets.main.java.srcDirs += '/build/generated/resources/graphqlGradlePlugin'
sourceSets.main.java.srcDirs += '/build/generated/sources/graphqlGradlePlugin'
// Let's configure the GraphQL Gradle Plugin:
// All available parameters are described here:
// https://graphql-maven-plugin-project.graphql-java-generator.com/graphql-maven-plugin/generateClientCode-mojo.html
generateClientCodeConf {
packageName = 'org.forum.client'
customScalars = [ [
graphQLTypeName: "Date",
javaType: "java.util.Date",
graphQLScalarTypeStaticField: "com.graphql_java_generator.customscalars.GraphQLScalarTypeDate.Date"
] ]
// The parameters below change the 1.x default behavior. They are set to respect the default behavior of the future 2.x versions
generateDeprecatedRequestResponse = false
separateUtilityClasses = true
}
The java version must be set to version 1.8 (or higher).
In this plugin declaration:
- (for Maven only) The plugin execution is mapped to its generateClientCode goal
- The plugin generates the GraphQL code in the packageName package (or in the com.generated.graphql if this parameter is not defined)
- The separateUtilityClasses set true allows this separation:
- All the classes generated directly from the GraphQL schema (object, enum, interfaces, input types...) are generated in packageName.
- All the utility classes are generated in the sub-package util
- This insures to have no collision between the GraphQL code and the GraphQL plugin's utility classes
- If you set it to false, or don't define it, then all classes are generated in the packageName package
- And we declare the Date scalar implementation.
- It is mandatory to give the implementation for each custom scalar defined in the GraphQL schema.
- You'll find the relevant documentation on the Plugin custom scalar doc page
The generated source is added to the IDE sources, thanks to:
- (for Maven) The build-helper-maven-plugin, so that the generated source is automatically added to the build path of your IDE.
- (for Gradle) The sourceSets.main.java.srcDirs += ... line
The graphql-java-runtime dependency add all necessary dependencies, for the generated code. Of course, its version must be the same as the plugin's version.
Don't forget to execute (or re-execute) a full build when you change the plugin configuration, to renegerate the proper code:
- (For Maven) Execute mvn clean compile
- (for Gradle) Execute gradlew clean build
This will generate the client code in the packageName package (or in the com.generated.graphql if this parameter is not defined)
The code is generated in the :
- (for Maven) /target/generated-sources/graphql-maven-plugin folder. And thanks to the build-helper-maven-plugin, it should automatically be added as a source folder to your favorite IDE.
- (for Gradle) /build/generated/sources/graphqlGradlePlugin folder. And thanks to the sourceSets.main.java.srcDirs += ... line in the build.gradle file, it should automatically be added as a source folder to your favorite IDE.
Let's take a look at the generated code:
- The org.forum.client package contains all classes that maps to the GraphQL schema:
- The classes starting by '__' (two underscores) are the GraphQL introspection classes. These are standard GraphQL types.
- All other classes are directly the items defined in the forum GraphQL schema, with their fields, getter and setter. All fields are annotated with the GraphQL information necessary on runtime, and the JSON annotations to allow the deserialization of the server response.
- The org.forum.client.util package contains:
- CustomScalarXxx classes are utility classes for custom scalars: a registry, and one JSON deserializer for each custom scalar defined in the GraphQL schema
- GraphQLRequest : its a base element to execute GraphQL request (query, mutation or subscription). See below for the details.
- QueryTypeExecutor , MutationTypeExecutor and SubscriptionTypeExecutor allow to execute the queries, mutations and subscriptions defined in the schema
- QueryType , MutationType and SubscriptionType are deprecated and will be removed in 2.0 version
- XxxResponse are deprecated class, that exist only for backward compatibility.
- XxxRootResponse are the target for deserialization when executing a query or a mutation.
- The com.graphql_java_generator and its subpackages is the plugin's runtime. It's added to your project, so that your project has no dependency from graphql-java-generator.
- You can also set the copyRuntimeSources plugin parameter to false, and add the com.graphql-java-generator:graphql-java-runtime dependency, with the exact same version as the plugin version.
To sum up, you'll use:
- The GraphQLRequest to store a prepared request
- The QueryType , MutationType and SubscriptionType classes to prepare and execute GraphQL requests
- The POJOs in the org.forum.client package to manipulate the data defined in the GraphQL schema
These are concepts proper to the plugin. You'll find more info about Full and Partial request on this plugins's doc page.
So let's explain that:
- A Partial request is the execution of only one query or mutation at a time. It's easier to use, as the generated method directly returns the POJO instance for the returned type, or throws a GraphQLRequestExecutionException when an error occurs.
- The query/mutation/subscription parameters are parameter of the generated java method
- You provide only the expected response (see below for a sample). For instance to get back from the server the id and name fields, you provide this request string: "{id name}"
- You can use directives only on the fields returned by the server (use full requests if you need others)
- You can use inline fragments (use full requests if you need named fragments)
- The execXxx methods of the query/mutation executor returns directly the result of the requests (for instance a List for boards request of the forum sample)
- The execXxx methods of the subscription returns a SubscriptionClient, that allows to latter unsubscribe from the subscription.
- A Full request is the execution of full GraphQL request. This allows to execute several requests at a time.
- Currently, the plugin doesn't manage aliases. So you can execute several different queries or mutations at at time. But to call several times the same query or mutation (with different parameters for instance), you'll need to execute several requests.
- Subscription can't be executed by a Full Request, as a callback class must be provided for each subscription.
- Full requests allows to use named fragments, and directive on the query/mutation/subscription and/or their parameters
- The execXxx methods of the query/mutation executor returns an instance of the query/mutation GraphQL object. It's up to you to call the relevant getter to retrieve the server's response
- A Prepared request is a request that is prepared at startup time, and reused at each execution.
- For internal reason, including bind parameter management, and proper deserialization of interfaces and unions, the plugin needs to parse the GraphQL request. Using Prepared request allows to parse it only once, at preparation time, so it's faster when executing the request.
- This also allows to check the GraphQL syntax when the request is prepared, typically when the application starts: the earlier you known there is an issue, the best it is to manage it.
- A Direct request is a when you provide the GraphQL query string at execution time.
- It's easier, as you don't have to call a first preparation method, then store its result, before executing the request
- It's less efficient, as the request preparation is a little overhead at each execution
- It's less secure as, as you may discover at execution time, that there is a syntax error in your query string
GraphQL queries and mutations are executed in exactly the same way.
The easiest way is to execute Partial Request. And the most effective is that this request is prepared first.
The code below executes the boards query, as defined in this extract of the GraphQL schema:
type QueryType {
boards: [Board]
[...]
}
public class GraphQLClient {
/** The logger for this class */
static protected Logger logger = LoggerFactory.getLogger(GraphQLClient.class);
QueryTypeExecutor queryExecutor;
GraphQLRequest boardsRequest;
/** This constructor prepares the GraphQL requests, so that they can be used by the {@link #exec()} method */
public GraphQLClient() throws GraphQLRequestPreparationException {
// Creation of the query executor, for this GraphQL endpoint
logger.info("Connecting to GraphQL endpoint");
queryExecutor = new QueryTypeExecutor("http://localhost:8180/graphql");
// Preparation of the GraphQL requests, that will be used in the exec method
boardsRequest = queryExecutor
.getBoardsGraphQLRequest("{id name publiclyAvailable topics {id title date nbPosts}}");
}
public void exec() throws GraphQLRequestExecutionException {
// Let's get, then display, all available boards
List<Board> boards = queryExecutor.boards(boardsRequest);
... do something with boards
}
}
And you're done:
- The GraphQLClient constructor prepares the request(s)
- The exec() method executes the query
Of course, in a real application case, you would prepare more requests
Execution of a Mutation works in exactly the same way.
The query/mutation parameters are the parameters that are defined on the field of the query or mutation, in the GraphQL schema.
Let's execute the topics query, that is defined like this:
type QueryType {
[...]
topics(boardName: String!): [Topic]!
[...]
}
This query is used this way:
public class GraphQLClient {
/** The logger for this class */
static protected Logger logger = LoggerFactory.getLogger(GraphQLClient.class);
QueryTypeExecutor queryExecutor;
GraphQLRequest allTopicsRequest;
/** This constructor prepares the GraphQL requests, so that they can be used by the {@link #exec()} method */
public GraphQLClient() throws GraphQLRequestPreparationException {
// Creation of the query executor, for this GraphQL endpoint
logger.info("Connecting to GraphQL endpoint");
queryExecutor = new QueryTypeExecutor("http://localhost:8180/graphql");
// Preparation of the GraphQL requests, that will be used in the exec method
allTopicsRequest = queryExecutor.getTopicsGraphQLRequest("{id date author {id name} nbPosts title content}");
}
public void exec() throws GraphQLRequestExecutionException {
// Let's get, then display, all topics of one of these boards
String aBoardName = "Board name 2";
List<Topic> topics = queryExecutor.topics(allTopicsRequest, aBoardName);
... do something with topics
}
}
The only change is that all the query parameters are parameter of the queryExecutor.topics(..) method. The topics(..) method take care of serializing and sending the board name parameter to the GraphQL server.
The posts field of the Topic object accepts three parameters:
type Topic {
id: ID!
date: Date!
author: Member!
publiclyAvailable: Boolean
nbPosts: Int
title: String!
content: String
posts(memberId: ID, memberName: String, since: Date!): [Post]!
}
So we can update the previous sample, by querying the topic's posts. We'll define these bind parameters:
- memberId. It's defined as optional, as it is prefixed by ?
- memberName. Also optional.
- since. This parameter is mandatory, as it is prefixed by &
You can define bind parameters as being optional or mandatory without enforcing what's optional or mandatory in the GraphQL schema :
- You can define a GraphQL optional parameter as mandatory in your use case. Just prefi the bind parameter by a &
- Of course, GraphQL mandatory parameter should be mandatory bind parameters
Here is the code:
public class GraphQLClient {
/** The logger for this class */
static protected Logger logger = LoggerFactory.getLogger(GraphQLClient.class);
QueryTypeExecutor queryExecutor;
GraphQLRequest topicsSinceRequest;
/** This constructor prepares the GraphQL requests, so that they can be used by the {@link #exec()} method */
public GraphQLClient() throws GraphQLRequestPreparationException {
// Creation of the query executor, for this GraphQL endpoint
logger.info("Connecting to GraphQL endpoint");
queryExecutor = new QueryTypeExecutor("http://localhost:8180/graphql");
// Preparation of the GraphQL requests, that will be used in the exec method
topicsSinceRequest = queryExecutor.getTopicsGraphQLRequest(""//
+ "{" //
+ " id date author {id name} nbPosts title content "//
+ " posts(memberId: ?memberId, memberName: ?memberName, since: &since) {id date title}"//
+ "}");
}
public void exec() throws GraphQLRequestExecutionException {
java.util.Date sinceParam = new GregorianCalendar(2018, 3 - 1, 2).getTime();
List<Topic> topicsSince = queryExecutor.topics(topicsSinceRequest, //
"Board name 2", // This the query parameter. Depending on the GraphQL schema, there could be others
"memberId", "00000000-0000-0000-0000-000000000002", //
// No value is given for the optional memberName parameter
"since", sinceParam);
... do something with topicsSince
}
}
As there is no provided value for the memberName bind parameter, this parameter is not sent to the server. It's correct as this parameter is optional in both the bind parameter definition in the query (it starts by a ? ) and the GraphQL schema.
Please note that:
- If a bind parameter is set for a GraphQL array/list, you'll have to provide a java.util.List instance, where YourObject is the type defined in the GraphQL schema.
- The since parameter is a custom scalar of type Date . In the pom.xml or the build.gradle file, the custom scalar is declared as being a java.util.Date so the value in your code is a standard java.util.Date. The custom scalar implementation provided in the pom.xml or the build.gradle file takes care of properly format the code (when executing the request) and read the value (when reading the server response). More information on that in the custom scalar plugin's doc page.
The above samples are all Partial requests.
The GraphQL Maven and Gradle plugin also manage Full requests (only for query and mutation, not for subscription).
A full request allows to:
- Execute several queries into one call toward the server
- Add directives to the query/mutation itself
- Use GraphQL global fragments into your query (inline fragment are usable with partial requests as well)
The main difference between Partial and Full requests, is that the method that executes a full request returns an instance of the QueryType or MutationType, as defined the query or mutation is defined in the GraphQL schema. This means:
- That you can have the response for several queries or mutations in one call
- As alias are not managed yet, you can execute several different queries or several different mutations in a call. But you can not execute several times the same query or mutation in one call.
- You need to call the relevant getter to retrieve the result for each query or mutation that you have executed
Here is a sample:
public class GraphQLClient {
/** The logger for this class */
static protected Logger logger = LoggerFactory.getLogger(GraphQLClient.class);
MutationTypeExecutor mutationExecutor;
GraphQLRequest boardsFullRequest;
/**
* This constructor prepares the GraphQL requests, so that they can be used by the {@link #execPartialRequests()}
* method
*/
public GraphQLClient() throws GraphQLRequestPreparationException {
// Creation of the query executor, for this GraphQL endpoint
logger.info("Connecting to GraphQL endpoint");
mutationExecutor = new MutationTypeExecutor("http://localhost:8180/graphql");
// Preparation of the GraphQL Full requests, that will be used in the execFullRequests() method
boardsFullRequest = mutationExecutor
.getGraphQLRequest("mutation {createPost(post: &postInput) { id date author{id name} title content}}");
}
public void execFullRequests() throws GraphQLRequestExecutionException {
// Let's create a dummy postInput parameter, with builders generated for each object by the plugin
TopicPostInput topicInput = new TopicPostInput.Builder().withAuthorId("00000000-0000-0000-0000-000000000001")
.withPubliclyAvailable(true).withDate(new GregorianCalendar(2019, 4 - 1, 30).getTime())
.withTitle("a title").withContent("Some content").build();
PostInput postInput = new PostInput.Builder().withFrom(new GregorianCalendar(2018, 3 - 1, 2).getTime())
.withInput(topicInput).withTopicId("00000000-0000-0000-0000-000000000002").build();
MutationType response = mutationExecutor.exec(boardsFullRequest, "postInput", postInput);
Post createdPost = response.getCreatePost();
... Do something with createdPost
}
}
In this sample, there is one bind parameter, which is the mutation parameter. You can note that it's a GraphQL input type.
You can use fragments in your queries, mutations or subscriptions:
- Inline fragments work with both Partial and Full requests
- Named fragments work only with Full request, as you declare these fragments at the root of the requests. So, named fragments work only with queries and mutations.
- This limitation will be overcome in the future
Below is a sample of a partial direct query with an inline fragment:
List<Board> boards = queryExecutor
.boards("{id name publiclyAvailable topics {... on Topic {id title date} nbPosts}}");
... Do something with boards
Below is another sample, with a full direct query with a named fragment:
import org.forum.client.Board;
import org.forum.client.QueryType;
QueryType response = queryExecutor.exec(
"fragment topicFields on Topic {id title date} " +
"query{boards{id name publiclyAvailable topics {...topicFields nbPosts}}}");
List<Board> boards = response.getBoards();
... Do something with boards
Subscription are documented on the GraphQL plugin's web site