After countless refactoring of the whole project, I decided to use neither DTOs nor Interfaces.
Maybe I just don't understand the concept of projections well enough, but this gives me more transparency about what happens and this finally made my project work again.
For flexibility the repository interfaces look like this:
@Query("MATCH (person:Person) WHERE ID(person) = $id " +
"OPTIONAL MATCH (person)-[tw:TAGGED_WITH]->(t:Tag) " +
"RETURN collect(tw) as tags, collect(t) as tag, " +
"person")
<T> Optional<T> findById(Long id, Class<T> type);
Then there is a PersonService, that does the actual interaction with the rest of the code, and can do other things, like build queries for more complex searches:
public Optional<Person> findById(Long id) {
return repository.findById(id, Person.class);
}
public Stream<Person> searchPaginated(String searchFirstname, String searchLastname, Integer searchAge,
PageRequest pageRequest, AccessRights accessRights) {
Node evt = Cypher.node("Person").named("p");
// Build the query based on which parameters are set
return repository.findAll(statement).stream();
}