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 中所定义)或已知的持久化实体类型。支持这些类型的集合,但不支持映射(map)。
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;
}
该属性在图中的名称不同
@Node
class ExtendedTestEntity extends TestEntity {
private String otherAttribute;
}
interface TestEntityInterfaceProjection {
String getName();
}
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;
}
}
该属性在所投影的实体上并不存在
下面展示了 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();
}
该仓库的领域类型是
TestEntity返回一个或多个
TestEntity的方法只会返回它的实例,因为它匹配领域类型返回一个或多个派生自该领域类型的类实例的方法,只会返回该派生类的实例。所讨论方法的领域类型将是该派生类,它仍然满足仓库自身的领域类型
该方法返回一个接口投影,因此该方法的返回类型与仓库的领域类型不同。该接口只能访问领域类型中定义的属性。需要添加后缀
By来让 SDN 不要在TestEntity中查找名为InterfaceProjections的属性该方法返回一个 DTO 投影。执行它会导致 SDN 发出一条警告,因为该 DTO 定义了
numberOfRelations作为额外属性,而这不在领域类型的契约中。TestEntity中的注解属性@Property会在查询中被正确转换为_property。与上面一样,返回类型与仓库的领域类型不同。需要添加后缀By来让 SDN 不要在TestEntity中查找名为DTOProjections的属性该方法同样返回一个 DTO 投影。不过不会发出警告,因为查询为投影中定义的额外属性包含了一个匹配的值。
聚合边界
通过引入多个投影来反映多层关系可能会很繁琐。为了在实体级别就简化这一点,可以添加一个额外参数 aggregateBoundary 并提供 1..n 个类。这样,被参数化的实体只会回报其 @Id 字段,而 SDN 不会追踪它的关系,也不会获取其他属性。
仍然可以对这些实体使用基于接口的投影。这些投影甚至可以比声明的聚合边界更宽泛,例如包含属性或关系。