属性路径
本章介绍属性路径的概念。属性路径是一种穿越领域类进行导航的形式,用于在与模型交互的上下文中应用某些方面。应用程序代码向数据访问组件提供属性路径,以表达诸如在查询中选择属性、构造谓词或应用排序之类的意图。属性路径源自其所属类型,可以由一段或多段组成。
按照领域驱动设计原则,构成您持久化领域模型骨干、并通过 Spring Data 访问的那些类称为实体。对象图的入口称为聚合根。
了解如何导航和引用这些属性,对于使用仓库和查询操作至关重要。
属性路径概述
属性路径提供了一种简单的、基于文本的机制来导航领域模型属性。本节介绍属性路径导航的基础,并展示基于字符串与类型安全两种方式之间的权衡。
- Java
- Kotlin
class Person {
String firstname, lastname;
int age;
Address address;
List<Address> previousAddresses;
String getFirstname() { … } // other property accessors omitted for brevity
}
class Address {
String city, street;
// accessors omitted for brevity
}
class Person {
var firstname: String? = null
var lastname: String? = null
var age: Int = 0
var address: Address? = null
var previousAddresses: List<Address> = emptyList()
}
class Address {
var city: String? = null
var street: String? = null
}
属性路径在整个 Spring Data 操作中使用点记法来表达属性引用,例如排序和过滤:
Sort.by("firstname", "address.city")
属性路径由一段或多段组成,段之间用点(.)分隔。接受属性路径的方法支持单段引用(顶层属性)和多段导航,除非另有说明。
集合和数组属性支持透明地遍历到其组件类型,从而可以直接引用嵌套属性:
Sort.by("address.city") // <1>
Sort.by("previousAddresses") // <2>
Sort.by("previousAddresses.city") // <3>
从顶层
address属性导航到city字段。引用整个
previousAddresses集合(某些技术支持基于集合的排序)。遍历该集合,按每个地址的
city字段排序。
基于字符串的属性路径简单易用,可广泛应用,但也有一些需要权衡之处:
-
灵活性:属性路径非常灵活,可以由常量字符串、配置或用户输入结果构造。
-
无类型:字符串路径不携带编译期类型信息。作为文本内容,它们不依赖于底层领域类型。
-
重构风险:重命名领域属性通常需要手动更新字符串字面量;IDE 无法可靠地跟踪这些引用。
为提高重构安全性和类型一致性,请优先使用方法引用的类型安全属性引用。这种方式将属性路径与编译期类型信息关联起来,从而实现编译器校验和 IDE 驱动的重构。细节请参阅类型安全的属性引用。
有关实现细节,请参阅属性路径内部原理以了解更多信息。
属性路径内部原理
org.springframework.data.core 包是 Spring Data 穿越领域类进行导航的基础。TypeInformation 接口提供类型内省能力,能够解析属性的类型。PropertyPath 表示穿越领域类的文本导航路径。
它们共同提供:
-
泛型类型解析与内省
-
属性路径的创建与校验
-
集合和映射等复杂属性的实际类型解析
类型安全的属性引用
类型安全的属性引用消除了数据访问代码中一个常见的错误来源:脆弱的、基于字符串的属性引用。本节说明如何使用方法引用来表达对重构安全的属性路径。
虽然属性路径是对象导航的简单表示,但基于字符串的属性路径在重构过程中本质上很脆弱,因为随着属性定义与使用处距离的增加,它们很容易被遗漏。通过 TypedPropertyPath 提供的类型安全替代方案,从方法引用派生属性路径,从而让编译器能够校验属性名,并让 IDE 能够支持重构操作。
- Java
- Kotlin
// Inline usage with Sort
Sort.by(Person::getFirstName, Person::getLastName);
// Composed navigation
Sort.by(TypedPropertyPath.of(Person::getAddress).then(Address::getCity),
Person::getLastName);
// Inline usage with Sort
Sort.by(Person::firstName, Person::lastName)
// Composed navigation
Sort.by(Person::address / Address::city, Person::lastName)
类型安全的属性路径与查询抽象和条件构建器无缝集成,从而在不使用基于字符串的属性引用的情况下实现声明式查询构造。
采用类型安全的属性引用符合现代 Spring 开发原则。提供声明式、类型安全且流畅的 API 能简化对数据访问的推理,并通过 IDE 重构支持和编译器对无效属性的早期反馈,消除一整类潜在 bug。
Lambda 内省会被缓存以提高效率,从而支持重复使用。JVM 会复用静态 lambda 实例,使一次性解析的开销降至最低。
如果您想要一个类型安全的变体,使其在不直接与 Spring Data API 集成的场景中也能受益于编译器校验和 IDE 支持,您可以单独使用 TypedPropertyPath:
- Java
- Kotlin
import static org.springframework.data.core.TypedPropertyPath.path;
// Static import variant
path(Person::getAddress)
.then(Address::getCity);
// Fluent composition
TypedPropertyPath.of(Person::getAddress)
.then(Address::getCity);
// Kotlin API
TypedPropertyPath.of(Person::address / Address::city)
// as extension function
(Person::address / Address::city).toPath()
类型安全属性引用 API 建议
在使用(或构建)基于类型安全属性引用的 API 时,请考虑以下建议:
-
使用方法引用:接受方法引用(例如
Person::getFirstName)而非字符串,以利用编译期校验和 IDE 重构支持。方法引用是首选,因为它们在 Java 和 Kotlin 中具有相似的表示。此外,方法引用由于表示更简单,相比 lambda 提供了更好的性能基线。 -
利用
TypedPropertyPath的T类型:每当接受类型化属性路径时,请考虑使用TypedPropertyPath<T, P>的泛型类型T。将属性路径限制为当前操作中使用的特定领域类型,可降低误用其他类型中属性的可能性。每当接受或提供多个属性路径时,请考虑使用
TypedPropertyPath<T, ?>,以便在所属类型T的上下文中允许使用属性,从而将属性路径限制为同一个所属类型。
GraalVM 原生映像编译需要为可序列化的 TypedPropertyPath lambda 提供可达性元数据。Spring Data 通过 org.springframework.data.core.TypedPropertyPathFeature 内置了一个特性。该特性会自动激活,并为 lambda 解析以及所引用的反射成员(字段和方法)注册所需的序列化和反射元数据。另请注意,当使用 lambda 表达式而非方法引用时,您需要自行将包含该 lambda 表达式的类的 Java 源代码纳入原生映像配置。