Spring Data Neo4j Extensions

Available extensions for Spring Data Neo4j repositories

Spring Data Neo4j offers a couple of extensions or "mixins" that can be added to repositories. What is a mixin? According to Wikipedia mixins are a language concept that allows a programmer to inject some code into a class. Mixin programming is a style of software development, in which units of functionality are created in a class and then mixed in with other classes.

Java does not support that concept on the language level, but we do emulate it via a couple of interfaces and a runtime that adds appropriate implementations and interceptors for.

Mixins added by default are QueryByExampleExecutor and ReactiveQueryByExampleExecutor respectively. Those interfaces are explained in detail in Query by Example.

Additional mixins provided are:

  • QuerydslPredicateExecutor

  • CypherdslConditionExecutor

  • CypherdslStatementExecutor

  • ReactiveQuerydslPredicateExecutor

  • ReactiveCypherdslConditionExecutor

  • ReactiveCypherdslStatementExecutor

Add dynamic conditions to generated queries

Both the QuerydslPredicateExecutor and CypherdslConditionExecutor provide the same concept: SDN generates a query, you provide "predicates" (Query DSL) or "conditions" (Cypher DSL) that will be added. We recommend the Cypher DSL, as this is what SDN uses natively. You might even want to consider using the annotation processor that generates a static meta model for you.

How does that work? Declare your repository as described above and add one of the following interfaces:

interface QueryDSLPersonRepository extends
        Neo4jRepository<Person, Long>, (1)
        QuerydslPredicateExecutor<Person> { (2)
}
1 Standard repository declaration
2 The Query DSL mixin

OR

import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;

    interface PersonRepository extends
            Neo4jRepository<Person, Long>, (1)
            CypherdslConditionExecutor<Person> { (2)
    }
1 Standard repository declaration
2 The Cypher DSL mixin

Exemplary usage is shown with the Cypher DSL condition executor:

Node person = Cypher.node("Person").named("person"); (1)
Property firstName = person.property("firstName"); (2)
Property lastName = person.property("lastName");

assertThat(
        repository.findAll(
                firstName.eq(Cypher.anonParameter("Helge"))
                        .or(lastName.eq(Cypher.parameter("someName", "B."))), (3)
                lastName.descending() (4)
        ))
        .extracting(Person::getFirstName)
        .containsExactly("Helge", "Bela");
1 Define a named Node object, targeting the root of the query
2 Derive some properties from it
3 Create an or condition. An anonymous parameter is used for the first name, a named parameter for the last name. This is how you define parameters in those fragments and one of the advantages over the Query-DSL mixin which can’t do that. Literals can be expressed with Cypher.literalOf.
4 Define a SortItem from one of the properties

The code looks pretty similar for the Query-DSL mixin. Reasons for the Query-DSL mixin can be familiarity of the API and that it works with other stores, too. Reasons against it are the fact that you need an additional library on the class path, it’s missing support for traversing relationships and the above-mentioned fact that it doesn’t support parameters in its predicates (it technically does, but there are no API methods to actually pass them to the query being executed).

Using (dynamic) Cypher-DSL statements for entities and projections

Adding the corresponding mixin is not different from using the condition excecutor:

interface PersonRepository extends
        Neo4jRepository<Person, Long>,
        CypherdslStatementExecutor<Person> {
}

Please use the ReactiveCypherdslStatementExecutor when extending the ReactiveNeo4jRepository.

The CypherdslStatementExecutor comes with several overloads for findOne and findAll. They all take a Cypher-DSL statement respectively an ongoing definition of that as a first parameter and in case of the projecting methods, a type.

If a query requires parameters, they must be defined via the Cypher-DSL itself and also populated by it, as the following listing shows:

static Statement whoHasFirstNameWithAddress(String name) { (1)
    Node p = Cypher.node("Person").named("p"); (2)
    Node a = Cypher.anyNode("a");
    Relationship r = p.relationshipTo(a, "LIVES_AT");
    return Cypher.match(r)
            .where(p.property("firstName").isEqualTo(Cypher.anonParameter(name))) (3)
            .returning(
                    p.getRequiredSymbolicName(),
                    Cypher.collect(r),
                    Cypher.collect(a)
            )
            .build();
}

@Test
void fineOneShouldWork(@Autowired PersonRepository repository) {

    Optional<Person> result = repository.findOne(whoHasFirstNameWithAddress("Helge"));  (4)

    assertThat(result).hasValueSatisfying(namesOnly -> {
        assertThat(namesOnly.getFirstName()).isEqualTo("Helge");
        assertThat(namesOnly.getLastName()).isEqualTo("Schneider");
        assertThat(namesOnly.getAddress()).extracting(Person.Address::getCity)
                .isEqualTo("Mülheim an der Ruhr");
    });
}

@Test
void fineOneProjectedShouldWork(@Autowired PersonRepository repository) {

    Optional<NamesOnly> result = repository.findOne(
            whoHasFirstNameWithAddress("Helge"),
            NamesOnly.class  (5)
    );

    assertThat(result).hasValueSatisfying(namesOnly -> {
        assertThat(namesOnly.getFirstName()).isEqualTo("Helge");
        assertThat(namesOnly.getLastName()).isEqualTo("Schneider");
        assertThat(namesOnly.getFullName()).isEqualTo("Helge Schneider");
    });
}
1 The dynamic query is build in a type safe way in a helper method
2 We already saw this in here, where we also defined some variables holding the model
3 We define an anonymous parameter, filled by the actual value of name, which was passed to the method
4 The statement returned from the helper method is used to find an entity
5 Or a projection.

The findAll methods works similar. The imperative Cypher-DSL statement executor also provides an overload returning paged results.