Site icon API Security Blog

Using Spring for GraphQL with Spring Data Neo4j

## Introduction

_This is a guest blog post by [Gerrit Meier]() from [Neo4j]() who maintain(s) the Spring Data Neo4j module._

A few weeks ago version 1.2.0 of Spring (for) GraphQL was released with a bunch of new features. This also includes even better integration with Spring Data modules. Motivated by those changes, more support in Spring Data Neo4j has been added, to give the best experience when using it in combination with Spring GraphQL. This post will guide you on creating a Spring application with data stored in Neo4j and GraphQL support. If you are only partial interested in the domain, you can happily skip the next section 😉

## Domain

For this example I have chosen to reach into the Fediverse. More concrete, to put some _Servers_ and _User_ into the focus. Why the domain was picked up for this, is now for the reader to discover in the following paragraphs.

The data itself is aligned to the properties that could be fetched from the [Mastodon API](). To keep the data set simple the data was created by hand instead of fetching _everything_. This results into an easier to inspect data set. The Cypher import statements looks like this:

**Cypher import**

CREATE (s1:Server {
uri:’mastodon.social’, title:’Mastodon’, registrations:true,
short_description:’The original server operated by the Mastodon gGmbH non-profit’})
CREATE (meistermeier:Account {id:’106403780371229004′, username:’meistermeier’, display_name:’Gerrit Meier’})
CREATE (rotnroll666:Account {id:’109258442039743198′, username:’rotnroll666′, display_name:’Michael Simons’})
CREATE
(meistermeier)-[:REGISTERED_ON]->(s1),
(rotnroll666)-[:REGISTERED_ON]->(s1)

CREATE (s2:Server {
uri:’chaos.social’, title:’chaos.social’, registrations:false,
short_description:’chaos.social – a Fediverse instance for & by the Chaos community’})
CREATE (odrotbohm:Account {id:’108194553063501090′, username:’odrotbohm’, display_name:’Oliver Drotbohm’})

CREATE
(odrotbohm)-[:REGISTERED_ON]->(s2)

CREATE
(odrotbohm)-[:FOLLOWS]->(rotnroll666),
(odrotbohm)-[:FOLLOWS]->(meistermeier),
(meistermeier)-[:FOLLOWS]->(rotnroll666),
(meistermeier)-[:FOLLOWS]->(odrotbohm),
(rotnroll666)-[:FOLLOWS]->(meistermeier),
(rotnroll666)-[:FOLLOWS]->(odrotbohm)

CREATE
(s1)-[:CONNECTED_TO]->(s2)

After running the statement, the graph forms this shape.

**Graph view of data set**

