Back in SDN 3.x there has been a method
findAllBySchemaPropertyValue
that took a property name and a possible value and retrieved all nodes having a property with that value. This tip demonstrates how to bring that back. All the example code is on GitHub in the projectuse-dynamic-finder
.
1. Problem
I want to be able to define a very generic query method on my Spring Data Neo4j repository. The method should find all nodes having a defined property with a fixed value.
2. Solution
Have a look at class org.neo4j.ogm.session.Session
.
It offers a method loadAll
that takes a class and a org.neo4j.ogm.cypher.Filter
as arguments.
Session
and Filter
are both part of the Neo4j-OGM project, that is part of the Object-Graph-Mapping framework on which SDN depends.
So we are safe to use that.
The other building block of the solution is the feature that we can add custom behaviour to all Spring Repositories by replacing the base class.
Read more about that here: "Customize the Base Repository".
The reason we are not just adding fragments to one repository as described in 7.6.1 is simple:
Those fragments are to be reused for different domain entities and offer no easy way to retrieve the target class, which we need to use Session#loadAll
as described above.
So here is our new base class for all repository beans:
@NoRepositoryBean (1)
public class Neo4jRepositoryWithDynamicFinderImpl<T, ID extends Serializable>
extends SimpleNeo4jRepository<T, ID> { (2)
private final Class<T> domainClass; (3)
private final Session session;
public Neo4jRepositoryWithDynamicFinderImpl(
Class<T> domainClass, Session session
) {
super(domainClass, session); (4)
this.domainClass = domainClass;
this.session = session;
}
public Iterable<T> findAllByPropertyValue( (5)
String property, Object value
) {
return this.session.loadAll(
this.domainClass,
new Filter(property, ComparisonOperator.EQUALS, value) (6)
);
}
}
1 | Mark this one class @NoRepositoryBean thus not making it a concrete repository itself. |
2 | Extend from our SimpleNeo4jRepository to get all the base functionality. |
3 | For every instance of this class created by the SDN framework, we need to have access to the domain class it’s created for. The same applies for the Neo4j-OGM session to execute a query. |
4 | Needed for our base class we’re inheriting from. |
5 | We will declare the signature of this method here on every concrete repository that should support "dynamic finders". Here, it’s the blueprint implementation. |
6 | Here we create an OGM filter that operates an EQUALS comparision on the given property with the given value. |
With this class in place we have an implementation for findAllByPropertyValue
in place.
Next step is to make Spring Data Neo4j aware of the new base class.
This is done through @EnableNeo4jRepositories
.
You put this annotation to any @Configuration
you like.
Your’re Main
-class will usually do just fine.
@SpringBootApplication
@EnableNeo4jRepositories(repositoryBaseClass = Neo4jRepositoryWithDynamicFinderImpl.class)
public class UseDynamicFinderApplication {
}
Now you can declare the new findAllByPropertyValue
in everyone of your repositories that should support it like this:
public interface ThingRepository extends Neo4jRepository<Thing, Long> {
Iterable<Thing> findAllByPropertyValue(String property, Object value);
}
Here the type parameter from the base class becomes a concrete Thing
.
Put it to use like in the included test:
@ExtendWith(SpringExtension.class) (1)
@DataNeo4jTest (2)
public class Neo4jRepositoryWithDynamicFinderImplTest {
private final ThingRepository thingRepository;
@Autowired (3)
public Neo4jRepositoryWithDynamicFinderImplTest(
ThingRepository thingRepository
) {
this.thingRepository = thingRepository;
}
@Test
public void findAllByPropertyValueShouldWork() {
(4)
thingRepository.save(new Thing("Thing 1", "This is the 1st thing", 9.99));
thingRepository.save(new Thing("AnotherThing", "This is a thing, too", 9.99));
thingRepository.save(new Thing("YetAnotherThing", "This is a thing, too", 9.99));
assertThat(thingRepository.findAllByPropertyValue("description", "This is a thing, too")) (5)
.extracting(Thing::getName)
.contains("AnotherThing", "YetAnotherThing");
}
}
1 | This is an integration test. The project uses JUnit 5 with the Spring extensions to run it. |
2 | It does only test the Neo4j data access layer. That includes all the repositories, the session factory and also an embedded Neo4j instance. |
3 | It’s JUnit 5, we can inject beans into the test class. |
4 | Create some things. |
5 | And use our dynamic finder method. |