Spring Data Neo4j 扩展
可用于 Spring Data Neo4j 仓库的扩展
Spring Data Neo4j 提供了若干可添加到仓库上的扩展(或称「混入」,mixin)。什么是混入?根据 Wikipedia 的定义,混入是一种语言概念,允许程序员将某些代码注入到一个类中。混入编程是一种软件开发风格:功能单元在一个类中被创建,然后与其他类混合在一起。
Java 在语言层面并不支持这一概念,但我们通过若干接口以及一个运行时来模拟它——该运行时会为这些接口添加相应的实现与拦截器。
默认添加的混入分别是 QueryByExampleExecutor 和 ReactiveQueryByExampleExecutor。这些接口在按示例查询中有详细说明。
此外还提供以下混入:
-
QuerydslPredicateExecutor -
CypherdslConditionExecutor -
CypherdslStatementExecutor -
ReactiveQuerydslPredicateExecutor -
ReactiveCypherdslConditionExecutor -
ReactiveCypherdslStatementExecutor
为生成的查询添加动态条件
QuerydslPredicateExecutor 与 CypherdslConditionExecutor 提供了相同的概念:由 SDN 生成一个查询,由您提供将被附加进去的「谓词」(Query DSL)或「条件」(Cypher DSL)。我们推荐使用 Cypher DSL,因为 SDN 原生就是用它来工作的。您甚至可以考虑使用注解处理器,它会为您生成一个静态元模型。
它是如何工作的?按前文所述声明您的仓库,并添加以下接口中的一个:
interface QueryDSLPersonRepository extends Neo4jRepository<Person, Long>, 1
QuerydslPredicateExecutor<Person> {
2
}
static class DtoPersonProjection {
private final String firstName;
DtoPersonProjection(String firstName) {
this.firstName = firstName;
}
String getFirstName() {
return this.firstName;
}
}
标准的仓库声明
Query DSL 混入
或者
interface PersonRepository extends Neo4jRepository<Person, Long>, 1
CypherdslConditionExecutor<Person> {
2
}
标准的仓库声明
Cypher DSL 混入
下面以 Cypher DSL 条件执行器为例展示用法:
Node person = Cypher.node("Person").named("person"); 1
Property firstName = person.property("firstName"); 2
Property lastName = person.property("lastName");
assertThat(repository.findAll(
this.firstName.eq(Cypher.anonParameter("Helge"))
.or(this.lastName.eq(Cypher.parameter("someName", "B."))), 3
this.lastName.descending() 4
)).extracting(Person::getFirstName).containsExactly("Helge", "Bela");
定义一个具名的
Node对象,指向查询的根节点从它派生出若干属性
创建一个
or条件。名字使用了一个匿名参数,姓氏使用了一个具名参数。这就是在这些片段中定义参数的方式,也是相比 Query DSL 混入的优势之一——后者做不到这一点。字面量可以用Cypher.literalOf来表达。从某个属性定义一个
SortItem
Query DSL 混入的代码看起来也相当相似。选择 Query DSL 混入的理由可能是对其 API 更熟悉,以及它也适用于其他存储。而不选它的理由则包括:您需要在 classpath 上额外引入一个库、它缺少对遍历关系的支持,以及前面提到的、它在谓词中不支持参数(严格来说它是支持的,但并没有 API 方法能把这些参数实际传递给正在执行的查询)。
对实体与投影使用(动态)Cypher-DSL 语句
添加相应的混入与使用条件执行器没有什么不同:
interface PersonRepository extends Neo4jRepository<Person, Long>, CypherdslStatementExecutor<Person> {
}
当您扩展 ReactiveNeo4jRepository 时,请使用 ReactiveCypherdslStatementExecutor。
CypherdslStatementExecutor 为 findOne 和 findAll 提供了若干重载。它们都以一个 Cypher-DSL 语句(或其正在进行中的定义)作为第一个参数;对于投影方法,还需传入一个类型。
如果查询需要参数,那么这些参数必须通过 Cypher-DSL 本身来定义、并由它来填充,如下例所示:
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");
});
}
动态查询在一个辅助方法中以类型安全的方式构建
我们在此处已经见过这种写法,那里也定义了若干持有模型的变量
我们定义了一个匿名参数,由传入方法的
name的实际值填充辅助方法返回的语句被用于查找一个实体
或一个投影。
findAll 方法的工作方式与此类似。命令式 Cypher-DSL 语句执行器还提供了一个返回分页结果的重载。