![graph data set](//images.ctfassets.net/mnrwi97vnhts/2T6pBPlwO4jNUIznjWdEh8/76fe89afd24fa58165a93c7cda375d31/graph_data_set.png)

Noticeable information is that even all users follow each others, the Mastodon servers are only connected in one direction. Users on server _chaos.social_ cannot search or explore timelines on _mastodon.social_.

_Disclaimer:_ The federation of the servers is made up with a non-bidirectional relationship for this example.

## Components

To follow along with the example shown, you should use the following minimum versions:

* Spring Boot 3.1.1 (which includes the following)
* Spring Data Neo4j 7.1.1
* Spring GraphQL 1.2.1
* Neo4j version 5

Best is to head over to and create a new project with Spring Data Neo4j and Spring GraphQL dependency. If you are a little bit lazy, you could also download the empty project from [this link]().

To follow along the example 100%, you would need to have Docker installed on your system. If you don’t have this option or don’t want to use Docker, you could use either [Neo4j Desktop]() or the plain [Neo4j Server]() artifact for local deployment, or as hosted options [Neo4j Aura]() or an [empty Neo4j Sandbox](). There will be a note later how to connect to a manual started instance. The use of the enterprise edition is not necessary and everything works with the community edition.

## First Spring for GraphQL steps

In this example the heavy lifting of configuration will be done by the Spring Boot autoconfiguration. There is no need to set up the beans manually. To find out more about what happens behind the scenes, please have a look at the [Spring for GraphQL documentation](). Later, specific sections of the documentation will be referenced.

## Entity and Spring Data Neo4j setup

First thing to do is to model the domain classes. As already seen in the import, there are only `Servers` and `Accounts`.

**Account domain class**

@Node
public class Account {

@Id String id;
String username;
@Property(“display_name”) String displayName;
@Relationship(“REGISTERED_ON”) Server server;
@Relationship(“FOLLOWS”) List following;

// constructor, etc.
}

* Given [Mastodon’s id strategy for user ids](),

it is valid to assume, that the id is (server) unique.

* Here and a few lines below in the `Server`, `@Property` is used to map the database field _display_name_ to camel-case _displayName_ in the Java entity.

**Server domain class**

@Node
public class Server {

@Id String uri;
String title;
@Property(“registrations”) Boolean registrationsAllowed;
@Property(“short_description”) String shortDescription;
@Relationship(“CONNECTED_TO”) List connectedServers;

// constructor, etc.
}

With these entity classes, a `AccountRepository` can be created.

**Account repository**

@GraphQlRepository
public interface AccountRepository extends Neo4jRepository { }

Details why this annotation is used will follow later. Here for completeness of the interface.

To connect to the Neo4j instance, the connection parameters needs to be added to the _application.properties_ file.

spring.neo4j.uri=neo4j://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=verysecret

If not happened yet, the database can be started and the Cypher statement from above run to set up the data. In a later part of this article, [Neo4j-Migrations]() will get used to be sure that the database is always in the desired state.

## Spring for GraphQL setup

Before looking into the integration features of Spring Data and Spring for GraphQL, the application will get set up with a `@Controller` stereotype annotated class. The controller will get registered by Spring for GraphQL as a `DataFetcher` for the query _accounts_.

@Controller
class AccountController {

private final AccountRepository repository;

AccountController(AccountRepository repository) {
this.repository = repository;
}

@QueryMapping
List accounts() {
return repository.findAll();
}
}

Defining a GraphQL schema, that defines not only our entities but also the query with the same name as the method in the controller (_accounts_).

type Query {
accounts: [Account]!
}
type Account {
id: ID!
username: String!
displayName: String!
server: Server!
following: [Account]
lastMessage: String!
}

type Server {
uri: ID!
title: String!
shortDescription: String!
connectedServers: [Server]
}

Also, to browse the GraphQL data in an easy way, GraphiQL should be enabled in the _application.properties_. This is a helpful tool during development time. Usually this should be disabled for production deployment.

spring.graphql.graphiql.enabled=true

## First run

If everything is set up as described above, the application can be started with `./mvnw spring-boot:run`. Browsing to will present the GraphiQL explorer.

**Querying in GraphiQL**

![graphiql](//images.ctfassets.net/mnrwi97vnhts/2KWCeki2SceR3mbQnYE8Ob/9c3566cb612f5c8d8188d58050a95f84/graphiql.png)

To verify that the `accounts` method is working, a GraphQL request is sent to the application.

**First GraphQL request**

{
accounts {
username
}
}

and the expected answer gets returned from the server.

**GraphQL response**

{
“data”: {
“accounts”: [
{
“username”: “meistermeier”
},
{
“username”: “rotnroll666”
},
{
“username”: “odrotbohm”
}
]
}
}

Of course the method in the controller can be tweaked by adding parameters for respecting arguments with `@Argument` or getting the requested fields (here _accounts.username_) to squeeze down the amount of data that gets transported over the network. In the previous example, the repository will fetch all properties for the given domain entity, including all relationships. This data will get mostly discarded to return only the _username_ to the user.

This example should give an impression of what can be done with [Annotated Controllers](). Adding the query generation and mapping capabilities of Spring Data Neo4j a (simple) GraphQL application was created.

But at this point both libraries seem to live in parallel in this application and not yet like an integration. How can SDN and Spring for GraphQL get _really_ combined?

## Spring Data Neo4j GraphQL integration

As a first step, the `accounts` method from the `AccountController` can be deleted. Restarting the application and querying it again with the request from above will still bring up the same result.

This works because Spring for GraphQL recognizes the result type (array of) `Account` from the GraphQL schema. It scans for eligible Spring Data repositories that matches the type. Those repositories have to extend the `QueryByExampleExecutor` or `QuerydslPredicateExecutor` (not part of this blog post) for the given type. In this example, the `AccountRepository` is already implicitly marked as `QueryByExampleExecutor` because it is extending the `Neo4jRespository`, that is already defining the executor. The `@GraphQlRepository` annotation makes Spring for GraphQL aware that this repository can and should be used for the queries, if possible.

Without any changes to the actual code, a second _Query field_ can be defined in the schema. This time it should filter the results by username. A username looks unique at the first glance but in the Fediverse this is only true for a given instance. Multiple instances could have the very same usernames in place. To respect this behaviour, the query should be able to return an array of `Accounts`.

The documentation about [query by example (Spring Data commons)]() provides more details about the inner workings of this mechanism.

**Updated query type**

type Query {
account(username: String!): [Account]!

Restarting the app will now present the option to add a username interactively as a parameter to the query.

**Query for an array of the same username**

{
account(username: “meistermeier”) {
username
following {
username
server {
uri
}
}
}
}

Obviously, there is only one `Account` with this username.

**Response for query by username**

{
“data”: {
“account”: [
{
“username”: “meistermeier”,
“following”: [
{
“username”: “rotnroll666”,
“server”: {
“uri”: “mastodon.social”
}
},
{
“username”: “odrotbohm”,
“server”: {
“uri”: “chaos.social”
}
}
]
}
]
}
}

Behind the scenes Spring for GraphQL adds the field as a parameter to the object that gets passed to the repositories as an example. Spring Data Neo4j then inspects the example and creates matching conditions for the Cypher query, executes it and sends the result back to Spring GraphQL for further processing to shape the result into the right response format.

**(schematic) API call flow**

![example flow](//images.ctfassets.net/mnrwi97vnhts/51rmOX116rFhpDaLfnoVjW/dd70e1b9be57c92a6ebca18ffa16c421/example_flow.png)

## Pagination

Although the example data set is not this huge, it’s often useful to have a proper functionality in place that allows to request the resulting data in chunks. Spring for GraphQL uses the [Cursor Connections specification]().

A complete schema specification with all types looks like this.

**Schema with cursor connections**

type Query {
accountScroll(username:String, first: Int, after: String, last: Int, before:String): AccountConnection
}
type AccountConnection {
edges: [AccountEdge]!
pageInfo: PageInfo!
}

type AccountEdge {
node: Account!
cursor: String!
}

type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
type Account {
id: ID!
username: String!
displayName: String!
server: Server!
following: [Account]
lastMessage: String!
}

type Server {
uri: ID!
title: String!
shortDescription: String!
connectedServers: [Server]
}

Even though I personally like to have a complete valid schema, it is possible to skip all the _Cursor Connections_ specific parts in the definition. Just the query with the `AccountConnection` definition is sufficient for Spring for GraphQL to derive and fill in the missing bits. The parameters read as following

* `first`: the amount of data to fetch if there is no default
* `after`: scroll position after the data should be fetched
* `last`: the amount of data to fetch before the `before` position
* `before`: scroll position until (exclusive) the data should be fetched

One question remains: In which order is the result set returned? A stable sort order is a _must_ in this scenario, otherwise there is no guarantee that the database returns the data in a predictable order. The repository needs also to extend the `QueryByExampleDataFetcher.QueryByExampleBuilderCustomizer` and implement the `customize` method. In there it is also possible to add the default limit for the query, in this case _1_ to show the pagination in action.

**Added sort ordering (and limit)**

@GraphQlRepository
interface AccountRepository extends Neo4jRepository,
QueryByExampleDataFetcher.QueryByExampleBuilderCustomizer
{

@Override
default QueryByExampleDataFetcher.Builder customize(QueryByExampleDataFetcher.Builder builder) {
return builder.sortBy(Sort.by(“username”))
.defaultScrollSubrange(new ScrollSubrange(ScrollPosition.offset(), 1, true));
}

}

After the application has restarted, it is now possible to call the first pagination query.

**Pagination for the first element**

{
accountScroll {
edges {
node {
username
}
}
pageInfo {
hasNextPage
endCursor
}
}
}

To get also the metadata for further interaction, some parts of the `pageInfo` got also requested.

**Result for the first element**

{
“data”: {
“accountScroll”: {
“edges”: [
{
“node”: {
“username”: “meistermeier”
}
}
],
“pageInfo”: {
“hasNextPage”: true,
“endCursor”: “T18x”
}
}
}
}

Now the `endCursor` can be used for the next interaction. Querying the application with this as the value for _after_ and with a limit of 2…

**Pagination for the last element**

{
accountScroll(after:”T18x”, first: 2) {
edges {
node {
username
}
}
pageInfo {
hasNextPage
endCursor
}
}
}

…results in the last element(s). Also, the marker that there is no next page (`hasNextPage=false`) indicates that the pagination reached the end of the data set.

**Result for the last element**

{
“data”: {
“accountScroll”: {
“edges”: [
{
“node”: {
“username”: “odrotbohm”
}
},
{
“node”: {
“username”: “rotnroll666”
}
}
],
“pageInfo”: {
“hasNextPage”: false,
“endCursor”: “T18z”
}
}
}
}

It is also possible to scroll through the data backwards by using the defined `last` and `before` parameters. Also, it is completely valid to combine this scrolling with the already known features of query by example and define a query in the GraphQL schema that also accepts fields of the `Account` as filter criteria.

**Filter with pagination**

accountScroll(username:String, first: Int, after: String, last: Int, before:String): AccountConnection

## Let’s federate

One of the big advantages in using GraphQL is the option to introduce federated data. In a nutshell this means that data stored, e.g. in the database of the application, can be enriched, like in this case, with data from a remote system / microservice / . In the end, the data will get presented via the GraphQL surface as one entity. The consumer should not need to care about that multiple systems assembled this result.

This data federation can be implemented by making use of the already defined controller.

**SchemaMapping for federated data**

@Controller
class AccountController {

@SchemaMapping
String lastMessage(Account account) {
var id = account.getId();
String serverUri = account.getServer().getUri();

WebClient webClient = WebClient.builder()
.baseUrl(“https://” + serverUri)
.build();

return webClient.get()
.uri(“/api/v1/accounts/{id}/statuses?limit=1”, id)
.exchangeToMono(clientResponse ->
clientResponse.statusCode().equals(HttpStatus.OK)
? clientResponse
.bodyToMono(String.class)
.map(AccountController::extractData)
: Mono.just(“could not retrieve last status”)
)
.block();
}

}

Adding the field `lastMessage` to the `Account` in the schema and restarting the application, gives now the option to query for the accounts with this additional information.

**Query with federated data**

{
accounts {
username
lastMessage
}
}

**Response with federated data**

{
“data”: {
“accounts”: [
{
“username”: “meistermeier”,
“lastMessage”: “@taseroth erst einmal schauen, ob auf die Aussage auch Taten folgen ;)”
},
{
“username”: “odrotbohm”,
“lastMessage”: “Some #jMoleculesp/#SpringCLI integration cooking to easily add the former[…]”
},
{
“username”: “rotnroll666”,
“lastMessage”: “Werd aber das Rad im Rückwärts-Turbo schon irgendwie vermissen.”
}
]
}
}

Looking at the controller again, it becomes clear that the retrieval of the data is quite a bottleneck right now. For every `Account` a request gets issued after another. But Spring for GraphQL helps to improve the situation of the ordered requests for each `Account` after another. The solution is to use [`@BatchMapping`]() on the _lastMessage_ field in contrast to `@SchemaMapping`.

**BatchMapping for federated data**

@Controller
public class AccountController {
@BatchMapping
public Flux lastMessage(List accounts) {
return Flux.concat(
accounts.stream().map(account -> {
var id = account.getId();
String serverUri = account.getServer().getUri();

WebClient webClient = WebClient.builder()
.baseUrl(“https://” + serverUri)
.build();

return webClient.get()
.uri(“/api/v1/accounts/{id}/statuses?limit=1”, id)
.exchangeToMono(clientResponse ->
clientResponse.statusCode().equals(HttpStatus.OK)
? clientResponse
.bodyToMono(String.class)
.map(AccountController::extractData)
: Mono.just(“could not retrieve last status”)
);
}).toList());
}

}

To improve this situation even more, it is recommended to also introduce proper caching to the result. It might not be necessary that the federated data gets fetched on every request but only refreshed after a certain period.

## Testing and test data

### Neo4j-Migrations

[Neo4j-Migrations]() is a project that applies migrations to Neo4j. To be sure that always a clean state of the data is present in the database, an initial Cypher statement is provided. It has the same content as the Cypher snippet in the beginning of this post. In fact, the content is included directly from this file.

Putting Neo4j-Migrations on the classpath by providing the Spring Boot starter, it will run all migrations from the default folder (_resources/neo4j/migrations_).

**Neo4j-Migrations dependency definition**

eu.michael-simons.neo4j
neo4j-migrations-spring-boot-starter
${neo4j-migrations.version}
test

### Testcontainers

Spring Boot 3.1 comes with a new features for [Testcontainers](). One of this feature is the automatic setting of properties without the need to define `@DynamicPropertySource`. The (to Spring Boot known) properties will get populated at test execution time after the container has started.

First the dependency definition for [Testcontainers Neo4j]() is needed in our _pom.xml_.

**Testcontainers dependency definition**

org.testcontainers
neo4j
test

org.testcontainers
junit-jupiter
test

To make use of Testcontainers Neo4j, a container definition _interface_ will be created.

**Container configuration**

interface Neo4jContainerConfiguration {

@Container
@ServiceConnection
Neo4jContainer> neo4jContainer = new Neo4jContainer(DockerImageName.parse(“neo4j:5”))
.withRandomPassword()
.withReuse(true);

}

This can then be used with the `@ImportTestContainers` annotation in the (integration) test class.

**Test annotated with `@ImportTestContainers`**

@SpringBootTest
@ImportTestcontainers(Neo4jContainerConfiguration.class)
class Neo4jGraphqlApplicationTests {

final GraphQlTester graphQlTester;

@Autowired
public Neo4jGraphqlApplicationTests(ExecutionGraphQlService graphQlService) {
this.graphQlTester = ExecutionGraphQlServiceTester.builder(graphQlService).build();
}

@Test
void resultMatchesExpectation() {
String query = “{” +
” account(username:”meistermeier”) {” +
” displayName” +
” }” +
“}”;

this.graphQlTester.document(query)
.execute()
.path(“account”)
.matchesJson(“[{“displayName”:”Gerrit Meier”}]”);

}

}

For completeness this test class also includes the `GraphQlTester` and an example how to test the application’s GraphQL API.

### Testcontainers at development time

It is also now possible to run the whole application directly from the test folder and use a Testcontainers image.

**Application start with container from test class**

@TestConfiguration(proxyBeanMethods = false)
class TestNeo4jGraphqlApplication {

public static void main(String[] args) {
SpringApplication.from(Neo4jGraphqlApplication::main)
.with(TestNeo4jGraphqlApplication.class)
.run(args);
}

@Bean
@ServiceConnection
Neo4jContainer> neo4jContainer() {
return new Neo4jContainer(“neo4j:5”).withRandomPassword();
}
}

The `@ServiceConnection` annotation also takes care that the application started from the test class knows the coordinates the container is running at (connection string, username, password…).

To start the application outside the IDE, it is now also possible to invoke `./mvnw spring-boot:test-run`. If there is only one class with a main method in the test folder, it will get started.

## Topics left out / Try it

In parallel to the `QueryByExampleExecutor`, support for `QuerydslPredicateExecutor` exists in the Spring Data Neo4j module. To make use of it, the repository needs to extend the `CrudRepository` instead of the `Neo4jRepository` and also declare it as a `QuerydslPredicateExecutor` for the given type. Adding support for scrolling/pagination would require to also add the `QuerydslDataFetcher.QuerydslBuilderCustomizer` and implement its `customize` method.

The whole infrastructure presented in this blog post is also available for the reactive stack. Basically prefixing everything with `Reactive…` (like `ReactiveQuerybyExampleExecutor`) will turn this into a reactive application.

Last but not least, the scroll mechanism used here is based on an `OffsetScrollPosition`. There is also a [`KeysetScrollPosition`]() that can be used. It makes use of the sort property/properties in combination with the defined id.

@Override
default QueryByExampleDataFetcher.Builder customize(QueryByExampleDataFetcher.Builder builder) {
return builder.sortBy(Sort.by(“username”))
.defaultScrollSubrange(new ScrollSubrange(ScrollPosition.keyset(), 1, true));
}

## Summary

It’s nice to see how convenient methods in Spring Data modules do not only provide a broader accessibility for users’ use-cases, but also get used by other Spring projects to reduce the amount of code that needs to be written. This results in less maintenance for the existing code base and helps focussing on the business problem instead of infrastructure.

This post got a little bit longer because I explicitly want to touch at least the surface on what is happening when a query gets invoked without speaking just about the _automagical_ result.

Please go ahead and explore more about what is possible and how the application behaves for different types of queries. It is next to impossible to cover every topic and feature that is available within one blog post.

Happy GraphQL coding and exploring. You can find the example project on GitHub at .Read More

Exit mobile version