Access Coherence using GraphQL

Accessing data managed in Coherence via GraphQL has never been easier

Tim Middleton
Oracle Coherence
7 min readJan 13, 2021

--

Helidon version 2.2.0 has just been released which contains support for the MicroProfile GraphQL Specification from the Eclipse Foundation. Coherence provides seamless integration with Helidon, and with the latest Coherence CE 20.12 release it is now very easy to create GraphQL endpoints to access data in Coherence.

About GraphQL

From the MicroProfile (MP) GraphQL spec page:

GraphQL is an open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data. GraphQL interprets strings from the client, and returns data in an understandable, predictable, pre-defined manner. GraphQL is an alternative, though not necessarily a replacement for REST.

GraphQL provides three types of data operations: query, mutation and subscription. The schema is the core of GraphQL and clearly defines the operations supported by the API, including input arguments and possible responses.

Note: The first release of the spec only supports queries and mutations.

If you would like to learn more about GraphQL in general, and GraphQL support in Helidon, please check out my Helidon MicroProfile GraphQL article before you continue.

Adding a GraphQL Endpoint to the To Do List example

In this article I will build upon the To Do list example described by Aleks Seovic in his Hello, Coherence article series.

The intent of the MicroProfile GraphQL specification is to provide a code-first approach to develop GraphQL applications and defines a number of annotations that can be used to develop using this approach:

  • GraphQLApi — marker annotation that identifies a CDI Bean as a GraphQL endpoint
  • Query — applies to a public method which allows a user to ask for all or specific fields on an object or collection of objects. They must be in a class annotated with the GraphQLApi annotation
  • Mutation — applies to a public method which allows a user to create or mutate entries. They must also be in a class annotated with the GraphQLApi annotation.

The steps I will take to enable GraphQL for the To Do List application are:

  • Add in helidon-microprofile-graphql-server dependency
  • Implement a TaskApi class to expose the GraphQL queries and mutations
  • Configure MicroProfile GraphQL
  • Run the example and interact via REST and GraphiQL

The completed example is available on GitHub.

Add Required Dependencies

To use GraphQL, you must be using Helidon 2.2.0 or greater and include the following additional dependency in your POM.

<dependency>
<groupId>io.helidon.microprofile.graphql</groupId>
<artifactId>helidon-microprofile-graphql-server</artifactId>
</dependency>

Also the To Do example already includes Coherence required dependency coherence-cdi-server.

<dependency>
<groupId>${coherence.groupId}</groupId>
<artifactId>coherence-cdi-server</artifactId>
<version>${coherence.version}</version>
</dependency>

Implement a TaskApi Class

Firstly we will create a TaskApi class and annotate it with the @GraphQLApi annotation. We will also mark this CDI Bean as @ApplicationScoped.

@GraphQLApi
@ApplicationScoped
public class TaskApi
{
// TBD
@Inject
private NamedMap<String, Task> tasks;
}

Next we will inject a NamedMap into the CDI Bean. Coherence CE’s NamedMap is an extension of java.util.Map but uses Coherence’s distributed data structures. For more explanation see Aleks’ original post.

Finally we add each of the queries and mutations below. I have added some comments under each one to clarify the details.

Create a Task

@Mutation
@Description("Create a task with the given description")
public Task createTask(@Name("description") String description)
{
Objects.requireNonNull(description,
"Description must be provided");
Task task = new Task(description);
tasks.put(task.getId(), task);
return task;
}

The above mutation creates a Task based on a task description. Normally the messages of unchecked exception, NullPointerException raised by Objects.requireNonNull in this case, will not be displayed but we have overridden this in the microprofile-config.properties to show the message for this example.

The @Description annotation will be included as comments in the generated GraphQL schema.

Query Tasks

@Query
@Description("Query tasks and optionally specify only completed")
public Collection<Task> getTasks(@Name("completed")
Boolean completed)
{
Filter<Task> filter = completed == null
? Filters.always()
: Filters.equal(Task::getCompleted,
completed);

return tasks.values(filter,
Comparator.comparingLong(Task::getCreatedAt));
}

The above query will either show all the tasks, or if a completed flag is specified, only show the tasks with that completion status.

Find a Task

@Query
@Description("Find a given task using the task id")
public Task findTask(@Name("id") String id)
throws TaskNotFoundException
{
return Optional.ofNullable(tasks.get(id))
.orElseThrow(() -> new TaskNotFoundException(MESSAGE + id));
}

The above query finds a task with the given task id. When the task id is not found we raise a checked exception TaskNotFoundException. By default in MicroProfile GraphQL, when a checked exception is raised, the exception message is displayed. This can be changed via config. The exception class itself is shown below:

public class TaskNotFoundException
extends Exception
{
/**
* Create the exception.
*
*
@param message reason for the exception.
*/
public TaskNotFoundException(String message)
{
super(message);
}
}

Delete a Task

@Mutation
@Description("Delete a task and return the deleted task details")
public Task deleteTask(@Name("id") String id)
throws TaskNotFoundException
{
return Optional.ofNullable(tasks.remove(id))
.orElseThrow(() -> new TaskNotFoundException(MESSAGE + id));
}

The above mutation deletes a given task and if not found will raise the checked exception and the exception message will be sent back to the client.

Remove Completed Tasks

@Mutation
@Description("Remove all completed tasks and return the tasks left")
public Collection<Task> deleteCompletedTasks()
{
tasks.invokeAll(Filters.equal(Task::getCompleted, true),
Processors.remove(Filters.always()));
return tasks.values();
}

