Spring Data Neo4j 投影
如上所述,投影有两种形式:基于接口的投影和基于 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 中定义)或已知的持久实体。支持这些类型的集合,但不支持映射。
Spring Data Neo4j 中还内置了一种额外的机制,允许在实体定义级别定义加载和持久化边界。请参阅聚合边界部分了解更多信息。
多级投影
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);
在这两种情况下,对于基于集合的操作也可用,只有在投影中定义的字段和关系才会更新。
| 为防止数据删除(例如,删除关系),您应该始终至少加载所有稍后将要持久化的数据。 |
一个完整的例子
给定以下实体、投影和相应的仓库
@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 的仓库,它将按照列表中所述的方式运行。
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();
}
| 1 | 仓库的领域类型是 TestEntity |
| 2 | 返回一个或多个 TestEntity 的方法将只返回其实例,因为它与领域类型匹配 |
| 3 | 返回一个或多个扩展领域类型的类的实例的方法将只返回扩展类的实例。该方法的领域类型将是扩展类,它仍然满足仓库本身的领域类型 |
| 4 | 此方法返回一个接口投影,因此该方法的返回类型与仓库的领域类型不同。该接口只能访问领域类型中定义的属性。需要后缀 By 以使 SDN 不在 TestEntity 中查找名为 InterfaceProjections 的属性 |
| 5 | 此方法返回一个 DTO 投影。执行它将导致 SDN 发出警告,因为 DTO 将 numberOfRelations 定义为附加属性,这不在领域类型的契约中。TestEntity 中带注解的属性 aProperty 将在查询中正确翻译为 a_property。如上所述,返回类型与仓库的领域类型不同。需要后缀 By 以使 SDN 不在 TestEntity 中查找名为 DTOProjections 的属性 |
| 6 | 此方法也返回 DTO 投影。但是,不会发出警告,因为查询包含与投影中定义的附加属性匹配的值 |