|
此版本仍在开发中,目前尚不被视为稳定版本。如需最新稳定版本,请使用 Spring Data Neo4j 8.0.4! |
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中所定义)或已知的持久化实体。支持这些类型的集合,但不支持映射(maps)。
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;
}
TestEntityinterface TestEntityInterfaceProjection {
String getName();
}
TestEntity,包含一个额外的属性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 的存储库,其行为将如列表中所述。
TestEntityinterface 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 | 返回一个或多个扩展了领域类型(domain type)的类的实例的方法,将直接返回扩展类的实例。所讨论方法的领域类型将是扩展类,这仍然满足该仓库(repository)自身的领域类型。 |
| 4 | 此方法返回一个接口投影,因此该方法的返回类型与存储库的领域类型不同。该接口只能访问领域类型中定义的属性。By 后缀用于确保 SDN 不会在 TestEntity 中查找名为 InterfaceProjections 的属性。 |
| 5 | 此方法返回一个 DTO 投影。执行它将导致 SDN 发出警告,因为 DTO 定义了 numberOfRelations 作为额外属性,而该属性不在领域类型契约中。TestEntity 中标注的属性 aProperty 将正确地映射到查询中的 a_property。与上文相同,返回类型与存储库的领域类型不同。后缀 By 是必需的,以确保 SDN 不会在 TestEntity 中查找名为 DTOProjections 的属性。 |
| 6 | 此方法也返回一个 DTO 投影。然而,不会发出警告,因为查询包含适用于投影中定义的额外属性的合适值。 |
聚合边界
通过引入多个投影来反映多层级的关系可能会很繁琐。为了简化这一操作(即使在实体级别上),可以添加一个额外的参数 aggregateBoundary 并提供 1..n 个类。使用此方法,参数化实体将仅返回其 @Id 字段,而 SDN 将不会追踪其关系或获取其他属性。
仍然可以为这些实体使用基于接口的投影。这些投影甚至可以更广泛,因为声明的聚合边界,例如包括属性或关系。