The above mutation removes all tasks that have been completed and returns the tasks that are left.

Update a Task

@Mutation
@Description("Update a task")
public Task updateTask(@Name("id") String id,
@Name("description") String description,
@Name("completed") Boolean completed)
throws TaskNotFoundException
{
try
{
return tasks.compute(id, (k, v) ->
{
Objects.requireNonNull(v);

if (description != null)
{
v.setDescription(description);
}
if (completed != null)
{
v.setCompleted(completed);
}
return v;
});
}
catch (Exception e)
{
throw new TaskNotFoundException(MESSAGE + id);
}
}

Finally, the above mutation updates a task and returns the updated task, or throws the TaskNotFoundException if an invalid task id was provided.

See https://github.com/coherence-community/todo-list-example/blob/master/java/server/src/main/java/com/oracle/coherence/examples/todo/server/TaskApi.java for the completed class.

Configure MicroProfile GraphQL

For this example we are going to include two additional config properties in the existing META-INF/microprofile-config.properties file. They are:

graphql.cors=Access-Control-Allow-Origin
mp.graphql.exceptionsWhiteList=java.lang.NullPointerException

The first entry allows CORS for when we run the GraphiQL UI and the second one will override the behaviour of hiding the error message from the unchecked NullPointerException.

Run the Example

Once the above changes have been made, build and run the To Do example as described here. we can access the generated GraphQL schema via issuing the following request.

$ curl http://localhost:7001/graphql/schema.graphqltype Mutation {
"Create a task with the given description"
createTask(description: String): Task
"Remove all completed tasks and return the tasks left"
deleteCompletedTasks: [Task]
"Delete a task and return the deleted task details"
deleteTask(id: String): Task
"Update a task"
updateTask(completed: Boolean, description: String,
id: String): Task
}

type Query {
"Find a given task using the task id"
findTask(id: String): Task
"Query tasks and optionally specify only completed"
tasks(completed: Boolean): [Task]
}

type Task {
completed: Boolean
createdAt: BigInteger!
"yyyy-MM-dd'T'HH:mm:ss"
createdAtDate: DateTime
description: String
id: String
}

"Custom: Built-in java.math.BigInteger"
scalar BigInteger

"Custom: An RFC-3339 compliant DateTime Scalar"
scalar DateTime

"Custom: An RFC-3339 compliant DateTime Scalar"
scalar FormattedDateTime

We can also use cURL to create a Task.

$ curl -X POST http://localhost:7001/graphql -d \
'{"query":"mutation createTask { createTask(description: \"Task Description 1\") { id description createdAt completed }}"}'
Response is a newly created task:

{"data":{"createTask":{"id":"0d4a8d","description":"Task Description 1","createdAt":1605501774877,"completed":false}}

Interacting via cURL is good enough to ensure that everything works, but a much nicer way to play with a GraphQL API is to use the GraphiQL UI in the browser.

Play with GraphiQL

GraphiQL provides a rich UI in which to execute GraphQL commands against a GraphQL endpoint. GraphiQL introspects the schema and provides validation of input as well as formatting of the results.

The code from the the sample HTML file from https://github.com/graphql/graphiql/blob/main/packages/graphiql/README.md has been included in the file at
examples/microprofile/graphql/src/main/resources/web/index.html to enable GraphiQL.

Access GraphiQL via the following URL: http://localhost:7001/graphiql.html

GraphiQL UI
GraphiQL UI

Once the UI is open, paste the following commands into the left pane and use the Play button to execute the individual queries and mutations.

# Fragment to allow shortcut to display all fields for a task
fragment task on Task {
id
description
createdAt
createdAtDate
completed
}

# Create a task
mutation createTask {
createTask(description: "Task Description 1") {
...task
}
}

# Create a task with empty description - will return error message
# Normally unchecked exceptions will not be displayed but
# We have overridden this in the microprofile-config.properties
mutation createTaskWithoutDescription {
createTask {
...task
}
}

# Find all the tasks
query findAllTasks {
tasks {
...task
}
}

# Find a task
query findTask {
findTask(id: "e07a00") {
...task
}
}

# Find completed Tasks
query findCompletedTasks {
tasks(completed: true) {
...task
}
}

# Find outstanding Tasks
query findOutstandingTasks {
tasks(completed: false) {
...task
}
}

mutation updateTask {
updateTask(id: "ad4b32" description:"New Description 2") {
...task
}
}

mutation completeTask {
updateTask(id: "b30c3d" completed:true) {
...task
}
}

# Delete a task
mutation deleteTask {
deleteTask(id: "ad4b32") {
...task
}
}

# Delete completed
mutation deleteCompleted {
deleteCompletedTasks {
...task
}
}

Running the React UI and GraphiQL Side By Side

Once you have the above running, you can open the React UI using http://localhost:7001/ and use the GraphiQL and issue the following requests and see the React UI be updated in realtime.

  1. createTask — The created task will be returned via the GraphQL UI and the task will appear in the task list within the React UI.
  2. Change the id parameter for the updateTask mutation to the newly created task and execute updateTask. The updated task description will be shown.
  3. Change the id parameter for the completeTask mutation to the updated task and execute completeTask. The completed task should be shown in the React UI.
  4. Execute findCompleted query and the completed task should be displayed.
  5. Finally, execute deleteCompleted and the completed task will be deleted, and the value returned will be and empty array indicating that no more tasks exist.

Conclusion

Hopefully you can see from the above that it is very easy to expose your Coherence data via GraphQL queries and mutations.

Watch out for more on Coherence and GraphQL examples coming soon!

--

--