原标题:Spring认证|Spring Data JDBC参考文档(内容来源:Spring中国教育管理中心)
9. JDBC 存储库
本章指出了 JDBC 存储库支持的特殊性。这建立在使用 Spring Data Repositories 中解释的核心存储库支持之上。您应该对那里解释的基本概念有充分的了解。
9.1. 为什么选择 Spring Data JDBC?
Java 世界中关系数据库的主要持久化 API 肯定是 JPA,它有自己的 Spring Data 模块。为什么还有一个?
JPA 做了很多事情来帮助开发人员。除其他外,它跟踪对实体的更改。它为你做延迟加载。它使您可以将广泛的对象构造映射到同样广泛的数据库设计。
这很棒,让很多事情变得非常简单。只需看一下基本的 JPA 教程。但是,对于 JPA 为什么要做某件事,这常常让人感到困惑。此外,概念上非常简单的事情在 JPA 中变得相当困难。
Spring Data JDBC 旨在通过采用以下设计决策在概念上更简单:
如果您加载一个实体,SQL 语句就会运行。完成此操作后,您将拥有一个完全加载的实体。没有进行延迟加载或缓存。
如果您保存一个实体,它将被保存。如果您不这样做,则不会。没有脏跟踪,也没有会话。
有一个关于如何将实体映射到表的简单模型。它可能只适用于相当简单的情况。如果您不喜欢那样,您应该编写自己的策略。Spring Data JDBC 仅对使用注释自定义策略提供非常有限的支持。
9.2. 领域驱动设计和关系数据库。
所有 Spring Data 模块都受到领域驱动设计中“存储库”、“聚合”和“聚合根”概念的启发。这些对于 Spring Data JDBC 来说可能更为重要,因为在某种程度上,它们与使用关系数据库时的常规做法背道而驰。
聚合是一组实体,可以保证在对其进行原子更改之间保持一致。一个经典的例子是Orderwith OrderItems。上的属性Order(例如,numberOfItems与 的实际数量OrderItems一致)在进行更改时保持一致。
跨聚合的引用不能保证在任何时候都是一致的。他们保证最终会变得一致。
每个聚合都有一个聚合根,它是聚合的实体之一。聚合仅通过该聚合根上的方法进行操作。这些是前面提到的原子变化。
存储库是对持久存储的抽象,它看起来像是某种类型的所有聚合的集合。对于 Spring Data 一般而言,这意味着您希望Repository每个聚合根都有一个。此外,对于 Spring Data JDBC,这意味着可从聚合根访问的所有实体都被视为该聚合根的一部分。Spring Data JDBC 假定只有聚合具有指向存储聚合的非根实体的表的外键,并且没有其他实体指向非根实体。
在当前的实现中,从聚合根引用的实体被 Spring Data JDBC 删除并重新创建。
您可以使用与您的工作和设计数据库的风格相匹配的实现来覆盖存储库方法。
9.3. 入门
引导设置工作环境的一种简单方法是在STS 中或从Spring Initializr创建一个基于 Spring 的项目。
首先,您需要设置一个正在运行的数据库服务器。请参阅您的供应商文档,了解如何为 JDBC 访问配置数据库。
在 STS 中创建 Spring 项目:
转到 File → New → Spring Template Project → Simple Spring Utility Project,然后在出现提示时按 Yes。然后输入项目和包名称,例如org.spring.jdbc.example.
将以下内容添加到pom.xmlfilesdependencies元素:
org.springframework.data
spring-data-jdbc
2.2.5
将 pom.xml 中 Spring 的版本改为
将 Maven 的 Spring Milestone 存储库的以下位置添加到您pom.xml的元素中,使其与您的元素处于同一级别:
spring-milestone
Spring Maven MILESTONE Repository
https://repo.spring.io/libs-milestone
存储库也可在此处浏览。
9.4. 示例库
有一个包含多个示例的GitHub 存储库,您可以下载并试用这些示例,以了解该库的工作原理。
9.5. 基于注解的配置
Spring Data JDBC 存储库支持可以通过 Java 配置的注解来激活,如下例所示:
示例 54.使用 Java 配置的 Spring Data JDBC 存储库
@Configuration
@EnableJdbcRepositories
class ApplicationConfig extends AbstractJdbcConfiguration {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
@Bean
NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
@Bean
TransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@EnableJdbcRepositories 为派生自的接口创建实现 Repository
AbstractJdbcConfiguration 提供 Spring Data JDBC 所需的各种默认 bean
创建DataSource到数据库的连接。这是以下两个 bean 方法所必需的。
创建
NamedParameterJdbcOperationsSpring Data JDBC 用来访问数据库的 。
Spring Data JDBC 利用 Spring JDBC 提供的事务管理。
在前面的例子中的配置类,通过使用设置了一个嵌入式HSQL数据库EmbeddedDatabaseBuilder的API spring-jdbc。该DataSource则用来建立
NamedParameterJdbcOperations和TransactionManager。我们最终通过使用@EnableJdbcRepositories. 如果没有配置基础包,则使用配置类所在的包。扩展AbstractJdbcConfiguration确保各种 bean 得到注册。覆盖其方法可用于自定义设置(见下文)。
使用 Spring Boot 可以进一步简化此配置。DataSource一旦启动器
spring-boot-starter-data-jdbc包含在依赖项中,使用 Spring Boot就足够了。其他一切都由 Spring Boot 完成。
在此设置中,您可能需要自定义几项内容。
9.5.1. 方言
Spring Data JDBC 使用接口的实现Dialect来封装特定于数据库或其 JDBC 驱动程序的行为。默认情况下,会AbstractJdbcConfiguration尝试确定正在使用的数据库并注册正确的Dialect. 这种行为可以通过覆盖来改变jdbcDialect(
NamedParameterJdbcOperations)。
如果您使用的数据库没有可用的方言,那么您的应用程序将无法启动。在这种情况下,您必须要求您的供应商提供Dialect实现。或者,您可以:
实施您自己的Dialect.
实现一个JdbcDialectProvider返回Dialect.
通过spring.factories在下面创建资源来注册提供者META-INF并通过添加一行来执行注册
org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=
9.6. 持久实体
可以使用该CrudRepository.save(…)方法执行保存聚合。如果聚合是新的,这将导致聚合根的插入,然后是所有直接或间接引用的实体的插入语句。
如果聚合根不是新的,则所有引用的实体都会被删除,聚合根会更新,并且所有引用的实体都会再次插入。请注意,实例是否为新实例是实例状态的一部分。
这种方法有一些明显的缺点。如果实际更改的引用实体很少,则删除和插入是浪费。虽然这个过程可以而且很可能会得到改进,但 Spring Data JDBC 可以提供的内容存在某些限制。它不知道聚合的先前状态。因此,任何更新过程始终必须采用它在数据库中找到的任何内容,并确保将其转换为传递给 save 方法的实体的任何状态。
9.6.1. 对象映射基础
本节涵盖 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不变性的基础知识。请注意,本节仅适用于不使用底层数据存储(如 JPA)的对象映射的 Spring Data 模块。此外,请务必查阅特定于存储的部分以获取特定于存储的对象映射,例如索引、自定义列或字段名称等。
Spring Data 对象映射的核心职责是创建域对象的实例并将存储本机数据结构映射到这些实例上。这意味着我们需要两个基本步骤:
使用公开的构造函数之一创建实例。
实例填充以实现所有公开的属性。
对象创建
Spring Data 会自动尝试检测要用于具体化该类型对象的持久实体的构造函数。解析算法的工作原理如下:
如果只有一个构造函数,则使用它。
如果有多个构造函数并且只有一个用 注释@PersistenceConstructor,则使用它。
如果存在无参数构造函数,则使用它。其他构造函数将被忽略。
值解析假定构造函数参数名称与实体的属性名称匹配,即解析将被执行,就像要填充属性一样,包括映射中的所有自定义(不同的数据存储列或字段名称等)。这还需要类文件中可用的参数名称信息或@ConstructorProperties构造函数中存在的注释。
可以通过使用@Value特定于商店的 SpEL 表达式使用 Spring Framework 的值注释来自定义值解析。有关更多详细信息,请参阅有关商店特定映射的部分。
对象创建内部
为了避免反射的开销,Spring Data 对象创建默认使用运行时生成的工厂类,它会直接调用域类构造函数。即对于此示例类型:
class Person {
Person(String firstname, String lastname) { … }
}
我们将在运行时创建一个语义上等同于这个的工厂类:
class PersonObjectInstantiator implements ObjectInstantiator {
Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
这使我们比反射提高了大约 10% 的性能。对于有资格进行此类优化的域类,它需要遵守一组约束:
它不能是私人课程
它不能是非静态内部类
它不能是 CGLib 代理类
Spring Data 使用的构造函数不能是私有的
如果这些条件中的任何一个匹配,Spring Data 将通过反射回退到实体实例化。
物业人口
一旦创建了实体的实例,Spring Data 就会填充该类的所有剩余持久属性。除非实体的构造函数已经填充(即通过其构造函数参数列表消耗),标识符属性将首先填充以允许循环对象引用的解析。之后,所有尚未由构造函数填充的非瞬态属性都在实体实例上设置。为此,我们使用以下算法:
如果属性是不可变的但公开了一个with…方法(见下文),我们使用该with…方法创建一个具有新属性值的新实体实例。
如果定义了属性访问(即通过 getter 和 setter 访问),我们将调用 setter 方法。
如果属性是可变的,我们直接设置字段。
如果属性是不可变的,我们将使用持久性操作(请参阅对象创建)使用的构造函数来创建实例的副本。
默认情况下,我们直接设置字段值。
财产人口内部
与我们在对象构造中的优化类似,我们也使用 Spring Data 运行时生成的访问器类与实体实例进行交互。
class Person {
private final Long id;
private String firstname;
private @AccessType(Type.PROPERTY) String lastname;
Person() {
this.id = null;
}
Person(Long id, String firstname, String lastname) {
// Field assignments
}
Person withId(Long id) {
return new Person(id, this.firstname, this.lastame);
}
void setLastname(String lastname) {
this.lastname = lastname;
}
}
示例 55. 生成的属性访问器
class PersonPropertyAccessor implements PersistentPropertyAccessor {
private static final MethodHandle firstname;
private Person person;
public void setProperty(PersistentProperty property, Object value) {
String name = property.getName();
if ("firstname".equals(name)) {
firstname.invoke(person, (String) value);
} else if ("id".equals(name)) {
this.person = person.withId((Long) value);
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value);
}
}
}
PropertyAccessor 持有底层对象的可变实例。这是为了启用其他不可变属性的突变。
默认情况下,Spring Data 使用字段访问来读取和写入属性值。根据private字段的可见性规则,MethodHandles用于与字段进行交互。
该类公开了一个withId(…)用于设置标识符的方法,例如,当一个实例插入到数据存储中并生成一个标识符时。调用withId(…)创建一个新Person对象。所有后续突变都将发生在新实例中,而前一个实例保持不变。
使用属性访问允许直接方法调用而不使用MethodHandles.
这使我们比反射提高了大约 25% 的性能。对于有资格进行此类优化的域类,它需要遵守一组约束:
类型不得位于默认值或java包下。
类型及其构造函数必须是 public
属于内部类的类型必须是static.
使用的 Java 运行时必须允许在原始ClassLoader. Java 9 和更新版本施加了某些限制。
默认情况下,Spring Data 会尝试使用生成的属性访问器,并在检测到限制时回退到基于反射的访问器。
内容提示:本文(Spring Data JDBC参考文档)未完待续......