Spring Data Neo4j Projections

As stated above, projections come in two flavors: Interface and DTO based projections. In Spring Data Neo4j both types of projections have a direct influence which properties and relationships are transferred over the wire. Therefore, both approaches can reduce the load on your database in case you are dealing with nodes and entities containing lots of properties which might not be needed in all usage scenarios in your application.

For both interface and DTO based projections, Spring Data Neo4j will use the repository’s domain type for building the query. All annotations on all attributes that might change the query will be taken in consideration. The domain type is the type that has been defined through the repository declaration (Given a declaration like interface TestRepository extends CrudRepository<TestEntity, Long> the domain type would be TestEntity).

Interface based projections will always be dynamic proxies to the underlying domain type. The names of the accessors defined on such interfaces (like getName) must resolve to properties (here: name) that are present on the projected entity. Whether those properties have accessors or not on the domain type is not relevant, as long as they can be accessed through the common Spring Data infrastructure. The latter is already ensured, as the domain type wouldn’t be a persistent entity in the first place.

DTO based projections are somewhat more flexible when used with custom queries. While the standard query is derived from the original domain type and therefore only the properties and relationship being defined there can be used, custom queries can add additional properties.

The rules are as follows: first, the properties of the domain type are used to populate the DTO. In case the DTO declares additional properties - via accessors or fields - Spring Data Neo4j looks in the resulting record for matching properties. Properties must match exactly by name and can be of simple types (as defined in org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes) or of known persistent entities. Collections of those are supported, but maps are not.

Multi-level projections

Spring Data Neo4j also supports multi-level projections.

Example of multi-level projection
interface ProjectionWithNestedProjection {

    String getName();

    List<Subprojection1> getLevel1();

    interface Subprojection1 {
        String getName();
        List<Subprojection2> getLevel2();
    }

    interface Subprojection2 {
        String getName();
    }
}

Even though it is possible to model cyclic projections or point towards entities that will create a cycle, the projection logic will not follow those cycles but only create cycle-free queries.

Multi-level projections are bounded to the entities they should project. RelationshipProperties fall into the category of entities in this case and needs to get respected if projections get applied.

Data manipulation of projections

If you have fetched the projection as a DTO, you can modify its values. But in case you are using the interface-based projection, you cannot just update the interface. A typical pattern that can be used is to provide a method in your domain entity class that consumes the interface and creates a domain entity with the copied values from the interface. This way, you can then update the entity and persist it again with the projection blueprint/mask as described in the next section.

Persistence of projections

Analogue to the retrieval of data via projections, they can also be used as a blueprint for persistence. The Neo4jTemplate offers a fluent API to apply those projections to a save operation.

You could either save a projection for a given domain class

Save projection for a given domain class
Projection projection = neo4jTemplate.save(DomainClass.class).one(projectionValue);

or you could save a domain object but only respect the fields defined in the projection.

Save domain object with a given projection blueprint
Projection projection = neo4jTemplate.saveAs(domainObject, Projection.class);

In both cases, that also are available for collection based operations, only the fields and relationships defined in the projection will get updated.

To prevent deletion of data (e.g. removal of relationships), you should always load at least all the data that should get persisted later.

A full example

Given the following entities, projections and the corresponding repository:

A simple entity
@Node
class TestEntity {
    @Id @GeneratedValue private Long id;

    private String name;

    @Property("a_property") (1)
    private String aProperty;
}
1 This property has a different name in the Graph
A derived entity, inheriting from TestEntity
@Node
class ExtendedTestEntity extends TestEntity {

    private String otherAttribute;
}
Interface projection of TestEntity
interface TestEntityInterfaceProjection {

    String getName();
}
DTO projection of TestEntity, including one additional attribute
class TestEntityDTOProjection {

    private String name;

    private Long numberOfRelations; (1)

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getNumberOfRelations() {
        return numberOfRelations;
    }

    public void setNumberOfRelations(Long numberOfRelations) {
        this.numberOfRelations = numberOfRelations;
    }
}
1 This attribute doesn’t exist on the projected entity

A repository for TestEntity is shown below and it will behave as explained with the listing.

A repository for the TestEntity
interface TestRepository extends CrudRepository<TestEntity, Long> { (1)

    List<TestEntity> findAll(); (2)

    List<ExtendedTestEntity> findAllExtendedEntities(); (3)

    List<TestEntityInterfaceProjection> findAllInterfaceProjectionsBy(); (4)

    List<TestEntityDTOProjection> findAllDTOProjectionsBy(); (5)

    @Query("MATCH (t:TestEntity) - [r:RELATED_TO] -> () RETURN t, COUNT(r) AS numberOfRelations") (6)
    List<TestEntityDTOProjection> findAllDTOProjectionsWithCustomQuery();
}
1 The domain type of the repository is TestEntity
2 Methods returning one or more TestEntity will just return instances of it, as it matches the domain type
3 Methods returning one or more instances of classes that extend the domain type will just return instances of the extending class. The domain type of the method in question will be the extended class, which still satisfies the domain type of the repository itself
4 This method returns an interface projection, the return type of the method is therefore different from the repository’s domain type. The interface can only access properties defined in the domain type. The suffix By is needed to make SDN not look for a property called InterfaceProjections in the TestEntity
5 This method returns a DTO projection. Executing it will cause SDN to issue a warning, as the DTO defines numberOfRelations as additional attribute, which is not in the contract of the domain type. The annotated attribute aProperty in TestEntity will be correctly translated to a_property in the query. As above, the return type is different from the repositories' domain type. The suffix By is needed to make SDN not look for a property called DTOProjections in the TestEntity
6 This method also returns a DTO projection. However, no warning will be issued, as the query contains a fitting value for the additional attributes defined in the projection
While the repository in the listing above uses a concrete return type to define the projection, another variant is the use of dynamic projections as explained in the parts of the documentation Spring Data Neo4j shares with other Spring Data Projects. A dynamic projection can be applied to both closed and open interface projections as well as to class based DTO projections:

The key to a dynamic projection is to specify the desired projection type as the last parameter to a query method in a repository like this: <T> Collection<T> findByName(String name, Class<T> type). This is a declaration that could be added to the TestRepository above and allow for different projections retrieved by the same method, without to repeat a possible @Query annotation on several methods.