© 2020 Neo4j, Inc.,
This is the Neo4j Cypher-DSL manual version 2021.1.1.
Who should read this?
This manual is written for people interested in creating Cypher queries in a typesafe way on the JVM.
The Cypher-DSL is considered EXPERIMENTAL at the time of writing.
This means you can and should use it and that we are looking actively for feedback, issues and problems.
We will try to keep breaking changes to a minimum, but we reserve the right to modify public methods, parameters
and behaviour in case we need those changes for the SDN/RX project, the origin of the Cypher-DSL.
All public classes inside org.neo4j.cypherdsl still subject to breaking changes are annotated with @API(status = EXPERIMENTAL) .
|
1. Introduction
1.1. Purpose
The Cypher-DSL has been developed with the needs of SDN/RX in mind: We wanted to avoid string concatenations in our query generation and decided do go with a builder approach, much like we find with jOOQ or in the relational module of Spring Data JDBC, but for Cypher.
What we don’t have - and don’t need for our mapping purpose - at the moment is a code generator that reads the database schema and generates static classes representing labels and relationship types. That is still up to the mapping framework (in our case SDN/RX). We however have a type safe API for Cypher that allows only generating valid Cypher constructs.
We worked closely with the OpenCypher spec here and you find a lot of these concepts in the API.
The Cypher-DSL can also be seen in the same area as the Criteria API of Spring Data Mongo.
1.2. Where to use it
The Cypher-DSL creates an Abstract Syntax Tree (AST) representing your Cypher-Statements.
An instance of a org.neo4j.cypherdsl.core.Statement
representing that AST is provided at the end of query building step.
A Renderer
is then used to create literal Java-Strings.
Those can be used in any context supporting String-based queries, for example with the Neo4j Java driver or inside embedded procedures and of course with Spring Data’s Neo4j-Client.
Parameters in the generated queries will use the $form
and as such be compatible with all current versions of Neo4j.
Users of SDN/RX respectively SDN 6 can use the generated org.neo4j.cypherdsl.core.Statement
directly with the Neo4jTemplate
or the ReactiveNeo4jTemplate
.
Both the imperative and the reactive variants allow the retrieval and counting of entities without rendering a String first,
for example through Neo4jTemplate#findAll(Statement, Class<T>)
.
1.3. Java API
Find the Java-API and a generated project info here: API and project info.
2. Getting started
2.1. Prepare dependencies
Please use a dependency management system. We recommend either Maven or Gradle.
2.2. How to use it
You use the Cypher-DSL as you would write Cypher: It allows to write down even complex Cypher queries from top to bottom in a type safe, compile time checked way.
The examples to follow are using JDK 11.
We find the var
keyword especially appealing in such a DSL as the types returned by the DSL much less important than
the further building methods they offer.
The AST parts and intermediate build steps are immutable. That is, the methods create new intermediate steps.
For example, you cannot reuse an ExposesLimit step, but have to use the returned object from its skip method.
|
An instance of a org.neo4j.cypherdsl.core.Statement
is provided at the end of every query building step.
This Statement
needs to be rendered into a string or passed to methods supporting it as input.
Please get an instance of the default renderer via org.neo4j.cypherdsl.renderer.Renderer#getDefaultRenderer()
.
The renderer provides a single method render
for rendering the AST into a string representation.
Furthermore, the Statement
will collect parameter names and if provided, parameter values.
Parameter names and values are available after the statement has been build and can for example be used directly with
Neo4j-Java-Driver.
2.2.1. Examples
The following examples are 1:1 copies of the queries you will find in the Neo4j browser after running :play movies
.
They use the following imports:
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collection;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.neo4j.cypherdsl.core.Conditions;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.SymbolicName;
import org.neo4j.cypherdsl.core.renderer.Configuration;
import org.neo4j.cypherdsl.core.renderer.Renderer;
To match and return all the movie, build your statement like this:
var m = Cypher.node("Movie").named("m"); (1)
var statement = Cypher.match(m) (2)
.returning(m)
.build(); (3)
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (m:`Movie`) RETURN m");
1 | Declare a variable storing your node labeled Movie and named m , so that you can |
2 | reuse it in both the match and the return part. |
3 | The build method becomes only available when a compilable Cypher statement can be rendered. |
Find
Match all nodes with a given set of properties:
var tom = Cypher.anyNode().named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var statement = Cypher
.match(tom).returning(tom)
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (tom {name: 'Tom Hanks'}) RETURN tom");
Limit the number of returned things and return only one attribute
var people = Cypher.node("Person").named("people");
statement = Cypher
.match(people)
.returning(people.property("name"))
.limit(10)
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (people:`Person`) RETURN people.name LIMIT 10");
Create complex conditions
var nineties = Cypher.node("Movie").named("nineties");
var released = nineties.property("released");
statement = Cypher
.match(nineties)
.where(released.gte(Cypher.literalOf(1990)).and(released.lt(Cypher.literalOf(2000))))
.returning(nineties.property("title"))
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo(
"MATCH (nineties:`Movie`) WHERE (nineties.released >= 1990 AND nineties.released < 2000) RETURN nineties.title");
Query
Build relationships
var tom = Cypher.node("Person").named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var tomHanksMovies = Cypher.anyNode("tomHanksMovies");
var statement = Cypher
.match(tom.relationshipTo(tomHanksMovies, "ACTED_IN"))
.returning(tom, tomHanksMovies)
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo(
"MATCH (tom:`Person` {name: 'Tom Hanks'})-[:`ACTED_IN`]->(tomHanksMovies) RETURN tom, tomHanksMovies");
var cloudAtlas = Cypher.anyNode("cloudAtlas").withProperties("title", Cypher.literalOf("Cloud Atlas"));
var directors = Cypher.anyNode("directors");
statement = Cypher
.match(cloudAtlas.relationshipFrom(directors, "DIRECTED"))
.returning(directors.property("name"))
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (cloudAtlas {title: 'Cloud Atlas'})<-[:`DIRECTED`]-(directors) RETURN directors.name");
tom = Cypher.node("Person").named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var movie = Cypher.anyNode("m");
var coActors = Cypher.anyNode("coActors");
var people = Cypher.node("Person").named("people");
statement = Cypher
.match(tom.relationshipTo(movie, "ACTED_IN").relationshipFrom(coActors, "ACTED_IN"))
.returning(coActors.property("name"))
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo(
"MATCH (tom:`Person` {name: 'Tom Hanks'})-[:`ACTED_IN`]->(m)<-[:`ACTED_IN`]-(coActors) RETURN coActors.name");
cloudAtlas = Cypher.node("Movie").withProperties("title", Cypher.literalOf("Cloud Atlas"));
people = Cypher.node("Person").named("people");
var relatedTo = people.relationshipBetween(cloudAtlas).named("relatedTo");
statement = Cypher
.match(relatedTo)
.returning(people.property("name"), Functions.type(relatedTo), relatedTo.getRequiredSymbolicName())
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo(
"MATCH (people:`Person`)-[relatedTo]-(:`Movie` {title: 'Cloud Atlas'}) RETURN people.name, type(relatedTo), relatedTo");
Solve
var bacon = Cypher.node("Person").named("bacon").withProperties("name", Cypher.literalOf("Kevin Bacon"));
var hollywood = Cypher.anyNode("hollywood");
var statement = Cypher
.match(bacon.relationshipBetween(hollywood).length(1, 4))
.returningDistinct(hollywood)
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (bacon:`Person` {name: 'Kevin Bacon'})-[*1..4]-(hollywood) RETURN DISTINCT hollywood");
Recommend
var tom = Cypher.node("Person").named("tom").withProperties("name", Cypher.literalOf("Tom Hanks"));
var coActors = Cypher.anyNode("coActors");
var cocoActors = Cypher.anyNode("cocoActors");
var strength = Functions.count(Cypher.asterisk()).as("Strength");
var statement = Cypher
.match(
tom.relationshipTo(Cypher.anyNode("m"), "ACTED_IN").relationshipFrom(coActors, "ACTED_IN"),
coActors.relationshipTo(Cypher.anyNode("m2"), "ACTED_IN").relationshipFrom(cocoActors, "ACTED_IN")
)
.where(
Conditions.not(tom.relationshipTo(Cypher.anyNode(), "ACTED_IN").relationshipFrom(cocoActors, "ACTED_IN")))
.and(tom.isNotEqualTo(cocoActors))
.returning(
cocoActors.property("name").as("Recommended"),
strength
).orderBy(strength.asName().descending())
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo(""
+ "MATCH "
+ "(tom:`Person` {name: 'Tom Hanks'})-[:`ACTED_IN`]->(m)<-[:`ACTED_IN`]-(coActors), "
+ "(coActors)-[:`ACTED_IN`]->(m2)<-[:`ACTED_IN`]-(cocoActors) "
+ "WHERE (NOT (tom)-[:`ACTED_IN`]->()<-[:`ACTED_IN`]-(cocoActors) AND tom <> cocoActors) "
+ "RETURN cocoActors.name AS Recommended, count(*) AS Strength ORDER BY Strength DESC");
2.2.2. More features
Retrieving parameters being defined
A placeholder for a parameter can be defined via Cypher.parameter("param")
.
This placeholder will be rendered as $param
and must be filled with the appropriate means of the environment you’re working with.
In addition, an arbitrary value can be bound to the name via ยดCypher.parameter("param", "a value")` or Cypher.parameter("param").withValue("a value")
.
NULL
is a valid value.
The Cypher-DSL will not use those values, but collect them for you.
The following example shows how to access them and how to use it:
var person = Cypher.node("Person").named("p");
var statement =
Cypher
.match(person)
.where(person.property("nickname").isEqualTo(Cypher.parameter("nickname")))
.set(
person.property("firstName").to(Cypher.parameter("firstName").withValue("Thomas")),
person.property("name").to(Cypher.parameter("name", "Anderson"))
)
.returning(person)
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (p:`Person`) WHERE p.nickname = $nickname SET p.firstName = $firstName, p.name = $name RETURN p");
Collection<String> parameterNames = statement.getParameterNames();
assertThat(parameterNames).containsExactlyInAnyOrder("nickname", "firstName", "name"); (1)
Map<String, Object> parameters = statement.getParameters();
assertThat(parameters).hasSize(2); (2)
assertThat(parameters).containsEntry("firstName", "Thomas");
assertThat(parameters).containsEntry("name", "Anderson");
1 | The names contain all placeholders, also those without a value |
2 | The parameter map contains only parameters with defined values |
If you define a parameter with conflicting values, a ConflictingParametersException
will be thrown the moment you try to retrieve the collected parameters.
Using the default renderer
A statement can render itself as well:
var statement = Cypher.returning(literalTrue().as("t")).build();
var cypher = statement.getCypher();
assertThat(cypher).isEqualTo("RETURN true AS t");
This, together with the above, makes the statement a complete accessor for a Cypher-statement and it’s parameters.
Generating formatted Cypher
The Cypher-DSL can also format the generated Cypher to some extend.
The Renderer
offers the overload Renderer getRenderer(Configuration configuration)
, taking in an instance of org.neo4j.cypherdsl.core.renderer.Configuration
.
Instances of Configuration
are threadsafe and reusable.
The class offers a couple of static convenience methods for retrieving some variants.
var n = Cypher.anyNode("n");
var a = Cypher.node("A").named("a");
var b = Cypher.node("B").named("b");
var mergeStatement = Cypher.merge(n)
.onCreate().set(n.property("prop").to(Cypher.literalOf(0)))
.merge(a.relationshipBetween(b, "T"))
.onCreate().set(a.property("name").to(Cypher.literalOf("me")))
.onMatch().set(b.property("name").to(Cypher.literalOf("you")))
.returning(a.property("prop")).build();
var renderer = Renderer.getRenderer(Configuration.prettyPrinting()); (1)
assertThat(renderer.render(mergeStatement))
.isEqualTo(
"MERGE (n)\n" +
" ON CREATE SET n.prop = 0\n" +
"MERGE (a:A)-[:T]-(b:B)\n" +
" ON CREATE SET a.name = 'me'\n" +
" ON MATCH SET b.name = 'you'\n" +
"RETURN a.prop"
); (2)
1 | Get a "pretty printing" instance of the renderer configuration and retrieve a renderer based on it |
2 | Enjoy formatted Cypher. |
Inserting raw Cypher
Cypher.raw
allows for creating arbitrary expressions from raw String literals.
Users discretion is advised:
var key = Cypher.name("key");
var cypher = Cypher.call("apoc.meta.schema")
.yield("value").with("value")
.unwind(Functions.keys(Cypher.name("value"))).as(key)
.returning(
key,
Cypher.raw("value[$E]", key).as("value") (1)
)
.build().getCypher();
assertThat(cypher).isEqualTo(
"CALL apoc.meta.schema() YIELD value WITH value UNWIND keys(value) AS key RETURN key, value[key] AS value");
1 | The Cypher-DSL doesn’t support a dynamic lookup of properties on expression at the moment.
We use a raw Cypher string with the placeholder $E which resolves to the symbolic name also passed to the raw function. |
3. Properties
Nodes and Relationships expose properties. This reflects directly in the Cypher-DSL:
var personNode = Cypher.node("Person").named("p");
var movieNode = Cypher.node("Movie");
var ratedRel = personNode.relationshipTo(movieNode, "RATED").named("r");
var statement = Cypher.match(ratedRel)
.returning(
personNode.property("name"), (1)
ratedRel.property("rating")) (2)
.build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (p:`Person`)-[r:`RATED`]->(:`Movie`) RETURN p.name, r.rating");
1 | Create a property expression from a Node |
2 | Create a property expression from a Relationship |
Both Node and Relationship should be named as in the example. The Cypher-DSL generates names if they are not named, to refer to them in the statements. Without the explicit names, the generated statement would look like this:
MATCH (geIcWNUD000:`Person`)-[TqfqBNcc001:`RATED`]->(:`Movie`) RETURN geIcWNUD000.name, TqfqBNcc001.rating
The name is of course random.
The Cypher
class exposes the property
method, too. This methods takes in one name (as symbolic name or as string literal) OR one expression
and at least one further string, referring to the name of the property.
Passing in a symbolic name would lead to a similar result like in [properties-on-nodes-and-rel], an expression can refer to the results of functions etc.:
var epochSeconds = Cypher.property(Functions.datetime(), "epochSeconds"); (1)
var statement = Cypher.returning(epochSeconds).build();
Assertions.assertThat(cypherRenderer.render(statement))
.isEqualTo("RETURN datetime().epochSeconds");
1 | Here an expression, the datetime() function is passed in and the epocheSeconds property is dereferenced. |
Nested properties are of course possible as well, either directly on nodes and relationships or via the static builder:
var node = Cypher.node("Person").named("p");
var locationPropV1 = Cypher.property(node.getRequiredSymbolicName(), "home.location", "y");
var locationPropV2 = Cypher.property("p", "home.location", "y");
var statement = Cypher.match(node)
.where(locationPropV1.gt(Cypher.literalOf(50)))
.returning(locationPropV2).build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("MATCH (p:`Person`) WHERE p.`home.location`.y > 50 RETURN p.`home.location`.y");
4. Functions
There are many more functions implemented in org.neo4j.cypherdsl.core.Functions . Not all of them are already documented here.
|
4.1. Lists
4.1.1. range()
Creates an invocation of range()
.
Given the following imports
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Functions;
var range = Functions.range(0, 10);
Gives you range(0,10)
.
The step size can be specified as well:
var range = Functions.range(0, 10, 1);
This gives you range(0,10,1)
.
Both variants of range
also take a NumberLiteral
for the start and end index and the step size.
4.1.2. Inserting a list operator
The list operator []
selects a specific value of a list or a range of values from a list.
A range can either be closed or open.
Find examples to select a value at a given index, a sublist based on a closed range and sublist based on open ranges below. While the examples use Section 4.1.1, the list operator doesn’t put any restrictions on the expression at the moment.
@Test
void valueAtExample() {
var range = Functions.range(0, 10);
var statement = Cypher.returning(Cypher.valueAt(range, 3)).build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("RETURN range(0, 10)[3]");
}
@Test
void subListUntilExample() {
var range = Functions.range(Cypher.literalOf(0), Cypher.literalOf(10));
var statement = Cypher.returning(Cypher.subListUntil(range, 3)).build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("RETURN range(0, 10)[..3]");
}
@Test
void subListFromExample() {
var range = Functions.range(0, 10, 1);
var statement = Cypher.returning(Cypher.subListFrom(range, -3)).build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("RETURN range(0, 10, 1)[-3..]");
}
@Test
void subListExample() {
var range = Functions.range(Cypher.literalOf(0), Cypher.literalOf(10), Cypher.literalOf(1));
var statement = Cypher.returning(Cypher.subList(range, 2, 4)).build();
assertThat(cypherRenderer.render(statement))
.isEqualTo("RETURN range(0, 10, 1)[2..4]");
}
4.2. Mathematical functions
The Cypher-DSL supports all mathematical functions as of Neo4j 4.2. Please find their description in the Neo4j cypher manual:
4.3. Calling arbitrary procedures and functions
Neo4j has plethora of builtin procedures and functions.
The Cypher-DSL does cover a lot of them already, and they can be called in a typesafe way on many Expression
-instances
taking in various expressions such as the results of other calls, literals or parameters.
However, there will probably always be a gap between what the Cypher-DSL includes and what the Neo4j database brings. More so, there are fantastic libraries out there like APOC. APOC has so many procedures and functions in so many categories that it is rather futile to add shims for all of them consistently.
Probably the most important aspect of all: Many Neo4j users bring their knowledge to the database themselves, in the form of their stored procedures. Those should be callable as well.
The Cypher-DSL is flexible enough to call all those procedures and functions.
4.3.1. Calling custom procedures
Procedures are called via the Cypher CALL-Clause. The CALL-Clause can appear as a StandAlone call and as an InQuery call. Both are supported by the Cypher-DSL.
Standalone procedure calls
Standalone calls are particular useful for VOID
procedures.
A VOID procedure is a procedure that does not declare any result fields and returns no result records and that has explicitly been declared as VOID
.
Let’s take the first example from here
var call = Cypher.call("db", "labels").build(); (1)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL db.labels()");
call = Cypher.call("db.labels").build(); (2)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL db.labels()");
1 | Cypher.call returns a buildable statement that can be rendered.
Cypher.call can be used with separate namespace and procedure name as shown here or |
2 | with a single argument equal to the name of the procedure. |
Of course, arguments can be specified as expressions.
Expressions can be literals as in [standalone-call-with-args], Cypher parameters (via Cypher.parameter
) that bind your input or nested calls:
var call = Cypher
.call("dbms.security.createUser")
.withArgs(Cypher.literalOf("johnsmith"), Cypher.literalOf("h6u4%kr"), Cypher.literalFalse())
.build();
assertThat(cypherRenderer.render(call))
.isEqualTo("CALL dbms.security.createUser('johnsmith', 'h6u4%kr', false)");
Last but not least, the Cypher-DSL can of course YIELD
the results from a standalone call:
var call = Cypher.call("dbms.procedures").yield("name", "signature").build(); (1)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL dbms.procedures() YIELD name, signature");
call = Cypher.call("dbms.procedures").yield(Cypher.name("name"), Cypher.name("signature")).build(); (2)
assertThat(cypherRenderer.render(call)).isEqualTo("CALL dbms.procedures() YIELD name, signature");
1 | Yielded items can be specified via stringโฆ |
2 | โฆor with symbolic names created earlier |
A standalone call can spot a WHERE
clause as well:
var name = Cypher.name("name");
var call = Cypher
.call("dbms.listConfig")
.withArgs(Cypher.literalOf("browser"))
.yield(name)
.where(name.matches("browser\\.allow.*"))
.returning(Cypher.asterisk())
.build();
assertThat(cypherRenderer.render(call)).isEqualTo(
"CALL dbms.listConfig('browser') YIELD name WHERE name =~ 'browser\\\\.allow.*' RETURN *");
In-query procedure calls
In-query calls are only possible with non-void procedures. An In-query call happens inside the flow of a normal query. The mechanics to construct those calls via the Cypher-DSL are identical to standalone calls:
var label = Cypher.name("label");
var statement = Cypher
.match(Cypher.anyNode().named("n")).with("n")
.call("db.labels").yield(label).with(label)
.returning(Functions.count(label).as("numLabels"))
.build();
assertThat(cypherRenderer.render(statement)).isEqualTo(
"MATCH (n) WITH n CALL db.labels() YIELD label WITH label RETURN count(label) AS numLabels");
A CALL
can be used after a MATCH
, a WITH
and also a WHERE
clause.
4.3.2. Use stored-procedure-calls as expressions (Calling custom functions)
All the mechanics described and shown above - how to define a custom call statement, supply it with arguments etc. - doesn’t distinguish between procedures and functions. Every stored procedure can be treated as a function - as long as the stored procedure returns a single value. It doesn’t matter if the single value returns a scalar or a list of objects. A list of objects is still a single value, in contrast to a stream of objects returned by a non-void procedure.
So the question is not how to call a stored custom function, but how to turn a call statement into an expression that can be used in any place in a query where an expression is valid.
This is where asFunction
comes in.
var p = Cypher.node("Person").named("p");
var createUuid = Cypher.call("apoc.create.uuid").asFunction(); (1)
var statement = Cypher.merge(p.withProperties(Cypher.mapOf("id", createUuid))) (2)
.set(
p.property("firstName").to(Cypher.literalOf("Michael")),
p.property("surname").to(Cypher.literalOf("Hunger"))
)
.returning(p)
.build();
assertThat(cypherRenderer.render(statement)).isEqualTo(
"MERGE (p:`Person` {id: apoc.create.uuid()}) "
+ "SET p.firstName = 'Michael', p.surname = 'Hunger' "
+ "RETURN p");
1 | First we define a call as seen earlier and turn it into an expression |
2 | This expression is than used as any other expression |
Of course, arguments to those functions can be express as well. Either as literals or expressions.
var p = Cypher.node("Person").named("p");
var createUuid = Cypher.call("apoc.create.uuid").asFunction(); (1)
var toCamelCase = Cypher.call("apoc.text.camelCase")
.withArgs(Cypher.literalOf("first name")) (2)
.asFunction();
var statement = Cypher.merge(p.withProperties(Cypher.mapOf("id",
createUuid)))
.set(p.property("surname").to(Cypher.literalOf("Simons")))
.with(p)
.call("apoc.create.setProperty").withArgs(
p.getRequiredSymbolicName(),
toCamelCase,
Cypher.parameter("nameParam") (3)
).yield("node")
.returning("node")
.build();
assertThat(cypherRenderer.render(statement)).isEqualTo(
"MERGE (p:`Person` {id: apoc.create.uuid()}) SET p.surname = 'Simons' "
+ "WITH p CALL apoc.create.setProperty(p, apoc.text.camelCase('first name'), $nameParam) "
+ "YIELD node RETURN node");
1 | Same as before |
2 | A call to APOC’s camelCase function, taking in the literal of first name . |
3 | A call to another APOC function to which a parameter is passed. You find that corresponding placeholder as $nameParam in the following assert |
4.3.3. Summary
Through Cypher.call
any procedure or function can be called iin case one of your favorite procedures is missing in org.neo4j.cypherdsl.core.Functions
.
All clauses, including YIELD
and WHERE
on procedures are supported.
All procedures can be turned into functions.
The Cypher-DSL however does not check if the procedure that is used as a function is actually eligible to do so.
If the Cypher-DSL misses an important builtin Neo4j function, please raise a ticket.
5. Building a static meta model
5.1. Concepts
5.1.1. A static meta model is optional
First let’s stress this: A static meta model is optional for the Cypher-DSL. It is completly ok to use the Cypher-DSL as shown in the examples in the "How to use it" part: All labels, types and properties can be named as you go. The Cypher-DSL will still give you type-safety in regard to the Cypher’s syntax.
This fit’s nicely with Neo4j’s capabilities: Neo4j is a schema less database. Nodes, their relationships between each other, their labels and properties can be changed as you go but all of this information can still be queried.
In a schemaless database or a database with dynamic scheme the scheme is often defined by the application. This definition takes many forms: It can be through an object mapper like Neo4j-OGM or Spring Data Neo4j 6, or maybe in form of Graph-QL schemes.
Another source maybe the information we can retrieve from the database itself via db.schema.nodeTypeProperties
and
db.schema.relTypeProperties
.
5.1.2. Building blocks
The Cypher DSL offers the following building blocks as part of the public API:
-
Two pattern elements:
-
Nodes via
Node
(and it’s default implementationNodeBase
) -
Relationships via
Relationship
(and it’s default implementationRelationshipBase
)
-
-
Property
, which is a a holder for properties
When you use Cypher-DSL like this:
var m = Cypher.node("Movie").named("m");
var statement = Cypher.match(m).returning(m).build();
m
will be a Node
instance having the label Movie
and an alias of m
. m
can be used everywhere where a pattern element
can be used according to the openCypher spec.
You don’t have to care about it’s type. That’s why we vouched for the JDK11+ local type inference here and omitted the
declaration of the type Node
: It just reads better.
5.1.3. A very simple, static model
Both NodeBase
and RelationshipBase
are meant to be extended to put your static model into something that is usable
with the Cypher-DSL.
Nodes
Start by extending NodeBase
like this:
import java.util.List;
import org.neo4j.cypherdsl.core.MapExpression;
import org.neo4j.cypherdsl.core.NodeBase;
import org.neo4j.cypherdsl.core.NodeLabel;
import org.neo4j.cypherdsl.core.Properties;
import org.neo4j.cypherdsl.core.SymbolicName;
public final class Movie extends NodeBase<Movie> { (1)
public static final Movie MOVIE = new Movie(); (2)
public Movie() {
super("Movie"); (3)
}
private Movie(SymbolicName symbolicName, List<NodeLabel> labels, Properties properties) { (4)
super(symbolicName, labels, properties);
}
@Override
public Movie named(SymbolicName newSymbolicName) { (5)
return new Movie(newSymbolicName, getLabels(), getProperties());
}
@Override
public Movie withProperties(MapExpression newProperties) { (6)
return new Movie(getSymbolicName().orElse(null), getLabels(), Properties.create(newProperties));
}
}
1 | Extend from NodeBase and specify your class as a "self" type-argument |
2 | Optional: Create one static instance of your model |
3 | This is where you specify one or more label |
4 | This constructor is optional, it is used in the next two steps |
5 | named must be overridden and must return new copies of the node, with the changed symbolic name.
It must be overridden to guarantee type integrity. |
6 | Same as obove |
With that in place, you can already use it like this:
var cypher = Cypher.match(Movie.MOVIE)
.returning(Movie.MOVIE)
.build().getCypher();
and it would generate a Cypher string like this: "MATCH (jwKyXzwS000:`Movie
) RETURN jwKyXzwS000"`, with generated variable names.
If you don’t like them, you can just rename one instance of the movie-model like this:
var movie = Movie.MOVIE.named("m");
var cypher = Cypher.match(movie)
.returning(movie)
.build().getCypher();
Of course, properties belong into a model as well. You add them like this:
import org.neo4j.cypherdsl.core.Property;
public final class Movie extends NodeBase<Movie> { (1)
public final Property TITLE = this.property("title"); (2)
}
1 | Same class before, extending from NodeBase . |
2 | Use this and the property method to create a new Property instance, stored on the given instance |
A possible usage scenario looks like this:
var movie = Movie.MOVIE.named("m");
var cypher = Cypher.match(movie)
.where(movie.TITLE.isEqualTo(Cypher.literalOf("The Matrix"))) (1)
.returning(movie)
.build().getCypher();
Assertions.assertThat(cypher)
.isEqualTo("MATCH (m:`Movie`) WHERE m.title = 'The Matrix' RETURN m");
1 | Make sure to use the renamed instance everywhere. Here: For accessing the property. Alternatively, don’t rename. |
Relationships
Relationships are a bit more complicated. Relationships of the same type can be used between nodes with different labels. We have these scenarios:
-
(s:LabelA) - (r:SomeType) → (e:LabelB)
: -
(s:LabelA) - (r:SomeType) → (e)
-
(s) - (r:SomeType) → (e)
We either have a type that is used only between the same set of labels, or a type is used always with one fixed label or a type is used between arbitrary labels.
To accomadate for that, the default relationship implementation, RelationshipBase
spots three type parameters:
public class RelationshipBase<S extends NodeBase<?>, E extends NodeBase<?>, SELF extends RelationshipBase<S, E, SELF>> {
}
S
is the type of a start node, E
of an end node and SELF
is the concrete implementation itself.
The public API of RelationshipBase
enforces a direction from start to end (LTR
, left to right).
We just have a look at the first case to make the concepts clear. We model the ACTED_IN
relationship of the movie graph.
It exists between people and movies (going from person to movie) and has an attribute roles:
import org.neo4j.cypherdsl.core.MapExpression;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Properties;
import org.neo4j.cypherdsl.core.Property;
import org.neo4j.cypherdsl.core.RelationshipBase;
import org.neo4j.cypherdsl.core.SymbolicName;
public final class ActedIn extends RelationshipBase<Person, Movie, ActedIn> { (1)
public final Property ROLE = this.property("role"); (2)
protected ActedIn(Person start, Movie end) {
super(start, "ACTED_IN", end); (3)
}
private ActedIn(SymbolicName symbolicName, Node start, String type, Properties properties, Node end) { (4)
super(symbolicName, start, type, properties, end);
}
@Override
public ActedIn named(SymbolicName newSymbolicName) { (5)
return new ActedIn(newSymbolicName, getLeft(), getRequiredType(), getDetails().getProperties(), getRight());
}
@Override
public ActedIn withProperties(MapExpression newProperties) { (6)
return new ActedIn(getSymbolicName().orElse(null), getLeft(), getRequiredType(), Properties.create(newProperties), getRight());
}
}
1 | The base class, with 3 concrete types. A Person , the Movie from [static-movie] and the type itself. |
2 | Same idea with as with nodes: Store all properties as final attributes. |
3 | There is no default constructor for a relationship: Start and end node must be specified. The type is fixed. |
4 | Copy constructor for the following two methods |
5 | Required to rename this relationship |
6 | Required for querying properties |
In contrast to the movie, we don’t see a static attribute allowing access to the relationship.
This is stored at the owner. In this case, the Person
:
public final class Person extends NodeBase<Person> {
public static final Person PERSON = new Person();
public final Directed<Movie> DIRECTED = new Directed<>(this, Movie.MOVIE);
public final ActedIn ACTED_IN = new ActedIn(this, Movie.MOVIE); (1)
public final Property NAME = this.property("name");
public final Property FIRST_NAME = this.property("firstName");
public final Property BORN = this.property("born");
}
1 | We create the relationship properties with this concrete instance pointing to another, concrete instance. |
5.2. Possible Usage
Please assume we did model the "Movie graph" (:play movies
in Neo4j-Browser) with the following scheme

