跳到主要内容

Spring Data Neo4j 投影

DeepSeek V3 中英对照 Spring Data Neo4j Projections

如上所述,投影有两种类型:基于接口的投影和基于 DTO 的投影。在 Spring Data Neo4j 中,这两种类型的投影都会直接影响哪些属性和关系通过网络传输。因此,如果你的应用程序处理的节点和实体包含大量属性,而这些属性在所有使用场景中可能并不需要,那么这两种方法都可以减少数据库的负载。

对于基于接口和 DTO 的投影,Spring Data Neo4j 将使用仓库的领域类型来构建查询。所有可能影响查询的属性上的注解都将被考虑在内。领域类型是通过仓库声明定义的类型(例如,给定声明 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();
}
}
java

尽管可以建模循环投影或指向将创建循环的实体,但投影逻辑不会遵循这些循环,而只会创建无循环的查询。

多级投影被绑定到它们应该投影的实体上。在这种情况下,RelationshipProperties 属于实体的范畴,如果应用了投影,则需要对其进行尊重。

投影的数据操作

如果你已经将投影获取为一个 DTO,你可以修改它的值。但如果你使用的是基于接口的投影,你不能直接更新接口。一种典型的模式是在你的领域实体类中提供一个方法,该方法接受接口并创建一个领域实体,将接口中的值复制到实体中。这样,你就可以更新实体,并按照下一节中描述的投影蓝图/掩码再次持久化它。

投影的持久性

与通过投影检索数据类似,它们也可以用作持久化的蓝图。Neo4jTemplate 提供了一个流畅的 API,用于将这些投影应用到保存操作中。

你可以为给定的领域类保存一个投影

Projection projection = neo4jTemplate.save(DomainClass.class).one(projectionValue);
java

或者你可以保存一个领域对象,但只保留投影中定义的字段。

Projection projection = neo4jTemplate.saveAs(domainObject, Projection.class);
java

在这两种情况下,这些操作也适用于基于集合的操作,只有投影中定义的字段和关系会被更新。

备注

为了防止数据被删除(例如关系的移除),您应该始终至少加载所有以后需要持久化的数据。

完整示例

给定以下实体、投影及相应的仓库:

@Node
class TestEntity {
@Id @GeneratedValue private Long id;

private String name;

@Property("a_property") 1
private String aProperty;
}
java
  • 该属性在 Graph 中有不同的名称

@Node
class ExtendedTestEntity extends TestEntity {

private String otherAttribute;
}
java
interface TestEntityInterfaceProjection {

String getName();
}
java
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;
}
}
java
  • 该属性在投影的实体中不存在

下面展示了 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();
}
java
  • 该仓库的领域类型为 TestEntity

  • 返回一个或多个 TestEntity 的方法将直接返回其实例,因为它与领域类型匹配

  • 返回一个或多个扩展领域类型类的实例的方法将直接返回扩展类的实例。该方法的领域类型将是扩展类,这仍然满足仓库本身的领域类型

  • 该方法返回一个接口投影,因此该方法的返回类型与仓库的领域类型不同。该接口只能访问领域类型中定义的属性。需要后缀 By 来使 SDN 不在 TestEntity 中查找名为 InterfaceProjections 的属性

  • 该方法返回一个 DTO 投影。执行它会导致 SDN 发出警告,因为 DTO 定义了 numberOfRelations 作为附加属性,而该属性不在领域类型的契约中。TestEntity 中注解的属性 aProperty 将在查询中正确转换为 a_property。如上所述,返回类型与仓库的领域类型不同。需要后缀 By 来使 SDN 不在 TestEntity 中查找名为 DTOProjections 的属性

  • 该方法也返回一个 DTO 投影。然而,不会发出警告,因为查询包含与投影中定义的附加属性相匹配的值

提示

虽然上面列出的 仓库使用具体返回类型来定义投影,但另一种变体是使用动态投影,正如 Spring Data Neo4j 与其他 Spring Data 项目共享的文档部分所解释的那样。动态投影可以应用于封闭和开放的接口投影,也可以应用于基于类的 DTO 投影:

动态投影的关键在于在仓库的查询方法中,将所需的投影类型指定为最后一个参数,例如:<T> Collection<T> findByName(String name, Class<T> type)。这是一个可以在上述 TestRepository 中添加的声明,允许通过同一方法检索不同的投影,而无需在多个方法上重复可能的 @Query 注解。