Spring Data Neo4j 投影
如上所述,投影有两种类型:基于接口的投影和基于 DTO 的投影。在 Spring Data Neo4j 中,这两种类型的投影都直接影响哪些属性和关系通过网络传输。因此,如果你处理的节点和实体包含大量属性,而在你的应用程序中并非所有使用场景都需要这些属性时,这两种方法都可以减少数据库的负载。
对于基于接口和基于 DTO 的投影,Spring Data Neo4j 将使用 repository 的领域类型来构建查询。所有可能改变查询的属性上的注解都将被考虑在内。领域类型是通过 repository 声明定义的类型(例如,给定 interface TestRepository extends CrudRepository<TestEntity, Long>
这样的声明,领域类型将是 TestEntity
)。
基于接口的投影将始终是底层领域类型的动态代理。在此类接口上定义的访问器(例如 getName
)的名称必须解析为投影实体上存在的属性(此处为 name
)。这些属性在领域类型上是否有访问器并不重要,只要它们可以通过通用的 Spring Data 基础设施访问即可。后者已经得到保证,因为如果领域类型不是持久化实体,它就不会首先被用作领域类型。
基于 DTO 的投影在使用自定义查询时更加灵活。虽然标准查询是派生自原始领域类型,因此只能使用其中定义的属性和关系,但自定义查询可以添加额外的属性。
规则如下:首先,使用领域类型的属性来填充 DTO。如果 DTO 声明了额外的属性(通过访问器或字段),Spring Data Neo4j 会在结果记录中查找匹配的属性。属性必须名称完全匹配,并且可以是简单类型(如 org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes
中定义的)或已知的持久化实体。支持这些类型的集合,但不支持 Map。
多层投影
Spring Data Neo4j 也支持多层投影。
interface ProjectionWithNestedProjection {
String getName();
List<Subprojection1> getLevel1();
interface Subprojection1 {
String getName();
List<Subprojection2> getLevel2();
}
interface Subprojection2 {
String getName();
}
}
即使可以建模循环投影或指向会创建循环的实体,投影逻辑也不会跟踪这些循环,只会创建无循环的查询。
多层投影受其应投影到的实体限制。在这种情况下,RelationshipProperties
属于实体类别,应用投影时需要考虑。
投影的数据操作
如果你以 DTO 的形式获取了投影,你可以修改其值。但如果你使用的是基于接口的投影,你不能直接更新该接口。一种典型的模式是在你的领域实体类中提供一个方法,该方法接收该接口并创建一个带有从接口复制的值的领域实体。这样,你就可以更新该实体,并按照下一节所述,使用投影蓝图/掩码再次持久化它。
投影的持久化
与通过投影检索数据类似,它们也可以用作持久化的蓝图。Neo4jTemplate
提供了一个流式 API,用于将这些投影应用于保存操作。
你可以保存给定领域类的投影
Projection projection = neo4jTemplate.save(DomainClass.class).one(projectionValue);
或者你可以保存一个领域对象,但只考虑投影中定义的字段。
Projection projection = neo4jTemplate.saveAs(domainObject, Projection.class);
在这两种情况下(对于基于集合的操作也同样适用),只有投影中定义的字段和关系会被更新。
为了防止数据被删除(例如关系的移除),你应该始终至少加载所有稍后需要持久化的数据。 |
完整示例
给定以下实体、投影和相应的 repository
@Node
class TestEntity {
@Id @GeneratedValue private Long id;
private String name;
@Property("a_property") (1)
private String aProperty;
}
1 | 这个属性在图中的名称不同 |
TestEntity
@Node
class ExtendedTestEntity extends TestEntity {
private String otherAttribute;
}
TestEntity
的接口投影interface TestEntityInterfaceProjection {
String getName();
}
TestEntity
的 DTO 投影,包含一个额外属性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 | 这个属性在投影实体上不存在 |
下面展示了一个 TestEntity
的 repository,其行为如列表所示。
TestEntity
的 repositoryinterface 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 | 这个 repository 的领域类型是 TestEntity |
2 | 返回一个或多个 TestEntity 的方法将只返回其实例,因为这与领域类型匹配 |
3 | 返回一个或多个扩展领域类型的类实例的方法将只返回扩展类的实例。相关方法的领域类型将是扩展类,这仍然满足 repository 本身的领域类型 |
4 | 这个方法返回一个接口投影,因此该方法的返回类型与 repository 的领域类型不同。该接口只能访问领域类型中定义的属性。需要后缀 By 以便 SDN 不在 TestEntity 中查找名为 InterfaceProjections 的属性 |
5 | 这个方法返回一个 DTO 投影。执行它会使 SDN 发出警告,因为该 DTO 将 numberOfRelations 定义为额外属性,这不在领域类型的契约中。TestEntity 中带有注解的属性 aProperty 将被正确地转换为查询中的 a_property 。如上所述,返回类型与 repositories 的领域类型不同。需要后缀 By 以便 SDN 不在 TestEntity 中查找名为 DTOProjections 的属性 |
6 | 这个方法也返回一个 DTO 投影。然而,不会发出警告,因为查询包含了投影中定义的额外属性的合适值 |
虽然 上面的列表 中的 repository 使用具体的返回类型来定义投影,但另一种变体是使用 动态投影,这在 Spring Data Neo4j 与其他 Spring Data 项目共享的文档部分中有解释。动态投影可以应用于封闭和开放接口投影以及基于类的 DTO 投影 动态投影的关键是在 repository 的查询方法中将所需的投影类型指定为最后一个参数,例如: <T> Collection<T> findByName(String name, Class<T> type) 。这是一个可以添加到上面 TestRepository 的声明,允许通过同一个方法检索不同的投影,而无需在多个方法上重复可能的 @Query 注解。 |