and these classes, which have been generated with the building blocks described earlier (the required functions have been omitted for brevity):
final class Movie extends NodeBase<Movie> {
public static final Movie MOVIE = new Movie();
public final Property TAGLINE = this.property("tagline");
public final Property TITLE = this.property("title");
public final Property RELEASED = this.property("released");
public Movie() {
super("Movie");
}
}
final class Person extends NodeBase<Person> {
public static final Person PERSON = new Person();
public final Property NAME = this.property("name");
public final Property FIRST_NAME = this.property("firstName");
public final Directed<Movie> DIRECTED = new Directed<>(this, Movie.MOVIE);
public final ActedIn ACTED_IN = new ActedIn(this, Movie.MOVIE);
public final Property BORN = this.property("born");
public Person() {
super("Person");
}
}
final class ActedIn extends RelationshipBase<Person, Movie, ActedIn> {
public final Property ROLE = this.property("role");
protected ActedIn(Person start, Movie end) {
super(start, "ACTED_IN", end);
}
}
final class Directed<E extends NodeBase<?>> extends RelationshipBase<Person, E, Directed<E>> {
protected Directed(Person start, E end) {
super(start, "DIRECTED", end);
}
}
5.2.1. Work with properties
Properties can be used like normal objects:
var cypher = Cypher.match(Person.PERSON)
.returning(Person.PERSON.NAME, Person.PERSON.BORN)
.build().getCypher();
Assertions.assertThat(cypher)
.matches("MATCH \\(\\w+:`Person`\\) RETURN \\w+\\.name, \\w+\\.born");
Of course, new properties can be derived:
var cypher = Cypher.match(Person.PERSON)
.returning(Person.PERSON.NAME.concat(Cypher.literalOf(" whatever")))
.build().getCypher();
Assertions.assertThat(cypher)
.matches("MATCH \\(\\w+:`Person`\\) RETURN \\(\\w+\\.name \\+ ' whatever'\\)");
5.2.2. Query nodes or relationships by properties
Use withProperties
(and named
you like) to model your queries as needed.
Applicable to properties of nodes such as the title:
var movie = Movie.MOVIE.withProperties(Movie.MOVIE.TITLE, Cypher.literalOf("The Matrix")).named("m1");
var cypher = Cypher.match(movie)
.returning(movie)
.build().getCypher();
Assertions.assertThat(cypher)
.isEqualTo("MATCH (m1:`Movie` {title: 'The Matrix'}) RETURN m1");
and relationships
var actedIn = Person.PERSON.ACTED_IN.withProperties(Person.PERSON.ACTED_IN.ROLE, Cypher.literalOf("Neo"));
var cypher = Cypher.match(actedIn)
.returning(Movie.MOVIE)
.build().getCypher();
Assertions.assertThat(cypher)
.matches("MATCH \\(\\w+:`Person`\\)-\\[\\w+:`ACTED_IN` \\{role: 'Neo'}]->\\(\\w+:`Movie`\\) RETURN \\w+");
Note that the query will look like this, as we didn’t rename the objects and they used generated names:
`MATCH (dZVpwHhe000:`Person`)-[JcVKsSrn001:`ACTED_IN` {role: 'Neo'}]->(cDWeUJSI002:`Movie`) RETURN cDWeUJSI002`` as we didn't specify aliases)
5.2.3. Work with relationships
Relationships can be worked with like with properties:
var cypher = Cypher.match(Person.PERSON.DIRECTED)
.match(Person.PERSON.ACTED_IN)
.returning(Person.PERSON.DIRECTED, Person.PERSON.ACTED_IN)
.build().getCypher();
They are quite flexible together with the inverse
method. The following example also shows how to include non-static parts:
var otherPerson = Person.PERSON.named("o");
var cypher = Cypher.match(
Person.PERSON.DIRECTED.inverse()
.relationshipTo(otherPerson, "FOLLOWS") (1)
)
.where(otherPerson.NAME.isEqualTo(Cypher.literalOf("Someone")))
.returning(Person.PERSON)
.build().getCypher();
Assertions.assertThat(cypher)
.matches(
"MATCH \\(\\w+:`Movie`\\)<-\\[:`DIRECTED`]-\\(\\w+:`Person`\\)-\\[:`FOLLOWS`]->\\(o:`Person`\\) WHERE o\\.name = 'Someone' RETURN \\w+");
1 | Using a non-static fragment |
5.3. The Spring Data Neo4j 6 annotation processor
We provide a Java annotation processor for Spring Data Neo4j under the following coordinates:
org.neo4j:neo4j-cypher-dsl-codegen-sdn6:2021.1.1
The annotation processor understands classes annotated with @Node
and @RelationshipProperties
.
Inside those classes @Relationship
and @Property
are read.
The processor generates a static meta model for each annotated class found in the same package with an underscore (_
) added to the name.
The processor needs Spring Data Neo4j 6 and the Cypher-DSL in version 2021.1.0
or later on it’s classpath.
We recommend using it explicitly on the separate annotation processor classpath (via --processor-path
to javac
).
5.3.1. Configure your build
Maven
As a Maven user, please configure the build as follows:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-cypher-dsl-codegen-sdn6</artifactId>
<version>{neo4j-cypher-dsl.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Gradle
In a Gradle project, please add the following:
dependencies {
annotationProcessor 'org.neo4j:neo4j-cypher-dsl-codegen-sdn6:2021.1.1'
}
As dependency on the classpath
We recommend the annotation processor path for two reasons: The processor needs SDN as dependency. While SDN is already on the classpath, it might not fit the one we build the annotation processor against exactly. While the processor is lenient in that regard, your dependency setup might not. Furthermore: Why should you have the annotation processor as a dependency in your final artifact? This would be unnecessary. |
If you insist on having the SDN 6 annotation processor on the standard class path, please include with your Spring Data Neo4j 6 application like as follows to avoid dependency conflicts:
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-cypher-dsl-codegen-sdn6</artifactId>
<version>{neo4j-cypher-dsl.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
</exclusion>
</exclusions>
</dependency>
5.3.2. Usage
The processor supports the following arguments:
Name | Meaning |
---|---|
org.neo4j.cypherdsl.codegen.prefix, |
An optional prefix for the generated classes |
org.neo4j.cypherdsl.codegen.suffix |
An optional suffix for the generated classes |
org.neo4j.cypherdsl.codegen.indent_style |
The indent style (Use |
org.neo4j.cypherdsl.codegen.indent_size |
The number of whitespaces for the indent style |
org.neo4j.cypherdsl.codegen.timestamp |
An optional timestampe in ISO_OFFSET_DATE_TIME format for the generated classes. Defaults to the time of generation. |
org.neo4j.cypherdsl.codegen.add_at_generated |
A flag whether |
The generated classes can be used in variety of places:
@RestController
@RequestMapping("/api/movies")
public final class MoviesController {
private final MovieRepository movieRepository;
private final PeopleRepository peopleRepository;
MoviesController(MovieRepository movieRepository, PeopleRepository peopleRepository) {
this.movieRepository = movieRepository;
this.peopleRepository = peopleRepository;
}
@GetMapping({ "", "/" })
public List<Movie> get() {
return movieRepository
.findAll(Sort.by(Movie_.MOVIE.TITLE.getName()).ascending()); (1)
}
}
1 | Pass the name of the property TITLE to Spring Data’s sort facility |
Or build complex queries with it:
class MovieRepositoryExtImpl implements MovieRepositoryExt {
private final Neo4jTemplate neo4jTemplate;
MovieRepositoryExtImpl(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
}
@Override
public List<Movie> findAllMoviesRelatedTo(Person personOfInterest) {
var person = Person_.PERSON.named("p");
var actedIn = Movie_.MOVIE.named("a");
var directed = Movie_.MOVIE.named("d");
var m = Cypher.name("m");
var statement = Cypher.match(person)
.where(person.NAME.isEqualTo(Cypher.parameter("name")))
.with(person)
.optionalMatch(new ActedIn_(person, actedIn))
.optionalMatch(new Directed_(person, directed))
.with(Functions.collect(actedIn).add(Functions.collect(directed))
.as("movies"))
.unwind("movies").as(m)
.returningDistinct(m)
.orderBy(Movie_.MOVIE.named(m).TITLE).ascending()
.build();
return this.neo4jTemplate.findAll(
statement, Map.of("name", personOfInterest.getName()), Movie.class);
}
}
The full example is in neo4j-cypher-dsl-examples/neo4j-cypher-dsl-examples-sdn6
.
6. Appendix
Query-DSL support
The Neo4j Cypher-DSL has some support for Query-DSL. It can
-
turn instances of
com.querydsl.core.types.Predicate
intoorg.neo4j.cypherdsl.core.Condition
, -
turn instances of
com.querydsl.core.types.Expression
intoorg.neo4j.cypherdsl.core.Expression
, -
create
org.neo4j.cypherdsl.core.Node
instances fromcom.querydsl.core.types.Path
and also -
create
org.neo4j.cypherdsl.core.SymbolicName
With this, many static meta models based on Query-DSL can be used to create Queries and match on nodes. Most operations supported by Query-DSL are translated into Cypher that is understood by Neo4j 4.0+, so that most predicates should work - at least from a syntactic point of view - out of the box.
Expressions are most useful to address properties in return statements and the like.
Here’s one example on how to use it:
QPerson n = new QPerson("n"); (1)
Statement statement = Cypher.match(Cypher.adapt(n).asNode()) (2)
.where(Cypher.adapt(n.firstName.eq("P").and(n.age.gt(25))).asCondition()) (3)
.returning(Cypher.adapt(n).asName()) (4)
.build();
assertThat(statement.getParameters()).isEmpty();
assertThat(statement.getCypher())
.isEqualTo("MATCH (n:`Person`) WHERE n.firstName = 'P' AND n.age > 25 RETURN n");
1 | This makes use of a "Q"-class generated by Query-DSL APT (using the general processor).
alias and class based paths works, too.
Please make sure to name the instance accordingly when you use it as node (see next step) |
2 | Adapt the "Q"-class into a node. Please note it must be named accordingly, otherwise you query won’t return the expected results |
3 | Create some Query-DSL predicate based on the properties and adapt it as condition |
4 | Return some Query-DSL properties and adapt it as expression |
The Statement
offers a way to render all constants as parameters, so that they don’t bust the query cache:
QPerson n = new QPerson("n");
Statement statement = Cypher.match(Cypher.adapt(n).asNode())
.where(Cypher.adapt(n.firstName.eq("P").and(n.age.gt(25))).asCondition())
.returning(Cypher.adapt(n).asName())
.build();
statement.setRenderConstantsAsParameters(true); (1)
assertThat(statement.getParameters()).containsEntry("pcdsl01", "P"); (2)
assertThat(statement.getParameters()).containsEntry("pcdsl02", 25);
assertThat(statement.getCypher())
.isEqualTo("MATCH (n:`Person`) WHERE n.firstName = $pcdsl01 AND n.age > $pcdsl02 RETURN n"); (3)
1 | Set this to true before accessing parameters of the statement or rendering the statement |
2 | Access the statements parameters for generated parameter names |
3 | Compare the statement to the first listening. Constants are gone now |
The Neo4j Cypher-DSL will collect all parameters defined via Query-DSL for you:
QPerson n = new QPerson("n");
Statement statement = Cypher.match(Cypher.adapt(n).asNode())
.where(Cypher.adapt(n.firstName.eq(new Param<>(String.class, "name"))
.and(n.age.gt(new Param<>(Integer.class, "age"))) (1)
).asCondition()
)
.returning(Cypher.adapt(n).asName())
.build();
assertThat(statement.getParameterNames()).hasSize(2); (2)
assertThat(statement.getCypher())
.isEqualTo("MATCH (n:`Person`) WHERE n.firstName = $name AND n.age > $age RETURN n");
1 | Basically the same predicate as above, but with parameters, which will be turned into correct placeholders |
2 | Access the parameter names via the Statement object |
Required dependencies
The Query-DSL support in the Neo4j Cypher-DSL is optional, and the Query-DSL dependency is only in the provided
scope.
To make use of Cypher.adapt()
, you must add the following dependency in addition to the Cypher-DSL:
Maven
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>4.4.0</version>
<scope>provided</scope>
</dependency>
Gradle
dependencies {
implementation 'com.querydsl:query-dsl-core:4.4.0'
}
In case you want to use an annotation processor, you have to add additional dependencies and depending on your Java environment. We use the following in our tests:
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.4.0</version>
<classifier>general</classifier>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
Building the Neo4j Cypher-DSL
Requirements
For the full project, including examples and native tests:
-
GraalVM based on JDK 11: https://www.graalvm.org/downloads/
For the project, including examples but skipping native tests
-
JDK 11+ (Can be OpenJDK or Oracle JDK)
Maven 3.6.3 is our build tool of choice. We provide the Maven wrapper, see mvnw
respectively mvnw.cmd
in the project root; the wrapper downloads the appropriate Maven version automatically.
The build requires a local copy of the project:
$ git clone git@github.com:neo4j-contrib/cypher-dsl.git
Full build (including examples and native tests)
Before you proceed, verify your locally installed JDK version. The output should be similar:
$ java -version
openjdk version "11.0.10" 2021-01-19
OpenJDK Runtime Environment GraalVM CE 21.0.0 (build 11.0.10+8-jvmci-21.0-b06)
OpenJDK 64-Bit Server VM GraalVM CE 21.0.0 (build 11.0.10+8-jvmci-21.0-b06, mixed mode, sharing)
Check whether GraalVM native-image
is present with:
native-image
$ gu list
ComponentId Version Component name Stability Origin
--------------------------------------------------------------------------------------------------------
js 21.0.0 Graal.js -
graalvm 21.0.0 GraalVM Core -
native-image 21.0.0 Native Image Early adopter github.com
You should see native-image
in the list. If not, install it via gu install native-image
.
After that, use ./mvnw
on a Unix-like operating system to build the Cypher-DSL:
$ ./mvnw clean verify
On a Windows machine, use
$ mvnw.cmd clean verify
Architecture
The Neo4j-Cypher-DSL consists of one main module: org.neo4j.cypherdsl.core
.
The coordinates of that module org.neo4j:neo4j-cypher-dsl
, the JDK module name is org.neo4j.cypherdsl.core
.
The rendering feature is part of the core module.
All other modules depend on the core. As the core reflects the Cypher language, it is not meant to be extendable. Therefore, there is little to know API to do so with the AST visitor being the exception.
We document our rules structure with jQAssistant, a Neo4j based tool for software analysis. jQAssistant is integrated in our build.
To run the analysis standalone, you have to compile and build the project first. As you can skip both examples and native tests, JDK 8 is sufficient todo so:
java --version (1)
./mvnw clean verify \
-pl \!org.neo4j:neo4j-cypher-dsl-examples,\!org.neo4j:neo4j-cypher-dsl-native-tests (2)
./mvnw jqassistant:server (3)
1 | Must be at least Java 8 |
2 | Build and verify all modules except the examples and the native tests, which includes all scannes |
3 | Start a local Neo4j server, reachable at http://localhost:7474/browser/. The instance contains a dedicated jQAssistant dashboard as well: http://localhost:7474/jqassistant/dashboard/ |
If you want to change the default Neo4j HTTP- and Bolt-ports, start the server like this:
./mvnw jqassistant:server -Djqassistant.embedded.httpPort=4711 -Djqassistant.embedded.boltPort=9999
Coding rules
Consistent naming
The following naming conventions are used throughout the project:
org.neo4j.cypherdsl
.MATCH
(project:Maven:Project)-[:CREATES]->(:Artifact)-[:CONTAINS]->(type:Type)
WHERE
NOT type.fqn starts with 'org.neo4j.cypherdsl'
RETURN
project as Project, collect(type) as TypeWithWrongName
API
General considerations
We use @API Guardian to keep track of what we expose as public or internal API. To keep things both clear and concise, we restrict the usage of those annotations to interfaces, classes (only public methods and constructors: and annotations.
MATCH (c:Java)-[:ANNOTATED_BY]->(a)-[:OF_TYPE]->(t:Type {fqn: 'org.apiguardian.api.API'}),
(p)-[:DECLARES]->(c)
WHERE c:Member AND NOT (c:Constructor OR c:Method)
RETURN p.fqn, c.name
Public interfaces, classes or annotations are either part of internal or public API and therefore must have a status:
MATCH (c:Java)-[:ANNOTATED_BY]->(a)-[:OF_TYPE]->(t:Type {fqn: 'org.apiguardian.api.API'}),
(a)-[:HAS]->({name: 'status'})-[:IS]->(s)
WHERE ANY (label IN labels(c) WHERE label in ['Interface', 'Class', 'Annotation'])
WITH c, trim(split(s.signature, ' ')[1]) AS status
WITH c, status,
CASE status
WHEN 'INTERNAL' THEN 'Internal'
ELSE 'Public'
END AS type
MERGE (a:Api {type: type, status: status})
MERGE (c)-[:IS_PART_OF]->(a)
RETURN c,a
Internal API
While we are pretty clear about the intended use of our classes (being experimental, public API or strictly internal), we want to make sure that no-one can coincidentally inherit from internal classes that we couldn’t restrict to default package visibility:
MATCH (c:Class)-[:IS_PART_OF]->(:Api {type: 'Internal'})
WHERE c.visibility = 'public'
AND coalesce(c.abstract, false) = false
AND NOT exists(c.final)
RETURN c.name
Structure
Neo4j-Cypher-DSL Core
The core of the Cypher-DSL is consist of a set of classese that loosely reassemble the openCypher spec, especially in the railroad diagrams.
The main package of the core module is org.neo4j.cypherdsl.core
which also reflects in the JDK module name: org.neo4j.cypherdsl.core
.
Part of the Cypher-DSL core is also the renderer
package as the main goal of the core is to render Cypher.
The renderer
package is a sub-package of core
as it is an essential part of it and in addition, the above mentioned
JDK module name should reflect exactly one package.
So while all other subpackages in core
can be used freely from the core
classes themselves, we don’t want to access the
renderer
package apart from one exception: The AbstractStatement
class can be used to invoke the rendering process without
explicitly specifying a renderer:
MATCH (a:Main:Artifact)
MATCH (a)-[:CONTAINS]->(p1:Package)
WHERE p1.fqn in ['org.neo4j.cypherdsl.core']
WITH p1, a
MATCH (p1)-[:CONTAINS]->(t:Type)
MATCH (t)-[:DEPENDS_ON]->(t2:Type)<-[:CONTAINS]-(p2:Package)<-[:CONTAINS]-(a)
WHERE p2.fqn = 'org.neo4j.cypherdsl.core.renderer'
AND t.fqn <> 'org.neo4j.cypherdsl.core.AbstractStatement'
RETURN t, t2
The renderer
package is not only free to use the whole core
, it must do so to fulfill its purpose.
The support
and utils
packages however should not have dependencies outside their own:
MATCH (a:Main:Artifact)
MATCH (a)-[:CONTAINS]->(p1:Package)-[:DEPENDS_ON]->(p2:Package)<-[:CONTAINS]-(a)
WHERE p1.fqn IN ['org.neo4j.cypherdsl.core.support', 'org.neo4j.cypherdsl.core.utils']
RETURN p1,p2;
Change log
2021.1
2021.1.1
This is a drop-in replacement for 2021.1.0. Introducing the interface for Property
broke the mutate
operation,
for which no test was in place. This and the bug has been fixed.
2021.1.0
2021.1.0 comes with a ton of new features and a handful of breaking changes.
Fear not, the breaking changes are resolvable by recompiling your application.
We turned Node
, Relationship
and Property
into interfaces and provide now NodeBase
and RelationshipBase
so that you can
use them to build a static meta-model of your application. A PropertyBase
might follow.
Find out everything about the new possibility to define a static meta model in the manual.
The manual also includes a major part about the two new modules we offer:
org.neo4j:neo4j-cypher-dsl-codegen-core
and org.neo4j:neo4j-cypher-dsl-codegen-sdn6
.
neo4j-cypher-dsl-codegen-core
provides the infrastructure necessary to build code generators for creating a domain model
following our recommendation and neo4j-cypher-dsl-codegen-sdn6
is a first implementation of that:
A Java annotation processor that can be added to any Spring Data Neo4j 6 project in version 6.0.6 or higher.
It will find your annotated domain classes and turn them into a model you can use to build queries.
Last but not least: We added support for some expressions of the more generic QueryDSL.
This will require com.querydsl:querydsl-core
on the class path but only if you decide to call Cypher#adapt(foreignExpression)
.
This is a feature that is driven by Spring Data Neo4j 6.1 in which we build upon this to provide a QuerydslPredicateExecutor
.
Find more in this section of the manual.
๐ Features
-
GH-154 - Make Node and Relationship extendable.
-
GH-155 - Provide infrastructure for generating a static meta model.
-
GH-156 - Create an annotation processor for Spring Data Neo4j 6.
-
GH-167 - Add support for some Query-DSL expressions.
-
Introduce a statement context for allowing anonymous parameters (use
Cypher#anonParameter()
to define a parameter with a value but without a name. The name will be accessible on the statement after rendering). -
Make rendering of constants as parameters configurable.
-
Allow specification of the direction while creating a sort item.
-
Introduce an interface for Property.
2021.0
2021.0.2
This will already be the last release of the 2021.0 line. 2021.1 will be API compatible but not ABI compatible, as some classes have been changed into interfaces. That means it is not a drop in replacement, but your application needs to be recompiled. |
๐ Features
-
GH-157 - Provide a method to turn a Java map into an expression.
-
GH-158 - Improve pretty printing of subqueries.
-
Allow the use of raw cypher as expressions.
-
Allow symbolic names to be used as aliases.
-
Cache some symbolic names.
-
Add support for the keys() function.
๐ Bug Fixes
-
GH-149 - Avoid possible stackoverflow exception during visitor traversal.
-
GH-159 - Fix missing labels for nodes after
WITH
.
๐งน Housekeeping
-
GH-148 - Add jQAssistant rules and improve building documentation.
-
Add Maven PMD plugin.
Thanks Andy for the improvements of the pretty printer.
2021.0.1
๐ Features
-
GH-147 - Configuration infrastructure for renderer. First use case being a simple, pretty printing renderer.
The feature looks like this:
var c = node("Configuration").named("c");
var d = node("Cypher-DSL").named("d");
var mergeStatement = merge(c.relationshipTo(d, "CONFIGURES"))
.onCreate()
.set(
d.property("version").to(literalOf("2021.0.1")),
c.property("prettyPrint").to(literalTrue())
)
.onMatch().set(c.property("indentStyle").to(literalOf("TAB")))
.returning(d).build();
var renderer = Renderer.getRenderer(Configuration.prettyPrinting());
System.out.println(renderer.render(mergeStatement));
and gives you:
MERGE (c:Configuration)-[:CONFIGURES]->(d:`Cypher-DSL`)
ON CREATE SET d.version = '2021.0.1', c.prettyPrint = true
ON MATCH SET c.indentStyle = 'TAB'
RETURN d
2021.0.0
2021.0.0 comes with a lot of new features. Thanks to Andy for his contributions!
Andy is one of our first users outside Spring Data Neo4j 6. He started to use the Cypher-DSL in Neo4j GraphQL Java. Neo4j GraphQL Java is a library to translate GraphQL based schemas and queries to Cypher and execute those statements with the Neo4j database. It can be used from a wide variety of frameworks.
We are happy and proud to be part of this and even more so about the input and contribution we got back from Andy.
Of course thanks for your input in form of tickets and discussions go out to @utnaf, @aaramg, @K-Lovelace and @maximelovino as well!
Noteworthy
Two things should be mentioned:
The bugfix for GH-121 might change behavior for some users: The changes prevents the forced rendering of an alias for objects when the original object - the one that has been aliased - is passed down to the DSL after an alias has been created.
The original intention for that behaviour was related to Map projection, in which the alias is actually rendered before the object.
So now the use of an aliased expression the first time triggers a AS b
respectively b: a
in a map projection.
All further calls will just render b
. If the original object is used again, a
will be rendered. If that is not desired
in your query and you rely on the alias, make sure you use the aliased expression returned from .as("someAlias")
.
The other thing are the combined features GH-135 and GH-146.
The Statement
class has become a fully fledged accessor to the Cypher String and the parameters used and if provided,
the values for those. The following shows a small example:
var person = Cypher.node("Person").named("p");
var statement = Cypher
.match(person)
.where(person.property("nickname").isEqualTo(Cypher.parameter("nickname")))
.set(
person.property("firstName").to(Cypher.parameter("firstName").withValue("Thomas")),
person.property("name").to(Cypher.parameter("name", "Anderson"))
)
.returning(person)
.build();
assertThat(statement.getCypher())
.isEqualTo("MATCH (p:`Person`) WHERE p.nickname = $nickname SET p.firstName = $firstName, p.name = $name RETURN p");
Collection<String> parameterNames = statement.getParameterNames();
assertThat(parameterNames).containsExactlyInAnyOrder("nickname", "firstName", "name");
Map<String, Object> parameters = statement.getParameters();
assertThat(parameters).hasSize(2);
assertThat(parameters).containsEntry("firstName", "Thomas");
assertThat(parameters).containsEntry("name", "Anderson");
๐ Features
-
GH-122 - Add support for index hints.
-
GH-123 - Expose nested building of nested properties as public API.
-
GH-124 - Add support for Neo4j’s mathematical functions.
-
GH-127 - Allow dynamic property lookup.
-
GH-128 - Provide asConditions for RelationshipPatterns.
-
GH-129 - Allow Expressions as Parameter for Skip and Limit.
-
GH-131 - Add support for projections on symbolic names.
-
GH-133 - Allow symbolic names to be used as condition.
-
GH-135 - Collect parameters defined on a statement.
-
GH-141 - Provide a property function on all expressions.
-
GH-142 - Provide a point function accepting generic expressions as parameter.
-
GH-146 - Allow a statement to render itself.
2020.1
2020.1.2
๐ Features
-
GH-88 - Add support for Neo4j 4.0 subqueries.
-
GH-104 - Add support for merge actions.
-
GH-101 - Introduce asFunction on an ongoing call definition.
Further improvements:
-
Add support for EXPLAIN and PROFILE keywords.
-
Qualify a yield call (only relevant for JDK15+)
-
Fix wrong offsets in the documentation.
-
Improve JavaDoc and document internal API.
-
Allow
WITH
clause afterYIELD
. -
Improve reusability of fragments.
-
Make ORDER clause buildable.
-
Remove parts of an experimental API.
We do publish the Project info now: Project info, including the Java API.
2020.1.0
๐ Features
-
GH-74 - Automatically generate symbolic name if required:
Node
andRelationship
objects generate a symbolic name if required and not set -
Added several new functions
-
GH-77
properties()
-
GH-81
relationships()
-
GH-83
startNode()
,endNode()
, -
GH-89 All temporal functions
-
-
GH-76 - Added the list operator (
[]
for accessing sub lists and indexes).
๐ Bug Fixes
-
GH-82 - Expose all necessary interfaces for
call
-
GH-84 - Fix rendering of nested sub trees.
-
GH-95 - NPE during the creation of map projections
-
GH-96 - Make sure aliased expressions are not rendered multiple times.
๐งน Housekeeping
-
GH-67 - Improvements in regards of Java generics.
-
GH-68 - Clean up the Functions api.
-
GH-69 - Avoid star and static imports.
-
GH-72 - Some release cleanup
-
GH-75 - Move Assert to internal utils package.
-
GH-89 -
RelationshipDetails
is now internal API. -
GH-93 - Ensure compatibility with GraalVM native.
-
GH-94 - Bring back SymbolicName#concat.
2020.0
2020.0.1
This is the first patch release for the rebooted Cypher-DSL project.
2020.0.0
This is the first version of the rebooted Neo4j Cypher-DSL project. This version has been extracted from SDN-RX.
It’s a completely revamped API and we use it in all places in SDN/RX for generating Cypher-Queries.
We use CalVer in the same way Spring does since early 2020 (see Updates to Spring Versions) from this release onwards.
You’ll find the manual of the latest release version under http://neo4j-contrib.github.io/cypher-dsl/current/ and the current development version - or main - under http://neo4j-contrib.github.io/cypher-dsl/main/.