Recently, I realized I am an old person:
but I can even add more to it: Until 2021, I was able to make my way around GraphQL almost everywhere except for one tiny thing I made for our family site.
I mean, I got the idea of GraphQL, but it never clicked with me. I totally love declarative query languages, such as SQL and Cypher. GraphQL never felt like this to me, despite it’s claim during time of writing “GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API”. I perceive it more like schema declaration that happens to be usable as query language too, which is at least aligned with GraphQLs own description:
Anyway, just because I didn’t click with it doesn’t mean it’s not fulfilling a need other people do have.
I happen to work at a company that creates a world famous Graphdatabase named “Neo4j”. Now, “Graph” is about 71% of GraphQL and the suspicion is quite close that GraphQL is a query lange for a Graph database. It isn’t, at least not native to Neo4j. A while back I needed to explain this to several people when I was approached by our friends at VMWare discussing their new module spring-graphql and saw more than one surprised face.
Now, what does Neo4j have? It has first and foremost, Cypher. Which is a great. But it has also neo4j/graphql.
Neo4j GraphQL
Neo4j’s officially supported product is neo4j/graphql.
The Neo4j GraphQL Library, released in April this year, builds on top of Cypher. My colleagues Darrell and Daniel have blogged a lot about it (here and here) and of course there are great talks.
Technically, the Neo4j GraphQL library is a JavaScript library, usable in a Node environment, together with something like the Apollo GraphQL server. Under the hood, the library and it’s associated object mapper translates the GraphQL scheme into Cypher queries.
Architecturally you setup a native Graph database (Neo4j) together with a middleware that you can either access directly or via other applications. This is as far as I can dive into Neo4j GraphQL. I wholeheartedly recommend following Oskar Hane, Dan Starns and team for great content about it.
Neo4j GraphQL satisfies the providers of APIs in the JavaScript space. For client applications (regardless of actual clients or other server site applications), the runtime for that Api doesn’t matter. But what about old people like me, doing Java in the backend?
Neo4j, GraphQL and Java
Actually, there are a ton of options. Let me walk you through:
neo4j-graphql-java
This is the one that comes most closely to what neo4j-graphl does: It takes in a schema definition and builds the translation to Cypher for you. Your job is to provide the runtime around it. You’ll find it here: neo4j-graphql/neo4j-graphql-java.
This library parses a GraphQL schema and uses the information of the annotated schema to translate GraphQL queries and parameters into Cypher queries and parameters.
Those Cypher queries can then executed, via the Neo4j-Java-Driver against the graph database and the results can be returned directly to the caller.
The library does not make assumptions about the runtime and the JVM language here. You are free to run it in Kotlin with KTor or Java with… Actually whatever server that is able to return JSON-ish structure.
As of now (July 2021), the schema augmented by neo4j-graphql-java differs from the one augmented by neo4j/graphql, but according to the readme, work is underway to support the same thing.
What I do like a lot about the library: It uses the Cypher-DSL for building the underlying Cypher queries. Why? We – Gerrit and me – always hoped that it would prove useful to someone else outside our own object mapping.
How to use it? I created a small gist Neo4jGraphqlJava that uses Javalin as a server and JBang to run. Assuming you have JBang installed, just run:
jbang https://gist.github.com/michael-simons/f8a8a122d1066f61b2ee8cd82b6401b8 -uneo4j -psecret -abolt://localhost:7687 |
jbang https://gist.github.com/michael-simons/f8a8a122d1066f61b2ee8cd82b6401b8 -uneo4j -psecret -abolt://localhost:7687
and point it to your Neo4j instance of choice. It comes with a predefined GraphQL scheme:
type Person {
name: ID!
born: Int
actedIn: [Movie] @relation(name: "ACTED_IN")
}
type Movie {
title: ID!
released: Int
tagline: String
}
type Query {
person: [Person]
} |
type Person {
name: ID!
born: Int
actedIn: [Movie] @relation(name: "ACTED_IN")
}
type Movie {
title: ID!
released: Int
tagline: String
}
type Query {
person: [Person]
}
but in the example gist, you can use --schema
to point it to another file. The whole translation happens in a couple of lines
var graphql = new Translator(SchemaBuilder.buildSchema(getSchema()));
var yourGraphQLQuery = "// Something something query";
graphql.translate(yourGraphQLQuery).stream().findFirst().ifPresent(cypher -> {
// Do something with the generated Cypher
}); |
var graphql = new Translator(SchemaBuilder.buildSchema(getSchema()));
var yourGraphQLQuery = "// Something something query";
graphql.translate(yourGraphQLQuery).stream().findFirst().ifPresent(cypher -> {
// Do something with the generated Cypher
});
Again, find the whole and executable program here.
The benefit: It’s all driven by the GraphQL scheme. I get it, that there are many people out there finding this whole query language intimidating and prefer using GraphQL for this whole area. And I don’t mean this in any way or form ironical or pejorative.
To put something like this into production not much more is needed: Of course, authentication is a pretty good idea, and maybe restricting GraphQL query complexity (while nobody would write a super deep nested query by hand with GraphQL, it’s easy enough to generate one).
Some ideas could be found here and here.
Using Spring Data Neo4j as a backend for GraphQL
Wait what? Isn’t the “old” enterprise stuff made obsolete by GraphQL? Not if you ask me. To the contrary, I think in many situations these things can benefit from each other. Why else do you think that VMWare created this?
When I started playing around with Spring Data Neo4j as a backend for a GraphQL scheme, VMWares implementation wasn’t quite there yet and so I went with Netflix DGS and the result can be found here michael-simons/neo4j-aura-sdn-graphql. It has “Aura” in the name as it demonstrates in addition Spring Data Neo4js compatibility with Aura, the hosted Neo4j offering.
Netflix DGS – as well as spring-graphql – are schema first design, so the project contains a GraphQL schema too.
Schema-First vs Object-First
As the nice people at VMWare wrote: “GraphQL provides a schema language that helps clients to create valid requests, enables the GraphiQL UI editor, promotes a common vocabulary across teams, and so on. It also brings up the age old schema vs object-first development dilemma.”
I wouldn’t speak of a dilemma, but of a choice. Both are valid choices, and sometimes one fits better than the other.
Both Netflix-DGS and spring-graphl will set you up with infrastructure based on graphql-java/graphql-java and your task is to bring it to live.
My example looks like this:
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.SelectedField;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.neo4j.tips.sdn.graphql.movies.MovieService;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.DgsQuery;
import com.netflix.graphql.dgs.InputArgument;
@DgsComponent
public class GraphQLApi {
private final MovieService movieService;
public GraphQLApi(final MovieService movieService) {
this.movieService = movieService;
}
@DgsQuery
public List<?> people(@InputArgument String nameFilter, DataFetchingEnvironment dfe) {
return movieService.findPeople(nameFilter, withFieldsFrom(dfe));
}
@DgsData(parentType = "Person", field = "shortBio")
public CompletableFuture<String> shortBio(DataFetchingEnvironment dfe) {
return movieService.getShortBio(dfe.getSource());
}
private static List<String> withFieldsFrom(DataFetchingEnvironment dfe) {
return dfe.getSelectionSet().getImmediateFields().stream().map(SelectedField::getName)
.sorted()
.collect(Collectors.toList());
}
} |
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.SelectedField;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.neo4j.tips.sdn.graphql.movies.MovieService;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.DgsQuery;
import com.netflix.graphql.dgs.InputArgument;
@DgsComponent
public class GraphQLApi {
private final MovieService movieService;
public GraphQLApi(final MovieService movieService) {
this.movieService = movieService;
}
@DgsQuery
public List<?> people(@InputArgument String nameFilter, DataFetchingEnvironment dfe) {
return movieService.findPeople(nameFilter, withFieldsFrom(dfe));
}
@DgsData(parentType = "Person", field = "shortBio")
public CompletableFuture<String> shortBio(DataFetchingEnvironment dfe) {
return movieService.getShortBio(dfe.getSource());
}
private static List<String> withFieldsFrom(DataFetchingEnvironment dfe) {
return dfe.getSelectionSet().getImmediateFields().stream().map(SelectedField::getName)
.sorted()
.collect(Collectors.toList());
}
}
This is an excerpt from GraphQLApi.java. The MovieService being called in the example is of course backed by a Spring Data Neo4j repository.
The whole application is running here: neo4j-aura-sdn-graphql.herokuapp.com.
I like the flexibility the federated approach brings: When someone queries your API and asks for a short biography of a person, the service goes to Wikipedia and fetches it, transparently returning it via the GraphQL response.
Of course, this requires a ton more knowledge of Java.
However, if I wanted to do something similar in a NodeJS / Apollo environment, I both think that this is absolutely possible and that I have to acquire knowledge too.
Using SmallRye GraphQL with Quarkus and custom Cypher queries
SmallRye GraphQL is an implementation of Eclipse MicroProfile GraphQL and GraphQL over HTTP. It’s the guided option when you want to do GraphQL with Quarkus.
My experiments on that topic are presented here: michael-simons/neo4j-aura-quarkus-graphql with a running instance at Heroku too: neo4j-aura-quarkus-graphql.herokuapp.com. I liked that approach so much I even did a front end for it.
Which approach exactly? Deriving the GraphQL from Java classes. BooksAndMovies.java shows how:
@GraphQLApi
@ApplicationScoped
public class BooksAndMovies {
private final Context context;
private final PeopleService peopleService;
@Inject
public BooksAndMovies(Context context, PeopleService peopleService,
) {
this.context = context;
this.peopleService = peopleService;
}
@Query("people")
public CompletableFuture<List<Person>> getPeople(@Name("nameFilter") String nameFilter) {
var env = context.unwrap(DataFetchingEnvironment.class);
return peopleService.findPeople(nameFilter, null, env.getSelectionSet());
}
} |
@GraphQLApi
@ApplicationScoped
public class BooksAndMovies {
private final Context context;
private final PeopleService peopleService;
@Inject
public BooksAndMovies(Context context, PeopleService peopleService,
) {
this.context = context;
this.peopleService = peopleService;
}
@Query("people")
public CompletableFuture<List<Person>> getPeople(@Name("nameFilter") String nameFilter) {
var env = context.unwrap(DataFetchingEnvironment.class);
return peopleService.findPeople(nameFilter, null, env.getSelectionSet());
}
}
and Person.java a simple PoJo or a JDK16+ record (I’m running the app as a native application and there we’re still on JDK11, so the linked class is not a record yet).
import org.eclipse.microprofile.graphql.Description;
import org.eclipse.microprofile.graphql.Id;
@Description("A person has some information about themselves and maybe played in a movie or is an author and wrote books.")
public class Person {
@Id
private String name;
private Integer born;
private List<Movie> actedIn;
private List<Book> wrote;
} |
import org.eclipse.microprofile.graphql.Description;
import org.eclipse.microprofile.graphql.Id;
@Description("A person has some information about themselves and maybe played in a movie or is an author and wrote books.")
public class Person {
@Id
private String name;
private Integer born;
private List<Movie> actedIn;
private List<Book> wrote;
}
In this example I don’t use any data mapping framework but the Cypher-DSL and the integration with the Java driver alone to query, map and return everything (in an asynchronous fashion):
public CompletableFuture<List<Person>> findPeople(String nameFilter, Movie movieFilter,
DataFetchingFieldSelectionSet selectionSet) {
var returnedExpressions = new ArrayList<Expression>();
var person = node("Person").named("p");
var match = match(person).with(person);
if (movieFilter != null) {
var movie = node("Movie").named("m");
match = match(person.relationshipTo(movie, "ACTED_IN"))
.where(movie.internalId().eq(anonParameter(movieFilter.getId())))
.with(person);
}
if (selectionSet.contains("actedIn")) {
var movie = node("Movie").named("m");
var actedIn = name("actedIn");
match = match
.optionalMatch(person.relationshipTo(movie, "ACTED_IN"))
.with(person, collect(movie).as(actedIn));
returnedExpressions.add(actedIn);
}
if (selectionSet.contains("wrote")) {
var book = node("Book").named("b");
var wrote = name("wrote");
var newVariables = new HashSet<>(returnedExpressions);
newVariables.addAll(List.of(person.getRequiredSymbolicName(), collect(book).as("wrote")));
match = match
.optionalMatch(person.relationshipTo(book, "WROTE"))
.with(newVariables.toArray(Expression[]::new));
returnedExpressions.add(wrote);
}
Stream.concat(Stream.of("name"), selectionSet.getImmediateFields().stream().map(SelectedField::getName))
.distinct()
.filter(n -> !("actedIn".equals(n) || "wrote".equals(n)))
.map(n -> person.property(n).as(n))
.forEach(returnedExpressions::add);
var statement = makeExecutable(
match
.where(Optional.ofNullable(nameFilter).map(String::trim).filter(Predicate.not(String::isBlank))
.map(v -> person.property("name").contains(anonParameter(nameFilter)))
.orElseGet(Conditions::noCondition))
.returning(returnedExpressions.toArray(Expression[]::new))
.build()
);
return executeReadStatement(statement, Person::of);
} |
public CompletableFuture<List<Person>> findPeople(String nameFilter, Movie movieFilter,
DataFetchingFieldSelectionSet selectionSet) {
var returnedExpressions = new ArrayList<Expression>();
var person = node("Person").named("p");
var match = match(person).with(person);
if (movieFilter != null) {
var movie = node("Movie").named("m");
match = match(person.relationshipTo(movie, "ACTED_IN"))
.where(movie.internalId().eq(anonParameter(movieFilter.getId())))
.with(person);
}
if (selectionSet.contains("actedIn")) {
var movie = node("Movie").named("m");
var actedIn = name("actedIn");
match = match
.optionalMatch(person.relationshipTo(movie, "ACTED_IN"))
.with(person, collect(movie).as(actedIn));
returnedExpressions.add(actedIn);
}
if (selectionSet.contains("wrote")) {
var book = node("Book").named("b");
var wrote = name("wrote");
var newVariables = new HashSet<>(returnedExpressions);
newVariables.addAll(List.of(person.getRequiredSymbolicName(), collect(book).as("wrote")));
match = match
.optionalMatch(person.relationshipTo(book, "WROTE"))
.with(newVariables.toArray(Expression[]::new));
returnedExpressions.add(wrote);
}
Stream.concat(Stream.of("name"), selectionSet.getImmediateFields().stream().map(SelectedField::getName))
.distinct()
.filter(n -> !("actedIn".equals(n) || "wrote".equals(n)))
.map(n -> person.property(n).as(n))
.forEach(returnedExpressions::add);
var statement = makeExecutable(
match
.where(Optional.ofNullable(nameFilter).map(String::trim).filter(Predicate.not(String::isBlank))
.map(v -> person.property("name").contains(anonParameter(nameFilter)))
.orElseGet(Conditions::noCondition))
.returning(returnedExpressions.toArray(Expression[]::new))
.build()
);
return executeReadStatement(statement, Person::of);
}
The Cypher-DSL really shines in building queries in an iterative way. The whole PeopleService is here.
Restricting query complexity
In anything that generates fetchers via GraphQL-Java you should consider using an instance of graphql.analysis.MaxQueryComplexityInstrumentation
.
In a Spring application you would do this like
@Configuration
public class MyConfig {
@Bean
public SimpleInstrumentation max() {
return new MaxQueryComplexityInstrumentation(64);
}
} |
@Configuration
public class MyConfig {
@Bean
public SimpleInstrumentation max() {
return new MaxQueryComplexityInstrumentation(64);
}
}
in Quarkus like this:
import graphql.GraphQL;
import graphql.analysis.MaxQueryComplexityInstrumentation;
import javax.enterprise.event.Observes;
// Also remember to configure
// quarkus.smallrye-graphql.events.enabled=true
public final class GraphQLConfig {
public GraphQL.Builder configureMaxAllowedQueryComplexity(@Observes GraphQL.Builder builder) {
return builder.instrumentation(new MaxQueryComplexityInstrumentation(64));
}
} |
import graphql.GraphQL;
import graphql.analysis.MaxQueryComplexityInstrumentation;
import javax.enterprise.event.Observes;
// Also remember to configure
// quarkus.smallrye-graphql.events.enabled=true
public final class GraphQLConfig {
public GraphQL.Builder configureMaxAllowedQueryComplexity(@Observes GraphQL.Builder builder) {
return builder.instrumentation(new MaxQueryComplexityInstrumentation(64));
}
}
This prevents the execution of arbitrary deep queries.
Summary
There is a plethora of options to use Neo4j as a native graph database backing your GraphQL API. If you are happy running a Node based middleware, you should definitely go with neo4j/graphql. I don’t have an example myself, but the repo has a lot.
A similar approach is feasible on the JVM with neo4j-graphql/neo4j-graphql-java. It is super flexible in regards of the actual runtime. Here is my example: Neo4jGraphqlJava.java.
Schema-first GraphQL and a more static approach with a Spring Data based repository or for that mapper, any other JVM OGM, is absolutely possible. Find my example here: michael-simons/neo4j-aura-sdn-graphql. Old school enterprise Spring based data access frameworks are not mutual exclusive to GraphQL at all.
Last but not least, Quarkus and Smallrye-GraphQL offers a beautiful Object-first approach, find my example here: michael-simons/neo4j-aura-quarkus-graphql. While I wanted to use “my” baby, the Cypher-DSL to handcraft my queries, I do think that this approach will work just excellent with Neo4j-OGM.
In the end, I am quite happy to have made up my mind a bit more about GraphQL and especially the accessibility it brings to the table. Many queries translate wonderful to Neo4j’s native Cypher.
I hope you enjoyed reading this post as much as I enjoyed writing it and the examples along it. Please make sure you visit the medium pages of my colleagues shared first here.
Happy coding and a nice summer.
The feature image on this post has been provided my Gerrit… If get the English/German pun in this, here’s one more in the same ball park 😉
Filed in Java
|