Spring中的数据访问:JDBC、R2DBC、ORM、Object-XML
Spring 框架提供了多种方法来进行数据访问,本文将依次介绍JDBC、R2DBC、ORM、Object-XML等不同的数据访问方式。
# 选择 JDBC 进行数据访问
你可以选择几种方法来作为 JDBC 数据库访问的基础。除了三种 JdbcTemplate
风格之外,SimpleJdbcInsert
和 SimpleJdbcCall
方法优化了数据库元数据,而 RDBMS 对象风格则产生了一种更面向对象的方法。一旦你开始使用这些方法中的一种,你仍然可以混合搭配以包含来自不同方法的功能。
JdbcTemplate
是经典的也是最流行的 Spring JDBC 方法。这种“最低级别”的方法和所有其他方法都在底层使用JdbcTemplate
。NamedParameterJdbcTemplate
包装了一个JdbcTemplate
,以提供命名参数而不是传统的 JDBC?
占位符。当 SQL 语句有多个参数时,此方法提供了更好的文档和易用性。SimpleJdbcInsert
和SimpleJdbcCall
优化了数据库元数据,以限制必要的配置量。这种方法简化了编码,因此你只需要提供表或过程的名称以及与列名匹配的参数映射。这仅在数据库提供足够的元数据时才有效。如果数据库未提供此元数据,则必须提供参数的显式配置。- RDBMS 对象(包括
MappingSqlQuery
、SqlUpdate
和StoredProcedure
)要求你在数据访问层初始化期间创建可重用且线程安全的对象。此方法允许你定义查询字符串、声明参数并编译查询。完成这些操作后,可以使用各种参数值多次调用execute(…)
、update(…)
和findObject(…)
方法。
# 包层次结构
Spring Framework 的 JDBC 抽象框架由四个不同的包组成:
core
:org.springframework.jdbc.core
包包含JdbcTemplate
类及其各种回调接口,以及各种相关类。一个名为org.springframework.jdbc.core.simple
的子包包含SimpleJdbcInsert
和SimpleJdbcCall
类。另一个名为org.springframework.jdbc.core.namedparam
的子包包含NamedParameterJdbcTemplate
类和相关的支持类。datasource
:org.springframework.jdbc.datasource
包包含一个用于轻松访问DataSource
的实用程序类和各种简单的DataSource
实现,你可以使用它们来测试和在 Jakarta EE 容器外部运行未修改的 JDBC 代码。一个名为org.springframework.jdbc.datasource.embedded
的子包提供了通过使用 Java 数据库引擎(如 HSQL、H2 和 Derby)创建嵌入式数据库的支持。object
:org.springframework.jdbc.object
包包含将 RDBMS 查询、更新和存储过程表示为线程安全的可重用对象的类。这种风格产生了一种更面向对象的方法,尽管查询返回的对象自然与数据库断开连接。这种更高级别的 JDBC 抽象依赖于org.springframework.jdbc.core
包中的较低级别的抽象。support
:org.springframework.jdbc.support
包提供SQLException
转换功能和一些实用程序类。JDBC 处理期间引发的异常将转换为org.springframework.dao
包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换后的异常都是未经检查的,这使你可以选择捕获可以从中恢复的异常,同时让其他异常传播给调用者。
# 使用 JDBC 核心类控制基本 JDBC 处理和错误处理
本节介绍如何使用 JDBC 核心类来控制基本 JDBC 处理,包括错误处理。
# 使用 JdbcTemplate
JdbcTemplate
是 JDBC 核心包中的中心类。它处理资源的创建和释放,这有助于你避免常见错误,例如忘记关闭连接。它执行核心 JDBC 工作流程的基本任务(例如语句创建和执行),让应用程序代码提供 SQL 并提取结果。JdbcTemplate
类:
- 运行 SQL 查询
- 更新语句和存储过程调用
- 执行对
ResultSet
实例的迭代并提取返回的参数值。 - 捕获 JDBC 异常并将它们转换为在
org.springframework.dao
包中定义的通用、更具信息性的异常层次结构。
当你为代码使用 JdbcTemplate
时,你只需要实现回调接口,为它们提供明确定义的约定。给定 JdbcTemplate
类提供的 Connection
,PreparedStatementCreator
回调接口创建一个预准备语句,提供 SQL 和任何必要的参数。CallableStatementCreator
接口也是如此,它创建可调用语句。RowCallbackHandler
接口从 ResultSet
的每一行中提取值。
你可以通过使用 DataSource
引用直接实例化在 DAO 实现中使用 JdbcTemplate
,或者你可以在 Spring IoC 容器中配置它并将其作为 bean 引用提供给 DAO。
DataSource
应该始终配置为 Spring IoC 容器中的 bean。在第一种情况下,bean 直接提供给服务;在第二种情况下,它提供给预准备模板。
此类发出的所有 SQL 都以 DEBUG
级别记录在对应于模板实例的完全限定类名的类别下(通常为 JdbcTemplate
,但如果你使用 JdbcTemplate
类的自定义子类,则可能不同)。
以下各节提供了一些 JdbcTemplate
用法的示例。这些示例并非 JdbcTemplate
公开的所有功能的详尽列表。有关更多信息,请参阅相关的 javadoc (opens new window)。
# 查询 (SELECT
)
以下查询获取关系中的行数:
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
以下查询使用绑定变量:
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
以下查询查找 String
:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
以下查询查找并填充单个域对象:
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
},
1212L);
在这个例子中,使用 lambda 表达式来映射 resultSet
中的数据到 Actor
对象。resultSet
是查询结果集,rowNum
是当前行号。lambda 表达式返回一个新的 Actor
对象,该对象填充了 resultSet
中 first_name
和 last_name
列的值。1212L
是查询中 id
参数的值。
以下查询查找并填充域对象列表:
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
在这个例子中,使用 lambda 表达式来映射 resultSet
中的数据到 Actor
对象。resultSet
是查询结果集,rowNum
是当前行号。lambda 表达式返回一个新的 Actor
对象,该对象填充了 resultSet
中 first_name
和 last_name
列的值。
如果最后两个代码片段实际存在于同一应用程序中,则将两个 RowMapper
lambda 表达式中存在的重复项删除并将它们提取到单个字段中,然后 DAO 方法可以根据需要引用它们是有意义的。例如,最好将前面的代码段编写如下:
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}
在这个例子中,actorRowMapper
是一个 RowMapper
类型的字段,它使用 lambda 表达式来映射 resultSet
中的数据到 Actor
对象。resultSet
是查询结果集,rowNum
是当前行号。lambda 表达式返回一个新的 Actor
对象,该对象填充了 resultSet
中 first_name
和 last_name
列的值。findAllActors
方法使用 jdbcTemplate.query
方法来执行查询,并将 actorRowMapper
作为参数传递,以便将查询结果映射到 Actor
对象。
# 使用 JdbcTemplate
更新 (INSERT
、UPDATE
和 DELETE
)
你可以使用 update(..)
方法来执行插入、更新和删除操作。参数值通常作为可变参数提供,或者作为对象数组提供。
以下示例插入一个新条目:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
以下示例更新现有条目:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
以下示例删除一个条目:
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
# 其他 JdbcTemplate
操作
你可以使用 execute(..)
方法来运行任何任意 SQL。因此,该方法通常用于 DDL 语句。它被大量重载,其中包含采用回调接口、绑定变量数组等的变体。以下示例创建一个表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
以下示例调用一个存储过程:
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
# JdbcTemplate
最佳实践
JdbcTemplate
类的实例在配置后是线程安全的。这很重要,因为它意味着你可以配置 JdbcTemplate
的单个实例,然后安全地将此共享引用注入到多个 DAO(或存储库)中。JdbcTemplate
是有状态的,因为它维护对 DataSource
的引用,但此状态不是会话状态。
使用 JdbcTemplate
类的常见做法是在你的 Spring 配置文件中配置一个 DataSource
,然后将该共享 DataSource
bean 依赖注入到你的 DAO 类中。JdbcTemplate
是在 DataSource
的 setter 中或在构造函数中创建的。这导致 DAO 类似于以下内容:
public class JdbcCorporateEventDao implements CorporateEventDao {
private final JdbcTemplate jdbcTemplate;
public JdbcCorporateEventDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
以下示例显示了相应的配置:
Java
@Bean JdbcCorporateEventDao corporateEventDao(DataSource dataSource) { return new JdbcCorporateEventDao(dataSource); } @Bean(destroyMethod = "close") BasicDataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword(""); return dataSource; }
Xml
<bean id="corporateEventDao" class="org.example.jdbc.JdbcCorporateEventDao"> <constructor-arg ref="dataSource"/> </bean> <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="jdbc.properties"/>
显式配置的替代方法是使用组件扫描和注解支持进行依赖注入。在这种情况下,你可以使用 @Repository
注解类(这使其成为组件扫描的候选对象)。以下示例显示了如何执行此操作:
@Repository
public class JdbcCorporateEventRepository implements CorporateEventRepository {
private JdbcTemplate jdbcTemplate;
// Implicitly autowire the DataSource constructor parameter
public JdbcCorporateEventRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventRepository follow...
}
以下示例显示了相应的配置:
Java
@Configuration @ComponentScan("org.example.jdbc") public class JdbcCorporateEventRepositoryConfiguration { @Bean(destroyMethod = "close") BasicDataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword(""); return dataSource; } }
Xml
<!-- Scans within the base package of the application for @Component classes to configure as beans --> <context:component-scan base-package="org.example.jdbc" /> <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="jdbc.properties"/>
如果你使用 Spring 的 JdbcDaoSupport
类并且你的各种 JDBC 支持的 DAO 类从中扩展,你的子类会从 JdbcDaoSupport
类继承一个 setDataSource(..)
方法。你可以选择是否从这个类继承。JdbcDaoSupport
类仅作为一种便利提供。
无论你选择使用哪种上述模板初始化样式(或不使用),都很少需要在每次要运行 SQL 时都创建一个 JdbcTemplate
类的新实例。配置完成后,JdbcTemplate
实例是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个 JdbcTemplate
实例,这需要多个 DataSources
,随后需要多个不同配置的 JdbcTemplate
实例。
# 使用 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate
类增加了对使用命名参数编程 JDBC 语句的支持,而不是仅使用经典占位符('?'
)参数编程 JDBC 语句。NamedParameterJdbcTemplate
类包装了一个 JdbcTemplate
,并委托给包装的 JdbcTemplate
来完成其大部分工作。本节仅描述 NamedParameterJdbcTemplate
类中与 JdbcTemplate
本身不同的那些区域,即使用命名参数编程 JDBC 语句。以下示例显示了如何使用 NamedParameterJdbcTemplate
:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
请注意在分配给 sql
变量的值中使用了命名参数表示法,以及插入到 namedParameters
变量(类型为 MapSqlParameterSource
)中的相应值。
或者,你可以通过使用基于 Map
的样式将命名参数及其对应的值传递给 NamedParameterJdbcTemplate
实例。NamedParameterJdbcOperations
公开的其余方法以及 NamedParameterJdbcTemplate
类实现的方法遵循类似的模式,此处不再赘述。
以下示例显示了基于 Map
的样式的使用:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
与 NamedParameterJdbcTemplate
相关的一个不错的功能(并且存在于同一个 Java 包中)是 SqlParameterSource
接口。你已经在前面的代码片段之一中看到了此接口的实现的示例(MapSqlParameterSource
类)。SqlParameterSource
是 NamedParameterJdbcTemplate
的命名参数值的来源。MapSqlParameterSource
类是一个简单的实现,它是围绕 java.util.Map
的适配器,其中键是参数名称,值是参数值。
另一个 SqlParameterSource
实现是 BeanPropertySqlParameterSource
类。此类包装了一个任意 JavaBean(即,一个符合 JavaBean 约定 (opens new window) 的类的实例),并将包装的 JavaBean 的属性用作命名参数值的来源。
以下示例显示了一个典型的 JavaBean:
public class Actor {
private Long id;
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public Long getId() {
return this.id;
}
// setters omitted...
}
以下示例使用 NamedParameterJdbcTemplate
返回上例中显示的类的成员计数:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActors(Actor exampleActor) {
// notice how the named parameters match the properties of the above 'Actor' class
String sql = "select count(*) from t_actor where first_name = :firstName and last_name = :lastName";
SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
请记住,NamedParameterJdbcTemplate
类包装了一个经典的 JdbcTemplate
模板。如果你需要访问包装的 JdbcTemplate
实例以访问仅存在于 JdbcTemplate
类中的功能,你可以使用 getJdbcOperations()
方法通过 JdbcOperations
接口访问包装的 JdbcTemplate
。
# 统一 JDBC 查询/更新操作:JdbcClient
从 6.1 开始,NamedParameterJdbcTemplate
的命名参数语句和常规 JdbcTemplate
的位置参数语句可以通过统一的客户端 API 使用流畅的交互模型。
例如,使用位置参数:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?")
.param(firstName)
.query(Integer.class).single();
}
例如,使用命名参数:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName")
.param("firstName", firstName)
.query(Integer.class).single();
}
RowMapper
功能也可用,具有灵活的结果解析:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name")))
.list();
除了自定义 RowMapper
之外,你还可以指定要映射到的类。例如,假设 Actor
具有 firstName
和 lastName
属性作为记录类、自定义构造函数、bean 属性或普通字段:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query(Actor.class)
.list();
使用所需的单个对象结果:
Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.single();
使用 java.util.Optional
结果:
Optional<Actor> actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.optional();
对于更新语句:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)")
.param("Leonor").param("Watling")
.update();
或者带有命名参数的更新语句:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.param("firstName", "Leonor").param("lastName", "Watling")
.update();
除了单独的命名参数之外,你还可以指定一个参数源对象——例如,一个记录类,一个带有 bean 属性的类,或者一个提供 firstName
和 lastName
属性的普通字段持有者,例如上面的 Actor
类:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.paramSource(new Actor("Leonor", "Watling")
.update();
上面参数以及查询结果的自动 Actor
类映射是通过隐式 SimplePropertySqlParameterSource
和 SimplePropertyRowMapper
策略提供的,这些策略也可用于直接使用。它们可以作为 BeanPropertySqlParameterSource
和 BeanPropertyRowMapper
/DataClassRowMapper
的常见替代品,也可以与 JdbcTemplate
和 NamedParameterJdbcTemplate
本身一起使用。
JdbcClient
是 JDBC 查询/更新语句的灵活但简化的外观。批量插入和存储过程调用等高级功能通常需要额外的自定义:对于 JdbcClient
中不可用的任何此类功能,请考虑 Spring 的 SimpleJdbcInsert
和 SimpleJdbcCall
类或普通的直接 JdbcTemplate
用法。
# 使用 SQLExceptionTranslator
SQLExceptionTranslator
是一个接口,由可以在 SQLException
和 Spring 自己的 org.springframework.dao.DataAccessException
之间进行转换的类实现,后者与数据访问策略无关。实现可以是通用的(例如,使用 JDBC 的 SQLState 代码)或专有的(例如,使用 Oracle 错误代码)以获得更高的精度。此异常转换机制在不传播 SQLException
而是 DataAccessException
的常见 JdbcTemplate
和 JdbcTransactionManager
入口点后面使用。
从 6.0 开始,默认的异常转换器是 SQLExceptionSubclassTranslator
,它检测 JDBC 4 SQLException
子类并进行一些额外的检查,并通过 SQLStateSQLExceptionTranslator
回退到 SQLState
内省。这通常足以进行常见的数据库访问,并且不需要特定于供应商的检测。为了向后兼容性,请考虑使用下面描述的 SQLErrorCodeSQLExceptionTranslator
,可能使用自定义错误代码映射。
当类路径的根目录中存在名为 sql-error-codes.xml
的文件时,SQLErrorCodeSQLExceptionTranslator
是默认使用的 SQLExceptionTranslator
的实现。此实现使用特定的供应商代码。它比 SQLState
或 SQLException
子类转换更精确。错误代码转换基于保存在名为 SQLErrorCodes
的 JavaBean 类型类中的代码。此类由 SQLErrorCodesFactory
创建和填充,后者(顾名思义)是一个基于名为 sql-error-codes.xml
的配置文件的内容创建 SQLErrorCodes
的工厂。此文件填充了供应商代码,并基于从 DatabaseMetaData
中获取的 DatabaseProductName
。使用你正在使用的实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator
按以下顺序应用匹配规则:
- 子类实现的任何自定义转换。通常,使用提供的具体
SQLErrorCodeSQLExceptionTranslator
,因此此规则不适用。仅当你实际提供了子类实现时才适用。 - 作为
SQLErrorCodes
类的customSqlExceptionTranslator
属性提供的SQLExceptionTranslator
接口的任何自定义实现。 - 搜索
CustomSQLErrorCodesTranslation
类的实例列表(为SQLErrorCodes
类的customTranslations
属性提供)以查找匹配项。 - 应用错误代码匹配。
- 使用回退转换器。
SQLExceptionSubclassTranslator
是默认的回退转换器。如果此转换不可用,则下一个回退转换器是SQLStateSQLExceptionTranslator
。
默认情况下,SQLErrorCodesFactory
用于定义错误代码和自定义异常转换。它们在类路径中名为 sql-error-codes.xml
的文件中查找,并且基于正在使用的数据库的数据库元数据中的数据库名称来定位匹配的 SQLErrorCodes
实例。
你可以扩展 SQLErrorCodeSQLExceptionTranslator
,如以下示例所示:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
if (sqlEx.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlEx);
}
return null;
}
}
在前面的示例中,特定错误代码 (-12345
) 被转换,而其他错误留给默认转换器实现来转换。要使用此自定义转换器,你必须通过方法 setExceptionTranslator
将其传递给 JdbcTemplate
,并且你必须将此 JdbcTemplate
用于需要此转换器的所有数据访问处理。以下示例显示了如何使用此自定义转换器:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
自定义转换器被传递一个数据源,以便在 sql-error-codes.xml
中查找错误代码。
# 运行语句
运行 SQL 语句只需要很少的代码。你需要一个 DataSource
和一个 JdbcTemplate
,包括 JdbcTemplate
提供的便利方法。以下示例显示了创建一个新表的最小但功能齐全的类需要包含的内容:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAStatement {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void doExecute() {
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
}
}
# 运行查询
某些查询方法返回单个值。要从一行中检索计数或特定值,请使用 queryForObject(..)
。后者将返回的 JDBC Type
转换为作为参数传入的 Java 类。如果类型转换无效,则会抛出 InvalidDataAccessApiUsageException
。以下示例包含两个查询方法,一个用于 int
,另一个用于查询 String
:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class RunAQuery {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int getCount() {
return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
}
public String getName() {
return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
}
}
除了单个结果查询方法之外,还有几种方法返回一个列表,其中查询返回的每一行都有一个条目。最通用的方法是 queryForList(..)
,它返回一个 List
,其中每个元素都是一个 Map
,其中包含每列的一个条目,使用列名作为键。如果向前面的示例添加一个方法来检索所有行的列表,则它可能如下所示:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getList() {
return this.jdbcTemplate.queryForList("select * from mytable");
}
返回的列表将类似于以下内容:
[{name=Bob, id=1}, {name=Mary, id=2}]
# 更新数据库
以下示例更新特定主键的列:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAnUpdate {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void setName(int id, String name) {
this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
}
}
在前面的示例中,SQL 语句具有行参数的占位符。你可以将参数值作为 varargs 传入,或者作为对象数组传入。因此,你应该显式地将原始类型包装在原始包装类中,或者你应该使用自动装箱。
# 检索自动生成的键
update()
便利方法支持检索数据库生成的主键。此支持是 JDBC 3.0 标准的一部分。有关详细信息,请参阅规范的第 13.6 章。该方法将 PreparedStatementCreator
作为其第一个参数,这是指定所需插入语句的方式。另一个参数是 KeyHolder
,它包含从更新成功返回后生成的键。没有创建适当的 PreparedStatement
的标准方法(这解释了为什么方法签名是这样的)。以下示例在 Oracle 上有效,但在其他平台上可能无效:
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
ps.setString(1, name);
return ps;
}, keyHolder);
// keyHolder.getKey() now contains the generated key
# 控制数据库连接
# 使用 DataSource
Spring 通过 DataSource
获取数据库连接。DataSource
是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架向应用程序代码隐藏连接池和事务管理问题。作为开发人员,您无需了解如何连接到数据库的详细信息。这是管理员的责任,由管理员设置数据源。在开发和测试代码时,您很可能同时扮演这两个角色,但您不一定需要知道生产数据源是如何配置的。
当您使用 Spring 的 JDBC 层时,您可以从 JNDI 获取数据源,也可以使用第三方提供的连接池实现来配置自己的数据源。传统的选择是带有 Bean 风格 DataSource
类的 Apache Commons DBCP 和 C3P0;对于现代 JDBC 连接池,请考虑使用 HikariCP 及其构建器风格的 API。
注意: 您应该仅将
DriverManagerDataSource
和SimpleDriverDataSource
类(包含在 Spring 发行版中)用于测试目的!这些变体不提供池化,并且在发出多个连接请求时性能不佳。
以下部分使用 Spring 的 DriverManagerDataSource
实现。稍后将介绍其他几个 DataSource
变体。
要配置 DriverManagerDataSource
,请执行以下操作:
- 像通常获取 JDBC 连接一样,使用
DriverManagerDataSource
获取连接。 - 指定 JDBC 驱动程序的完全限定类名,以便
DriverManager
可以加载驱动程序类。 - 提供一个 URL,该 URL 在 JDBC 驱动程序之间有所不同。(有关正确的值,请参阅驱动程序的文档。)
- 提供用户名和密码以连接到数据库。
以下示例展示了如何配置 DriverManagerDataSource
:
@Bean
DriverManagerDataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
接下来的两个示例展示了 DBCP 和 C3P0 的基本连接和配置。要了解有关有助于控制池化功能的更多选项,请参阅相应连接池实现的文档。
以下示例显示 DBCP 配置:
@Bean(destroyMethod = "close")
BasicDataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
以下示例显示 C3P0 配置:
@Bean(destroyMethod = "close")
ComboPooledDataSource dataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("org.hsqldb.jdbcDriver");
dataSource.setJdbcUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUser("sa");
dataSource.setPassword("");
return dataSource;
}
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
# 使用 DataSourceUtils
DataSourceUtils
类是一个方便而强大的帮助类,它提供 static
方法来从 JNDI 获取连接并在必要时关闭连接。它支持带有 DataSourceTransactionManager
的线程绑定 JDBC Connection
,但也支持 JtaTransactionManager
和 JpaTransactionManager
。
请注意,JdbcTemplate
隐含 DataSourceUtils
连接访问,在每个 JDBC 操作背后使用它,隐式地参与正在进行的事务。
# 实现 SmartDataSource
SmartDataSource
接口应由可以提供与关系数据库连接的类来实现。它扩展了 DataSource
接口,以允许使用它的类查询连接是否应在给定操作后关闭。当您知道需要重用连接时,此用法非常有效。
# 扩展 AbstractDataSource
AbstractDataSource
是 Spring 的 DataSource
实现的 abstract
基类。它实现了所有 DataSource
实现通用的代码。如果您编写自己的 DataSource
实现,则应扩展 AbstractDataSource
类。
# 使用 SingleConnectionDataSource
SingleConnectionDataSource
类是 SmartDataSource
接口的实现,它包装了一个在每次使用后都不会关闭的单个 Connection
。它不具备多线程能力。
如果任何客户端代码在假定池化连接的情况下调用 close
(例如使用持久性工具时),则应将 suppressClose
属性设置为 true
。此设置返回一个关闭抑制代理,该代理包装物理连接。请注意,您不能再将其强制转换为本机 Oracle Connection
或类似对象。
SingleConnectionDataSource
主要是一个测试类。它通常可以方便地在应用程序服务器外部测试代码,并结合简单的 JNDI 环境。与 DriverManagerDataSource
相比,它始终重用相同的连接,从而避免了过多物理连接的创建。
# 使用 DriverManagerDataSource
DriverManagerDataSource
类是标准 DataSource
接口的实现,它通过 Bean 属性配置一个普通的 JDBC 驱动程序,并每次都返回一个新的 Connection
。
此实现对于 Jakarta EE 容器之外的测试和独立环境很有用,既可以作为 Spring IoC 容器中的 DataSource
Bean,也可以与简单的 JNDI 环境结合使用。假定池化的 Connection.close()
调用会关闭连接,因此任何支持 DataSource
的持久性代码都应工作。但是,即使在测试环境中,使用 JavaBean 风格的连接池(例如 commons-dbcp
)也非常容易,因此几乎总是优于使用 DriverManagerDataSource
。
# 使用 TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy
是目标 DataSource
的代理。该代理包装该目标 DataSource
以添加对 Spring 管理的事务的感知。在这方面,它类似于事务性 JNDI DataSource
,由 Jakarta EE 服务器提供。
注意: 很少需要使用此类,除非必须调用已存在的代码并传递标准的 JDBC
DataSource
接口实现。在这种情况下,您仍然可以使用此代码,同时使此代码参与 Spring 管理的事务。通常,最好使用更高级别的资源管理抽象来编写自己的新代码,例如JdbcTemplate
或DataSourceUtils
。
有关更多详细信息,请参见 TransactionAwareDataSourceProxy (opens new window) javadoc。
# 使用 DataSourceTransactionManager
/ JdbcTransactionManager
DataSourceTransactionManager
类是单个 JDBC DataSource
的 PlatformTransactionManager
实现。它将指定 DataSource
中的 JDBC Connection
绑定到当前执行的线程,从而可能允许每个 DataSource
一个线程绑定的 Connection
。
应用程序代码需要通过 DataSourceUtils.getConnection(DataSource)
而不是 Java EE 的标准 DataSource.getConnection
来检索 JDBC Connection
。它抛出未经检查的 org.springframework.dao
异常,而不是经过检查的 SQLExceptions
。所有框架类(例如 JdbcTemplate
)都隐式地使用此策略。如果未与事务管理器一起使用,则查找策略的行为与 DataSource.getConnection
完全相同,因此可以在任何情况下使用。
DataSourceTransactionManager
类支持保存点 (PROPAGATION_NESTED
)、自定义隔离级别和超时,这些超时将作为适当的 JDBC 语句查询超时应用。为了支持后者,应用程序代码必须使用 JdbcTemplate
或为每个创建的语句调用 DataSourceUtils.applyTransactionTimeout(..)
方法。
您可以在单资源情况下使用 DataSourceTransactionManager
而不是 JtaTransactionManager
,因为它不需要容器支持 JTA 事务协调器。在这两个事务管理器之间切换只是配置问题,前提是您坚持所需的连接查找模式。请注意,JTA 不支持保存点或自定义隔离级别,并且具有不同的超时机制,但在 JDBC 资源和 JDBC 提交/回滚管理方面表现出类似的行为。
对于 JTA 风格的实际资源连接的延迟检索,Spring 为目标连接池提供了一个相应的 DataSource
代理类:请参见 LazyConnectionDataSourceProxy (opens new window)。这对于没有实际语句执行的潜在空事务(在这种情况下从不获取实际资源)以及在路由 DataSource
前面特别有用,这意味着要考虑事务同步的只读标志和/或隔离级别(例如,IsolationLevelDataSourceRouter
)。
LazyConnectionDataSourceProxy
还为只读连接池提供特殊支持,以便在只读事务期间使用,从而避免了在从主连接池获取 JDBC 连接时,在每个事务的开始和结束时切换 JDBC 连接的只读标志的开销(这可能因 JDBC 驱动程序而异)。
注意: 从 5.3 开始,Spring 提供了一个扩展的
JdbcTransactionManager
变体,它在提交/回滚时添加了异常转换功能(与JdbcTemplate
对齐)。DataSourceTransactionManager
将只抛出TransactionSystemException
(类似于 JTA),而JdbcTransactionManager
将数据库锁定失败等转换为相应的DataAccessException
子类。请注意,应用程序代码需要为这些异常做好准备,而不仅仅是期望TransactionSystemException
。在这种情况下,建议选择JdbcTransactionManager
。
在异常行为方面,JdbcTransactionManager
大致相当于 JpaTransactionManager
和 R2dbcTransactionManager
,彼此充当直接的同伴/替代品。另一方面,DataSourceTransactionManager
等效于 JtaTransactionManager
,并且可以在那里充当直接替代品。
# JDBC 批量操作
如果对同一预处理语句进行多次调用,则大多数 JDBC 驱动程序都会提供更高的性能。通过将更新分组到批处理中,您可以限制与数据库的往返次数。
# 使用 JdbcTemplate
的基本批量操作
您可以通过实现特殊接口 BatchPreparedStatementSetter
的两个方法,并将该实现作为第二个参数传递到 batchUpdate
方法调用中,来完成 JdbcTemplate
批量处理。您可以使用 getBatchSize
方法来提供当前批处理的大小。您可以使用 setValues
方法来设置预处理语句的参数值。此方法被调用的次数与您在 getBatchSize
调用中指定的次数相同。以下示例根据列表中的条目更新 t_actor
表,整个列表用作批处理:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName()); // 设置 first_name
ps.setString(2, actor.getLastName()); // 设置 last_name
ps.setLong(3, actor.getId().longValue()); // 设置 id
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... 其他方法
}
如果您处理更新流或从文件读取,则可能有一个首选的批处理大小,但最后一个批处理可能没有那么多条目。在这种情况下,您可以使用 InterruptibleBatchPreparedStatementSetter
接口,该接口允许您在输入源耗尽后中断批处理。isBatchExhausted
方法允许您发出批处理结束的信号。
# 使用对象列表的批量操作
JdbcTemplate
和 NamedParameterJdbcTemplate
都提供了一种提供批量更新的替代方法。您无需实现特殊的批处理接口,而是在调用中以列表形式提供所有参数值。框架循环遍历这些值并使用内部预处理语句设置器。API 各不相同,具体取决于您是否使用命名参数。对于命名参数,您提供一个 SqlParameterSource
数组,每个批处理成员一个条目。您可以使用 SqlParameterSourceUtils.createBatch
便利方法来创建此数组,传入 Bean 风格的对象数组(带有与参数对应的 getter 方法)、字符串键控的 Map
实例(包含相应的参数作为值)或两者的混合。
以下示例显示使用命名参数的批量更新:
public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int[] batchUpdate(List<Actor> actors) {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... 其他方法
}
对于使用经典 ?
占位符的 SQL 语句,您传入一个列表,其中包含一个对象数组,其中包含更新值。此对象数组必须为 SQL 语句中的每个占位符包含一个条目,并且它们的顺序必须与在 SQL 语句中定义的顺序相同。
以下示例与前面的示例相同,只不过它使用经典的 JDBC ?
占位符:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
List<Object[]> batch = new ArrayList<>();
for (Actor actor : actors) {
Object[] values = new Object[] {
actor.getFirstName(), actor.getLastName(), actor.getId()};
batch.add(values);
}
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
batch);
}
// ... 其他方法
}
我们前面描述的所有批量更新方法都返回一个 int
数组,其中包含每个批处理条目的受影响行数。此计数由 JDBC 驱动程序报告。如果该计数不可用,则 JDBC 驱动程序将返回值 -2
。
注意: 在这种情况下,在底层
PreparedStatement
上自动设置值,需要从给定的 Java 类型派生每个值的相应 JDBC 类型。虽然这通常效果很好,但存在问题的可能性(例如,包含在 Map 中的null
值)。默认情况下,Spring 在这种情况下调用ParameterMetaData.getParameterType
,这对于您的 JDBC 驱动程序来说可能代价高昂。如果您遇到应用程序的特定性能问题,则应使用最新的驱动程序版本,并考虑将spring.jdbc.getParameterType.ignore
属性设置为true
(作为 JVM 系统属性或通过 SpringProperties (opens new window) 机制)。从 6.1.2 开始,Spring 绕过了 PostgreSQL 和 MS SQL Server 上的默认
getParameterType
解析。这是一种常见的优化,可以避免进一步往返 DBMS 仅仅为了参数类型解析,已知这种优化对 PostgreSQL 和 MS SQL Server 尤其重要,尤其是在批量操作中。如果您碰巧看到副作用,例如,在没有特定类型指示的情况下将字节数组设置为 null,您可以显式设置spring.jdbc.getParameterType.ignore=false
标志作为系统属性(如上所述)以恢复完整的getParameterType
解析。或者,您可以考虑显式指定相应的 JDBC 类型,可以通过
BatchPreparedStatementSetter
(如前所示),通过给基于List<Object[]>
的调用提供显式类型数组,通过在自定义MapSqlParameterSource
实例上调用registerSqlType
,通过从 Java 声明的属性类型派生 SQL 类型的BeanPropertySqlParameterSource
即使对于 null 值,或者通过提供单独的SqlParameterValue
实例而不是普通的 null 值。
# 使用多个批次的批量操作
前面的批量更新示例处理的批次非常大,以至于您想将它们分解为几个较小的批次。您可以通过多次调用 batchUpdate
方法来使用前面提到的方法来做到这一点,但是现在有了一种更方便的方法。除了 SQL 语句之外,此方法还接受一个包含参数的对象的 Collection
,每次批处理要进行的更新次数以及一个 ParameterizedPreparedStatementSetter
来设置预处理语句的参数值。框架循环遍历提供的值,并将更新调用分解为指定大小的批次。
以下示例显示使用批处理大小为 100 的批量更新:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[][] batchUpdate(final Collection<Actor> actors) {
int[][] updateCounts = jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors,
100,
(PreparedStatement ps, Actor actor) -> {
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
});
return updateCounts;
}
// ... 其他方法
}
此调用的批量更新方法返回一个 int
数组的数组,该数组包含每个批处理的数组条目,其中包含每个更新的受影响行数的数组。顶层数组的长度表示运行的批处理数,第二层数组的长度表示该批处理中的更新数。每个批处理中的更新数应为所有批处理提供的批处理大小(除了最后一个批处理可能小于),具体取决于提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的计数。如果该计数不可用,则 JDBC 驱动程序将返回值 -2
。
# 使用 SimpleJdbc
类简化 JDBC 操作
SimpleJdbcInsert
和 SimpleJdbcCall
类通过利用可以通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。 这意味着您需要预先配置的内容更少,尽管如果您希望在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。
# 使用 SimpleJdbcInsert
插入数据
我们首先来看一下具有最少配置选项的 SimpleJdbcInsert
类。 您应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert
。 对于此示例,初始化方法是 setDataSource
方法。 您无需对 SimpleJdbcInsert
类进行子类化。 而是,您可以创建一个新实例,并使用 withTableName
方法设置表名。 此类的配置方法遵循返回 SimpleJdbcInsert
实例的 fluid
样式,从而可以链接所有配置方法。 以下示例仅使用一种配置方法(稍后我们将展示多个方法的示例):
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(3);
parameters.put("id", actor.getId()); // 设置 id
parameters.put("first_name", actor.getFirstName()); // 设置 first_name
parameters.put("last_name", actor.getLastName()); // 设置 last_name
insertActor.execute(parameters);
}
// ... 其他方法
}
此处使用的 execute
方法采用普通的 java.util.Map
作为其唯一参数。 这里要注意的重要一点是,用于 Map
的键必须与数据库中定义的表的列名匹配。 这是因为我们读取元数据以构造实际的插入语句。
# 使用 SimpleJdbcInsert
检索自动生成的键
下一个示例使用与前一个示例相同的插入,但是,它不是传入 id
,而是检索自动生成的键并将其设置在新 Actor
对象上。 当它创建 SimpleJdbcInsert
时,除了指定表名之外,它还使用 usingGeneratedKeyColumns
方法指定生成的键列的名称。 以下清单显示了它的工作方式:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("first_name", actor.getFirstName()); // 设置 first_name
parameters.put("last_name", actor.getLastName()); // 设置 last_name
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... 其他方法
}
使用第二种方法运行插入时的主要区别在于,您不会将 id
添加到 Map
,而是调用 executeAndReturnKey
方法。 这将返回一个 java.lang.Number
对象,您可以使用该对象创建域类中使用的数值类型的实例。 您不能依赖所有数据库在此处返回特定的 Java 类。 java.lang.Number
是您可以依赖的基类。 如果您有多个自动生成的列,或者生成的值是非数字的,则可以使用从 executeAndReturnKeyHolder
方法返回的 KeyHolder
。
# 为 SimpleJdbcInsert
指定列
您可以通过使用 usingColumns
方法指定列名列表来限制插入的列,如以下示例所示:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("first_name", actor.getFirstName()); // 设置 first_name
parameters.put("last_name", actor.getLastName()); // 设置 last_name
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... 其他方法
}
插入的执行与您依靠元数据来确定要使用的列时相同。
# 使用 SqlParameterSource
提供参数值
使用 Map
来提供参数值可以正常工作,但它不是最方便使用的类。 Spring 提供了 SqlParameterSource
接口的几个实现,您可以改用它们。 第一个是 BeanPropertySqlParameterSource
,如果您有一个包含值的符合 JavaBean 规范的类,这是一个非常方便的类。 它使用相应的 getter 方法来提取参数值。 以下示例显示如何使用 BeanPropertySqlParameterSource
:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... 其他方法
}
另一种选择是 MapSqlParameterSource
,它类似于 Map
,但提供了一个更方便的 addValue
方法,可以链接该方法。 以下示例显示如何使用它:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", actor.getFirstName()) // 设置 first_name
.addValue("last_name", actor.getLastName()); // 设置 last_name
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... 其他方法
}
如您所见,配置是相同的。 只有执行代码必须更改才能使用这些替代输入类。
# 使用 SimpleJdbcCall
调用存储过程
SimpleJdbcCall
类使用数据库中的元数据来查找 in
和 out
参数的名称,因此您不必显式声明它们。 如果您愿意这样做,或者如果您有未自动映射到 Java 类的参数,则可以声明参数。 第一个示例显示了一个简单的过程,该过程仅从 MySQL 数据库以 VARCHAR
和 DATE
格式返回标量值。 示例过程读取指定的 actor 条目,并以 out
参数的形式返回 first_name
、last_name
和 birth_date
列。 以下清单显示了第一个示例:
CREATE PROCEDURE read_actor (
IN in_id INTEGER,
OUT out_first_name VARCHAR(100),
OUT out_last_name VARCHAR(100),
OUT out_birth_date DATE)
BEGIN
SELECT first_name, last_name, birth_date
INTO out_first_name, out_last_name, out_birth_date
FROM t_actor where id = in_id;
END;
in_id
参数包含您要查找的 actor 的 id
。 out
参数返回从表中读取的数据。
您可以以类似于声明 SimpleJdbcInsert
的方式声明 SimpleJdbcCall
。 您应该在数据访问层的初始化方法中实例化和配置该类。 与 StoredProcedure
类相比,您无需创建子类,也无需声明可以在数据库元数据中查找的参数。 以下 SimpleJdbcCall
配置示例使用前面的存储过程(除了 DataSource
之外,唯一的配置选项是存储过程的名称):
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
this.procReadActor = new SimpleJdbcCall(dataSource)
.withProcedureName("read_actor");
}
public Actor readActor(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id); // 设置 in_id
Map out = procReadActor.execute(in);
Actor actor = new Actor();
actor.setId(id);
actor.setFirstName((String) out.get("out_first_name")); // 获取 out_first_name
actor.setLastName((String) out.get("out_last_name")); // 获取 out_last_name
actor.setBirthDate((Date) out.get("out_birth_date")); // 获取 out_birth_date
return actor;
}
// ... 其他方法
}
您为执行调用编写的代码涉及创建一个包含 IN 参数的 SqlParameterSource
。 您必须将为输入值提供的名称与存储过程中声明的参数名称相匹配。 大小写不必匹配,因为您使用元数据来确定如何在存储过程中引用数据库对象。 存储过程的源代码中指定的内容不一定是它存储在数据库中的方式。 某些数据库将名称转换为全部大写,而另一些数据库使用小写或使用指定的大小写。
execute
方法采用 IN 参数并返回一个 Map
,其中包含任何 out
参数,这些参数由名称键入,如存储过程中指定的那样。 在这种情况下,它们是 out_first_name
、out_last_name
和 out_birth_date
。
execute
方法的最后一部分创建一个 Actor
实例以用于返回检索到的数据。 同样,重要的是使用 out
参数的名称,因为它们在存储过程中声明。 此外,存储在结果映射中的 out
参数名称的大小写与数据库中 out
参数名称的大小写相匹配,这可能因数据库而异。 为了使您的代码更具可移植性,您应该执行不区分大小写的查找或指示 Spring 使用 LinkedCaseInsensitiveMap
。 要执行后者,您可以创建自己的 JdbcTemplate
并将 setResultsMapCaseInsensitive
属性设置为 true
。 然后,您可以将此自定义 JdbcTemplate
实例传递到 SimpleJdbcCall
的构造函数中。 以下示例显示了此配置:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor");
}
// ... 其他方法
}
通过执行此操作,您可以避免在用于返回的 out
参数名称的情况下发生冲突。
# 显式声明要用于 SimpleJdbcCall
的参数
在本章的前面,我们描述了如何从元数据中推断参数,但如果您愿意,可以显式声明它们。 您可以通过使用 declareParameters
方法创建和配置 SimpleJdbcCall
来执行此操作,该方法将可变数量的 SqlParameter
对象作为输入。
注意: 如果您使用的数据库不是 Spring 支持的数据库,则必须进行显式声明。 目前,Spring 支持以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。 我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数的元数据查找。
您可以选择显式声明一个、一些或所有参数。 如果您未显式声明参数,则仍使用参数元数据。 要绕过对潜在参数的所有元数据查找处理并仅使用声明的参数,您可以调用方法 withoutProcedureColumnMetaDataAccess
作为声明的一部分。 假设您为数据库函数声明了两个或多个不同的调用签名。 在这种情况下,您可以调用 useInParameterNames
来指定要为给定签名包括的 IN 参数名称列表。
以下示例显示了一个完全声明的过程调用,并使用了前面示例中的信息:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
new SqlOutParameter("out_last_name", Types.VARCHAR),
new SqlOutParameter("out_birth_date", Types.DATE)
);
}
// ... 其他方法
}
这两个示例的执行和最终结果是相同的。 第二个示例显式指定所有详细信息,而不是依赖元数据。
# 如何定义 SqlParameters
要为 SimpleJdbc
类以及 RDBMS 操作类定义参数,您可以使用 SqlParameter
或其子类之一。 为此,您通常在构造函数中指定参数名称和 SQL 类型。 SQL 类型通过使用 java.sql.Types
常量指定。 在本章的前面,我们看到了类似于以下的声明:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
带有 SqlParameter
的第一行声明了一个 IN 参数。 您可以使用 IN 参数进行存储过程调用和通过使用 SqlQuery
及其子类进行查询。
带有 SqlOutParameter
的第二行声明了一个 out
参数,该参数用于存储过程调用。 还有一个 SqlInOutParameter
用于 InOut
参数(向过程提供 IN 值并且还返回值)。
注意: 只有声明为
SqlParameter
和SqlInOutParameter
的参数才用于提供输入值。 这与StoredProcedure
类不同,后者(出于向后兼容的原因)允许为声明为SqlOutParameter
的参数提供输入值。
对于 IN 参数,除了名称和 SQL 类型之外,您还可以为数值数据指定比例,或为自定义数据库类型指定类型名称。 对于 out
参数,您可以提供一个 RowMapper
来处理从 REF
游标返回的行的映射。 另一种选择是指定一个 SqlReturnType
,它提供了一个定义自定义处理返回值机会。
# 使用 SimpleJdbcCall
调用存储函数
您可以调用存储函数,其方式几乎与调用存储过程相同,不同之处在于您提供函数名称而不是过程名称。 您可以使用 withFunctionName
方法作为配置的一部分,以指示您要调用函数,并且会生成函数调用的相应字符串。 专用调用 (executeFunction
) 用于运行函数,它将函数返回值作为指定类型的对象返回,这意味着您不必从结果映射中检索返回值。 类似的便捷方法(名为 executeObject
)也可用于仅具有一个 out
参数的存储过程。 以下示例(对于 MySQL)基于一个名为 get_actor_name
的存储函数,该函数返回 actor 的全名:
CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
DECLARE out_name VARCHAR(200);
SELECT concat(first_name, ' ', last_name)
INTO out_name
FROM t_actor where id = in_id;
RETURN out_name;
END;
要调用此函数,我们再次在初始化方法中创建一个 SimpleJdbcCall
,如以下示例所示:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name");
}
public String getActorName(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
String name = funcGetActorName.executeFunction(String.class, in);
return name;
}
// ... 其他方法
}
使用的 executeFunction
方法返回一个 String
,其中包含函数调用的返回值。
# 从 SimpleJdbcCall
返回 ResultSet
或 REF 游标
调用返回结果集的存储过程或函数有点棘手。 某些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则需要显式注册特定类型的 out
参数。 两种方法都需要额外的处理来循环访问结果集并处理返回的行。 使用 SimpleJdbcCall
,您可以使用 returningResultSet
方法并声明一个 RowMapper
实现以用于特定参数。 如果在结果处理期间返回结果集,则未定义任何名称,因此返回的结果必须与您声明 RowMapper
实现的顺序相匹配。 指定的名称仍用于将处理后的结果列表存储在从 execute
语句返回的结果映射中。
下一个示例(对于 MySQL)使用一个不带 IN 参数并返回 t_actor
表中所有行的存储过程:
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
要调用此过程,您可以声明 RowMapper
。 因为您要映射到的类遵循 JavaBean 规则,所以您可以使用通过在 newInstance
方法中传入要映射的必需类来创建的 BeanPropertyRowMapper
。 以下示例显示了如何执行此操作:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadAllActors;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor.class));
}
public List getActorsList() {
Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
return (List) m.get("actors");
}
// ... 其他方法
}
execute
调用传入一个空 Map
,因为此调用不带任何参数。 然后从结果映射中检索 actor 列表并将其返回给调用方。
# 将 JDBC 操作建模为 Java 对象
org.springframework.jdbc.object
包包含一些类,使你可以以更面向对象的方式访问数据库。 例如,你可以运行查询并将结果作为列表返回,其中包含业务对象,关系列数据映射到业务对象的属性。 你还可以运行存储过程并运行更新、删除和插入语句。
注意: 许多 Spring 开发者认为,下面描述的各种 RDBMS 操作类(除了 StoredProcedure
类)通常可以用直接的 JdbcTemplate
调用代替。 通常,编写一个直接在 JdbcTemplate
上调用方法的 DAO 方法(而不是将查询封装为一个完整的类)更简单。
但是,如果你从使用 RDBMS 操作类中获得了可衡量的价值,则应继续使用这些类。
# 理解 SqlQuery
SqlQuery
是一个可重用的线程安全类,它封装了一个 SQL 查询。 子类必须实现 newRowMapper(..)
方法来提供一个 RowMapper
实例,该实例可以为从查询执行期间创建的 ResultSet
迭代获得的每一行创建一个对象。 SqlQuery
类很少直接使用,因为 MappingSqlQuery
子类为将行映射到 Java 类提供了一个更方便的实现。 扩展 SqlQuery
的其他实现是 MappingSqlQueryWithParameters
和 UpdatableSqlQuery
。
# 使用 MappingSqlQuery
MappingSqlQuery
是一个可重用的查询,其中具体的子类必须实现抽象的 mapRow(..)
方法,以将提供的 ResultSet
的每一行转换为指定类型的对象。 以下示例显示了一个自定义查询,该查询将来自 t_actor
关系的数据映射到 Actor
类的实例:
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
public ActorMappingQuery(DataSource ds) {
super(ds, "select id, first_name, last_name from t_actor where id = ?");
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}
@Override
protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
Actor actor = new Actor();
actor.setId(rs.getLong("id"));
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
该类扩展了参数化为 Actor
类型的 MappingSqlQuery
。 此自定义查询的构造函数将 DataSource
作为唯一参数。 在此构造函数中,你可以使用 DataSource
和 SQL 调用超类上的构造函数,该 SQL 应运行以检索此查询的行。 此 SQL 用于创建 PreparedStatement
,因此它可能包含要在执行期间传入的任何参数的占位符。 你必须通过使用传入 SqlParameter
的 declareParameter
方法来声明每个参数。 SqlParameter
接受一个名称和 JDBC 类型,如 java.sql.Types
中定义的那样。 定义所有参数后,可以调用 compile()
方法,以便可以准备语句并在以后运行。 此类在编译后是线程安全的,因此,只要在初始化 DAO 时创建这些实例,就可以将它们保留为实例变量并重复使用。 以下示例显示了如何定义这样的类:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Actor getActor(Long id) {
return actorMappingQuery.findObject(id);
}
前一个示例中的方法检索以 id
作为唯一参数传入的 actor。 由于我们只希望返回一个对象,因此我们使用 id
作为参数调用 findObject
便利方法。 如果我们有一个返回对象列表并采用其他参数的查询,我们将使用其中一个 execute
方法,该方法采用作为 varargs 传入的参数值数组。 以下示例显示了这样的方法:
public List<Actor> searchForActors(int age, String namePattern) {
return actorSearchMappingQuery.execute(age, namePattern);
}
# 使用 SqlUpdate
SqlUpdate
类封装了一个 SQL 更新。 与查询一样,更新对象是可重用的,并且与所有 RdbmsOperation
类一样,更新可以具有参数并在 SQL 中定义。 此类提供了许多类似于查询对象的 execute(..)
方法的 update(..)
方法。 SqlUpdate
类是具体的。 它可以被子类化——例如,添加一个自定义更新方法。 但是,你无需对 SqlUpdate
类进行子类化,因为可以通过设置 SQL 和声明参数轻松地对其进行参数化。 以下示例创建一个名为 execute
的自定义更新方法:
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class UpdateCreditRating extends SqlUpdate {
public UpdateCreditRating(DataSource ds) {
setDataSource(ds);
setSql("update customer set credit_rating = ? where id = ?");
declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
declareParameter(new SqlParameter("id", Types.NUMERIC));
compile();
}
/**
* @param id for the Customer to be updated
* @param rating the new value for credit rating
* @return number of rows updated
*/
public int execute(int id, int rating) {
return update(rating, id);
}
}
# 使用 StoredProcedure
StoredProcedure
类是 RDBMS 存储过程的对象抽象的 abstract
超类。
继承的 sql
属性是 RDBMS 中存储过程的名称。
要为 StoredProcedure
类定义参数,你可以使用 SqlParameter
或其子类之一。 你必须在构造函数中指定参数名称和 SQL 类型,如以下代码片段所示:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SQL 类型使用 java.sql.Types
常量指定。
第一行(带有 SqlParameter
)声明一个 IN 参数。 你可以将 IN 参数用于存储过程调用以及使用 SqlQuery
及其子类的查询。
第二行(带有 SqlOutParameter
)声明一个 out
参数,用于存储过程调用。 还有一个 SqlInOutParameter
用于 InOut
参数(向过程提供 in
值并返回值的参数)。
对于 in
参数,除了名称和 SQL 类型之外,你还可以为数字数据指定比例或为自定义数据库类型指定类型名称。 对于 out
参数,你可以提供一个 RowMapper
来处理从 REF
游标返回的行的映射。 另一种选择是指定一个 SqlReturnType
,它允许你定义对返回值进行自定义处理。
下一个简单 DAO 示例使用 StoredProcedure
调用一个函数 ( sysdate()
),该函数随任何 Oracle 数据库一起提供。 要使用存储过程功能,你必须创建一个扩展 StoredProcedure
的类。 在此示例中,StoredProcedure
类是一个内部类。 但是,如果你需要重用 StoredProcedure
,你可以将其声明为顶级类。 此示例没有输入参数,但通过使用 SqlOutParameter
类将输出参数声明为日期类型。 execute()
方法运行该过程并从结果 Map
中提取返回的日期。 结果 Map
为每个声明的输出参数(在本例中只有一个)都有一个条目,使用参数名称作为键。 以下列表显示了我们的自定义 StoredProcedure 类:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class StoredProcedureDao {
private GetSysdateProcedure getSysdate;
@Autowired
public void init(DataSource dataSource) {
this.getSysdate = new GetSysdateProcedure(dataSource);
}
public Date getSysdate() {
return getSysdate.execute();
}
private class GetSysdateProcedure extends StoredProcedure {
private static final String SQL = "sysdate";
public GetSysdateProcedure(DataSource dataSource) {
setDataSource(dataSource);
setFunction(true);
setSql(SQL);
declareParameter(new SqlOutParameter("date", Types.DATE));
compile();
}
public Date execute() {
// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
Map<String, Object> results = execute(new HashMap<String, Object>());
Date sysdate = (Date) results.get("date");
return sysdate;
}
}
}
以下 StoredProcedure
示例有两个输出参数(在本例中为 Oracle REF 游标):
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAndGenresStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "AllTitlesAndGenres";
public TitlesAndGenresStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
compile();
}
public Map<String, Object> execute() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap<String, Object>());
}
}
请注意,在 TitlesAndGenresStoredProcedure
构造函数中使用的 declareParameter(..)
方法的重载变体是如何传递 RowMapper
实现实例的。 这是一种非常方便且强大的重用现有功能的方法。 下面两个示例提供了两个 RowMapper
实现的代码。
TitleMapper
类将 ResultSet
映射到提供的 ResultSet
中每一行的 Title
域对象,如下所示:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;
public final class TitleMapper implements RowMapper<Title> {
public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
Title title = new Title();
title.setId(rs.getLong("id"));
title.setName(rs.getString("name"));
return title;
}
}
GenreMapper
类将 ResultSet
映射到提供的 ResultSet
中每一行的 Genre
域对象,如下所示:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;
public final class GenreMapper implements RowMapper<Genre> {
public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Genre(rs.getString("name"));
}
}
要将参数传递给在其 RDBMS 定义中具有一个或多个输入参数的存储过程,你可以编写一个强类型的 execute(..)
方法,该方法将委托给超类中未类型的 execute(Map)
方法,如以下示例所示:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAfterDateStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "TitlesAfterDate";
private static final String CUTOFF_DATE_PARAM = "cutoffDate";
public TitlesAfterDateStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE));
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
compile();
}
public Map<String, Object> execute(Date cutoffDate) {
Map<String, Object> inputs = new HashMap<String, Object>();
inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
return super.execute(inputs);
}
}
# 嵌入式数据库支持
org.springframework.jdbc.datasource.embedded
包提供了对嵌入式 Java 数据库引擎的支持。原生支持 HSQL (opens new window)、H2 (opens new window) 和 Derby (opens new window)。您还可以使用可扩展的 API 来插入新的嵌入式数据库类型和 DataSource
实现。
# 为什么使用嵌入式数据库?
由于其轻量级的特性,嵌入式数据库在项目的开发阶段非常有用。其优点包括易于配置、启动时间快、可测试性以及在开发过程中快速演进 SQL 的能力。
# 创建嵌入式数据库
您可以将嵌入式数据库实例公开为一个 bean,如以下示例所示:
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true) // 生成唯一名称
.setType(EmbeddedDatabaseType.H2) // 设置数据库类型为H2
.addScripts("schema.sql", "test-data.sql") // 添加初始化脚本
.build();
}
<jdbc:embedded-database id="dataSource" generate-name="true" type="H2">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
上述配置创建了一个嵌入式 H2 数据库,该数据库使用 classpath 根目录下的 schema.sql
和 test-data.sql
资源中的 SQL 进行填充。此外,作为最佳实践,嵌入式数据库被分配了一个唯一生成的名称。嵌入式数据库作为 javax.sql.DataSource
类型的 bean 提供给 Spring 容器,然后可以根据需要将其注入到数据访问对象中。
有关所有支持选项的更多详细信息,请参阅 EmbeddedDatabaseBuilder
的 javadoc (opens new window)。
# 选择嵌入式数据库类型
本节介绍如何选择 Spring 支持的三种嵌入式数据库之一。
# 使用 HSQL
Spring 支持 HSQL 1.8.0 及以上版本。如果未显式指定类型,则 HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将 embedded-database
标记的 type
属性设置为 HSQL
。如果使用构建器 API,请使用 EmbeddedDatabaseType.HSQL
调用 setType(EmbeddedDatabaseType)
方法。
# 使用 H2
Spring 支持 H2 数据库。要启用 H2,请将 embedded-database
标记的 type
属性设置为 H2
。如果使用构建器 API,请使用 EmbeddedDatabaseType.H2
调用 setType(EmbeddedDatabaseType)
方法。
# 使用 Derby
Spring 支持 Apache Derby 10.5 及以上版本。要启用 Derby,请将 embedded-database
标记的 type
属性设置为 DERBY
。如果使用构建器 API,请使用 EmbeddedDatabaseType.DERBY
调用 setType(EmbeddedDatabaseType)
方法。
# 自定义嵌入式数据库类型
虽然每种支持的类型都带有默认连接设置,但如有必要,可以自定义它们。以下示例使用带有自定义驱动程序的 H2:
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setDatabaseConfigurer(EmbeddedDatabaseConfigurers
.customizeConfigurer(H2, this::customize)) // 使用自定义配置器
.addScript("schema.sql") // 添加初始化脚本
.build();
}
private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) {
return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) {
@Override
public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {
super.configureConnectionProperties(properties, databaseName);
properties.setDriverClass(CustomDriver.class); // 设置自定义驱动类
}
};
}
}
# 使用嵌入式数据库测试数据访问逻辑
嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在测试类之间重用时,使用这样的模板对于一次性测试可能很有用。但是,如果您希望创建一个在测试套件中共享的嵌入式数据库,请考虑使用 Spring TestContext Framework (opens new window) 并在 Spring ApplicationContext
中将嵌入式数据库配置为 bean。以下清单显示了测试模板:
public class DataAccessIntegrationTestTemplate {
private EmbeddedDatabase db;
@BeforeEach
public void setUp() {
// creates an HSQL in-memory database populated from default scripts
// classpath:schema.sql and classpath:data.sql
db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true) // 生成唯一名称
.addDefaultScripts() // 添加默认脚本
.build();
}
@Test
public void testDataAccess() {
JdbcTemplate template = new JdbcTemplate(db);
template.query( /* ... */ );
}
@AfterEach
public void tearDown() {
db.shutdown();
}
}
# 为嵌入式数据库生成唯一名称
如果开发团队的测试套件无意中尝试重新创建同一数据库的其他实例,他们通常会遇到嵌入式数据库的错误。如果 XML 配置文件或 @Configuration
类负责创建嵌入式数据库,并且相应的配置随后在同一测试套件(即,在同一 JVM 进程中)的多个测试场景中重用,则很容易发生这种情况 — 例如,针对嵌入式数据库的集成测试,这些嵌入式数据库的 ApplicationContext
配置仅在哪个 bean 定义 profile 处于活动状态方面有所不同。
此类错误的根本原因是 Spring 的 EmbeddedDatabaseFactory
(由 <jdbc:embedded-database>
XML 命名空间元素和用于 Java 配置的 EmbeddedDatabaseBuilder
在内部使用)如果未另行指定,则将嵌入式数据库的名称设置为 testdb
。对于 <jdbc:embedded-database>
,嵌入式数据库通常被分配一个等于 bean 的 id
的名称(通常,类似于 dataSource
)。因此,后续创建嵌入式数据库的尝试不会导致新的数据库。相反,相同的 JDBC 连接 URL 被重用,并且创建新嵌入式数据库的尝试实际上指向从相同配置创建的现有嵌入式数据库。
为了解决这个常见问题,Spring Framework 4.2 提供了对为嵌入式数据库生成唯一名称的支持。要启用生成名称的使用,请使用以下选项之一。
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
EmbeddedDatabaseBuilder.generateUniqueName()
<jdbc:embedded-database generate-name="true" … >
# 扩展嵌入式数据库支持
您可以通过两种方式扩展 Spring JDBC 嵌入式数据库支持:
- 实现
EmbeddedDatabaseConfigurer
以支持新的嵌入式数据库类型。 - 实现
DataSourceFactory
以支持新的DataSource
实现,例如连接池以管理嵌入式数据库连接。
# 初始化 DataSource
org.springframework.jdbc.datasource.init
包提供了对初始化现有 DataSource
的支持。嵌入式数据库支持提供了一种创建和初始化应用程序 DataSource
的选项。但是,您有时可能需要初始化在某处服务器上运行的实例。
# 使用 Spring XML 初始化数据库
如果您想初始化一个数据库,并且可以提供对 DataSource
bean 的引用,您可以使用 spring-jdbc
命名空间中的 initialize-database
标签:
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
<jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>
前面的示例针对数据库运行两个指定的脚本。第一个脚本创建一个模式,第二个脚本使用测试数据集填充表。脚本位置也可以是模式,其中包含用于 Spring 中资源的常用 Ant 风格的通配符(例如,classpath*:/com/foo/**/sql/*-data.sql
)。如果使用模式,则脚本按其 URL 或文件名的词法顺序运行。
数据库初始值设定项的默认行为是无条件地运行提供的脚本。这可能并不总是您想要的 — 例如,如果您针对已经有测试数据的数据库运行脚本。通过遵循创建表,然后插入数据的常见模式(如前所示),可以降低意外删除数据的可能性。如果表已存在,则第一步将失败。
但是,为了更好地控制现有数据的创建和删除,XML 命名空间提供了一些额外的选项。第一个是打开和关闭初始化的标志。您可以根据环境设置此值(例如,从系统属性或从环境 bean 中提取布尔值)。以下示例从系统属性获取值:
<jdbc:initialize-database data-source="dataSource"
enabled="#{systemProperties.INITIALIZE_DATABASE}"> <!-- (1) -->
<jdbc:script location="..."/>
</jdbc:initialize-database>
(1) 从名为 INITIALIZE_DATABASE
的系统属性获取 enabled
的值。
控制现有数据发生情况的第二个选项是对失败更加宽容。为此,您可以控制初始值设定项忽略脚本中运行的 SQL 中的某些错误的能力,如以下示例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的示例中,我们说我们希望有时针对空数据库运行脚本,并且脚本中存在一些 DROP
语句,因此这些语句会失败。因此,失败的 SQL DROP
语句将被忽略,但其他失败将导致异常。如果您的 SQL 方言不支持 DROP … IF EXISTS
(或类似语句),但您想在重新创建所有测试数据之前无条件地删除所有测试数据,这将非常有用。在这种情况下,第一个脚本通常是一组 DROP
语句,后跟一组 CREATE
语句。
ignore-failures
选项可以设置为 NONE
(默认值)、DROPS
(忽略失败的删除)或 ALL
(忽略所有失败)。
如果脚本中根本不存在 ;
字符,则每个语句应以 ;
或新行分隔。您可以全局或按脚本控制它,如以下示例所示:
<jdbc:initialize-database data-source="dataSource" separator="@@"> <!-- (1) -->
<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> <!-- (2) -->
<jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
(1) 将分隔符脚本设置为 @@
。
(2) 将 db-schema.sql
的分隔符设置为 ;
。
在此示例中,两个 test-data
脚本使用 @@
作为语句分隔符,只有 db-schema.sql
使用 ;
。此配置指定默认分隔符为 @@
,并覆盖 db-schema
脚本的默认分隔符。
如果您需要比从 XML 命名空间获得的更多控制,您可以直接使用 DataSourceInitializer
并将其定义为应用程序中的组件。
# 初始化依赖于数据库的其他组件
很大一类应用程序(那些在 Spring 上下文启动后才使用数据库的应用程序)可以使用数据库初始值设定项,而无需进一步的复杂性。如果您的应用程序不是其中之一,您可能需要阅读本节的其余部分。
数据库初始值设定项依赖于 DataSource
实例,并运行其初始化回调中提供的脚本(类似于 XML bean 定义中的 init-method
、组件中的 @PostConstruct
方法或实现 InitializingBean
的组件中的 afterPropertiesSet()
方法)。如果其他 bean 依赖于同一数据源,并在初始化回调中使用数据源,则可能会出现问题,因为数据尚未初始化。一个常见的例子是缓存,它急切地初始化并在应用程序启动时从数据库加载数据。
要解决此问题,您有两个选择:将缓存初始化策略更改为稍后阶段,或确保首先初始化数据库初始值设定项。
如果应用程序在您的控制之下而不是其他情况下,更改缓存初始化策略可能很容易。以下是一些关于如何实现此操作的建议:
- 使缓存在首次使用时延迟初始化,这可以缩短应用程序启动时间。
- 让您的缓存或初始化缓存的单独组件实现
Lifecycle
或SmartLifecycle
。当应用程序上下文启动时,您可以通过设置其autoStartup
标志自动启动SmartLifecycle
,并且可以通过在封闭上下文中调用ConfigurableApplicationContext.start()
手动启动Lifecycle
。 - 使用 Spring
ApplicationEvent
或类似的自定义观察者机制来触发缓存初始化。当上下文准备好使用时(在所有 bean 都已初始化之后),上下文始终会发布ContextRefreshedEvent
,因此这通常是一个有用的钩子(默认情况下,SmartLifecycle
就是这样工作的)。
确保首先初始化数据库初始值设定项也很容易。以下是一些关于如何实现此操作的建议:
- 依赖于 Spring
BeanFactory
的默认行为,即 bean 按注册顺序初始化。您可以通过采用 XML 配置中一组<import/>
元素的常见做法来轻松安排这一点,这些元素对您的应用程序模块进行排序,并确保首先列出数据库和数据库初始化。 - 分离
DataSource
和使用它的业务组件,并通过将它们放在单独的ApplicationContext
实例中来控制它们的启动顺序(例如,父上下文包含DataSource
,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更广泛地应用。
# 使用 R2DBC 进行数据访问
R2DBC (opens new window) ("Reactive Relational Database Connectivity" - 响应式关系数据库连接) 是一项由社区驱动的规范制定工作,旨在标准化使用响应式模式访问 SQL 数据库的方式。
# 包结构
Spring Framework 的 R2DBC 抽象框架由两个不同的包组成:
core
:org.springframework.r2dbc.core
包包含DatabaseClient
类以及各种相关的类。connection
:org.springframework.r2dbc.connection
包包含一个实用工具类,用于轻松访问ConnectionFactory
以及各种简单的ConnectionFactory
实现,您可以使用它们进行测试和运行未经修改的 R2DBC。
# 使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理
本节介绍如何使用 R2DBC 核心类来控制基本的 R2DBC 处理,包括错误处理。
# 使用 DatabaseClient
DatabaseClient
是 R2DBC 核心包中的中心类。它处理资源的创建和释放,这有助于避免常见的错误,例如忘记关闭连接。它执行核心 R2DBC 工作流程的基本任务(例如语句创建和执行),将应用程序代码留下来提供 SQL 并提取结果。DatabaseClient
类:
- 运行 SQL 查询
- 更新语句和存储过程调用
- 执行对
Result
实例的迭代 - 捕获 R2DBC 异常,并将它们转换为
org.springframework.dao
包中定义的通用、更具信息性的异常层次结构。
该客户端具有功能性、流畅的 API,使用响应式类型进行声明式组合。
当您为代码使用 DatabaseClient
时,您只需要实现 java.util.function
接口,为它们提供明确定义的契约。给定一个由 DatabaseClient
类提供的 Connection
,Function
回调会创建一个 Publisher
。对于提取 Row
结果的映射函数也是如此。
您可以通过使用 ConnectionFactory
引用直接实例化在 DAO 实现中使用 DatabaseClient
,或者您可以在 Spring IoC 容器中配置它,并将其作为 bean 引用提供给 DAO。
创建 DatabaseClient
对象的最简单方法是通过静态工厂方法,如下所示:
DatabaseClient client = DatabaseClient.create(connectionFactory);
笔记
ConnectionFactory
应该始终配置为 Spring IoC 容器中的 bean。
前面的方法使用默认设置创建一个 DatabaseClient
。
您还可以从 DatabaseClient.builder()
获取一个 Builder
实例。您可以通过调用以下方法来自定义客户端:
….bindMarkers(…)
: 提供一个特定的BindMarkersFactory
来配置命名参数到数据库绑定标记的转换。….executeFunction(…)
: 设置ExecuteFunction
如何运行Statement
对象。….namedParameters(false)
: 禁用命名参数展开。默认启用。
提示
Dialect 通过来自 ConnectionFactory
的 BindMarkersFactoryResolver
(opens new window) 解析,通常通过检查 ConnectionFactoryMetadata
。
您可以通过注册一个通过 META-INF/spring.factories
实现 org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider
的类,让 Spring 自动发现您的 BindMarkersFactory
。BindMarkersFactoryResolver
使用 Spring 的 SpringFactoriesLoader
从类路径中发现绑定标记提供程序实现。
当前支持的数据库有:
- H2
- MariaDB
- Microsoft SQL Server
- MySQL
- Postgres
此类发出的所有 SQL 都以 DEBUG
级别记录在对应于客户端实例的完全限定类名的类别下(通常为 DefaultDatabaseClient
)。此外,每次执行都会在响应式序列中注册一个检查点,以帮助调试。
以下各节提供了一些 DatabaseClient
用法的示例。这些示例并非 DatabaseClient
公开的所有功能的详尽列表。有关更多信息,请参阅随附的 javadoc (opens new window)。
# 执行语句
DatabaseClient
提供了运行语句的基本功能。以下示例显示了创建新表所需的最小但功能完整的代码:
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
DatabaseClient
旨在方便、流畅地使用。它在执行规范的每个阶段都公开了中间方法、延续方法和终端方法。前面的示例使用 then()
返回一个完成 Publisher
,该 Publisher
在查询(或查询,如果 SQL 查询包含多个语句)完成时立即完成。
笔记
execute(…)
接受 SQL 查询字符串或查询 Supplier<String>
,以将实际查询创建延迟到执行时。
# 查询 (SELECT
)
SQL 查询可以通过 Row
对象或受影响的行数返回值。DatabaseClient
可以返回更新的行数或行本身,具体取决于发出的查询。
以下查询从表中获取 id
和 name
列:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
.fetch().first();
以下查询使用绑定变量:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().first();
您可能已经注意到上面示例中使用了 fetch()
。fetch()
是一个延续运算符,可让您指定要消耗多少数据。
调用 first()
返回结果的第一行并丢弃剩余的行。您可以使用以下运算符来消耗数据:
first()
返回整个结果的第一行。它的 Kotlin 协程变体对于不可为空的返回值命名为awaitSingle()
,如果该值是可选的,则命名为awaitSingleOrNull()
。one()
返回正好一个结果,如果结果包含更多行,则失败。使用 Kotlin 协程,awaitOne()
用于正好一个值,如果该值可能为null
,则使用awaitOneOrNull()
。all()
返回结果的所有行。使用 Kotlin 协程时,请使用flow()
。rowsUpdated()
返回受影响的行数(INSERT
/UPDATE
/DELETE
计数)。它的 Kotlin 协程变体命名为awaitRowsUpdated()
。
如果不指定进一步的映射详细信息,查询会将表格结果作为 Map
返回,其键是不区分大小写的列名,这些列名映射到它们的列值。
您可以通过提供一个 Function<Row, T>
来控制结果映射,该函数为每个 Row
调用,因此它可以返回任意值(单数值、集合和映射以及对象)。
以下示例提取 name
列并发出其值:
Flux<String> names = client.sql("SELECT name FROM person")
.map(row -> row.get("name", String.class))
.all();
或者,有一种映射到单个值的快捷方式:
Flux<String> names = client.sql("SELECT name FROM person")
.mapValue(String.class)
.all();
或者,您可以映射到具有 bean 属性或记录组件的结果对象:
// assuming a name property on Person
Flux<Person> persons = client.sql("SELECT name FROM person")
.mapProperties(Person.class)
.all();
:::sidebar
# null
呢?
关系数据库结果可以包含 null
值。Reactive Streams 规范禁止发出 null
值。该要求要求在提取器函数中正确处理 null
值。虽然您可以从 Row
获取 null
值,但您不得发出 null
值。您必须将任何 null
值包装在一个对象中(例如,单数值的 Optional
),以确保提取器函数永远不会直接返回 null
值。
:::
# 使用 DatabaseClient
更新 (INSERT
, UPDATE
和 DELETE
)
修改语句的唯一区别在于这些语句通常不返回表格数据,因此您使用 rowsUpdated()
来消耗结果。
以下示例显示了一个 UPDATE
语句,该语句返回更新的行数:
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().rowsUpdated();
# 将值绑定到查询
典型的应用程序需要参数化的 SQL 语句,以便根据一些输入选择或更新行。这些通常是由 WHERE
子句约束的 SELECT
语句,或者接受输入参数的 INSERT
和 UPDATE
语句。如果参数没有正确转义,参数化语句会带来 SQL 注入的风险。DatabaseClient
利用 R2DBC 的 bind
API 来消除查询参数的 SQL 注入风险。您可以使用 execute(…)
运算符提供一个参数化的 SQL 语句,并将参数绑定到实际的 Statement
。然后,您的 R2DBC 驱动程序通过使用预处理语句和参数替换来运行该语句。
参数绑定支持两种绑定策略:
- 按索引,使用从零开始的参数索引。
- 按名称,使用占位符名称。
以下示例显示了查询的参数绑定:
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
或者,您可以传入名称和值的映射:
Map<String, Object> params = new LinkedHashMap<>();
params.put("id", "joe");
params.put("name", "Joe");
params.put("age", 34);
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindValues(params);
或者,您可以传入具有 bean 属性或记录组件的参数对象:
// assuming id, name, age properties on Person
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindProperties(new Person("joe", "Joe", 34);
或者,您可以使用位置参数将值绑定到语句。索引从零开始。
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind(0, "joe")
.bind(1, "Joe")
.bind(2, 34);
如果您的应用程序绑定到许多参数,则可以使用单个调用来实现相同的目的:
List<?> values = List.of("joe", "Joe", 34);
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindValues(values);
:::sidebar
# R2DBC 原生绑定标记
R2DBC 使用依赖于实际数据库供应商的数据库原生绑定标记。例如,Postgres 使用索引标记,例如 $1
、$2
、$n
。另一个例子是 SQL Server,它使用以 @
开头的命名绑定标记。
这与 JDBC 不同,JDBC 需要 ?
作为绑定标记。在 JDBC 中,实际驱动程序在语句执行时将 ?
绑定标记转换为数据库原生标记。
Spring Framework 的 R2DBC 支持允许您使用原生绑定标记或带有 :name
语法的命名绑定标记。
命名参数支持利用 BindMarkersFactory
实例在查询执行时将命名参数展开为原生绑定标记,这使您可以在各种数据库供应商之间获得一定程度的查询可移植性。
:::
查询预处理器将命名的 Collection
参数展开为一系列绑定标记,从而无需基于参数数量动态创建查询。嵌套对象数组被展开以允许使用(例如)选择列表。
考虑以下查询:
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
前面的查询可以参数化并按如下方式运行:
List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann", 50});
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples);
笔记
选择列表的使用取决于供应商。
以下示例显示了使用 IN
谓词的更简单变体:
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50));
笔记
R2DBC 本身不支持类似 Collection 的值。尽管如此,在 Spring 的 R2DBC 支持中,展开上面示例中给定的 List
适用于命名参数,例如,用于 IN
子句,如上所示。但是,插入或更新数组类型的列(例如,在 Postgres 中)需要底层 R2DBC 驱动程序支持的数组类型:通常是一个 Java 数组,例如,String[]
来更新 text[]
列。不要将 Collection<String>
或类似内容作为数组参数传递。
# 语句过滤器
有时您需要在实际 Statement
运行之前对其进行微调。为此,请向 DatabaseClient
注册一个 Statement
过滤器 (StatementFilterFunction
),以拦截和修改执行中的语句,如以下示例所示:
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
DatabaseClient
还公开了一个简化的 filter(…)
重载,它接受一个 Function<Statement, Statement>
:
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
.filter(statement -> s.fetchSize(25));
StatementFilterFunction
实现允许过滤 Statement
和过滤 Result
对象。
# DatabaseClient
最佳实践
DatabaseClient
类的实例一旦配置好,就是线程安全的。这一点很重要,因为它意味着您可以配置 DatabaseClient
的单个实例,然后安全地将这个共享引用注入到多个 DAO(或存储库)中。DatabaseClient
是有状态的,因为它维护对 ConnectionFactory
的引用,但此状态不是会话状态。
使用 DatabaseClient
类的一个常见做法是在 Spring 配置文件中配置一个 ConnectionFactory
,然后将该共享的 ConnectionFactory
bean 依赖注入到您的 DAO 类中。DatabaseClient
在 ConnectionFactory
的 setter 中创建。这导致 DAO 类似于以下内容:
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
显式配置的替代方法是使用组件扫描和注释支持进行依赖注入。在这种情况下,您可以使用 @Component
注释该类(这使其成为组件扫描的候选者),并使用 @Autowired
注释 ConnectionFactory
setter 方法。以下示例显示了如何执行此操作:
@Component
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
@Autowired
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
无论您选择使用哪种上述模板初始化样式(或不使用),每次要运行 SQL 时都很少需要创建 DatabaseClient
类的新实例。配置完成后,DatabaseClient
实例是线程安全的。如果您的应用程序访问多个数据库,您可能需要多个 DatabaseClient
实例,这需要多个 ConnectionFactory
,并因此需要多个不同配置的 DatabaseClient
实例。
# 检索自动生成的键
当将行插入到定义自增或标识列的表中时,INSERT
语句可能会生成键。要完全控制要生成的列名,只需注册一个 StatementFilterFunction
,该函数请求所需列的生成的键。
Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"))
.map(row -> row.get("id", Integer.class))
.first();
// generatedId emits the generated key once the INSERT statement has finished
# 控制数据库连接
# 使用 ConnectionFactory
Spring 通过 ConnectionFactory
获取与数据库的 R2DBC 连接。ConnectionFactory
是 R2DBC 规范的一部分,是驱动程序的通用入口点。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,您无需了解如何连接到数据库的详细信息。这是管理员的责任,管理员设置 ConnectionFactory
。在开发和测试代码时,您很可能同时扮演这两个角色,但您不一定需要知道生产数据源是如何配置的。
当您使用 Spring 的 R2DBC 层时,您可以使用第三方提供的连接池实现来配置您自己的连接池。一个流行的实现是 R2DBC Pool (r2dbc-pool
)。Spring 发行版中的实现仅用于测试目的,不提供池化。
要配置 ConnectionFactory
:
- 使用
ConnectionFactory
获取连接,就像通常获取 R2DBCConnectionFactory
一样。 - 提供一个 R2DBC URL(有关正确的值,请参阅驱动程序的文档)。
以下示例显示了如何配置 ConnectionFactory
:
ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
# 使用 ConnectionFactoryUtils
ConnectionFactoryUtils
类是一个方便而强大的辅助类,它提供了 static
方法来从 ConnectionFactory
获取连接并关闭连接(如果需要)。
它支持订阅者 Context
绑定的连接,例如 R2dbcTransactionManager
。
# 使用 SingleConnectionFactory
SingleConnectionFactory
类是 DelegatingConnectionFactory
接口的一个实现,它包装了一个在使用后不会关闭的单个 Connection
。
如果任何客户端代码在假设池连接的情况下调用 close
(例如,在使用持久性工具时),您应该将 suppressClose
属性设置为 true
。此设置返回一个关闭抑制代理,该代理包装物理连接。请注意,您不能再将其强制转换为原生 Connection
或类似对象。
SingleConnectionFactory
主要是一个测试类,如果您的 R2DBC 驱动程序允许这种使用,则可以将其用于特定的需求,例如流水线。与池化的 ConnectionFactory
相比,它始终重用相同的连接,从而避免了过度创建物理连接。
# 使用 TransactionAwareConnectionFactoryProxy
TransactionAwareConnectionFactoryProxy
是目标 ConnectionFactory
的代理。该代理包装该目标 ConnectionFactory
以添加对 Spring 管理的事务的感知。
笔记
如果您使用的 R2DBC 客户端没有以其他方式与 Spring 的 R2DBC 支持集成,则需要使用此类。在这种情况下,您仍然可以使用此客户端,同时让此客户端参与 Spring 管理的事务。通常,最好将 R2DBC 客户端与适当访问 ConnectionFactoryUtils
的资源管理集成。
有关更多详细信息,请参阅 TransactionAwareConnectionFactoryProxy
(opens new window) javadoc。
# 使用 R2dbcTransactionManager
R2dbcTransactionManager
类是单个 R2DBC ConnectionFactory
的 ReactiveTransactionManager
实现。它将来自指定 ConnectionFactory
的 R2DBC Connection
绑定到订阅者 Context
,从而可能允许每个 ConnectionFactory
有一个订阅者 Connection
。
应用程序代码需要通过 ConnectionFactoryUtils.getConnection(ConnectionFactory)
而不是 R2DBC 的标准 ConnectionFactory.create()
来检索 R2DBC Connection
。所有框架类(例如 DatabaseClient
)都隐式地使用此策略。如果未使用事务管理器,则查找策略的行为与 ConnectionFactory.create()
完全相同,因此可以在任何情况下使用。
# 对象关系映射 (ORM) 数据访问
# Spring 中 ORM 简介
Spring 框架支持与 Java 持久性 API (JPA) 集成,并支持原生 Hibernate,用于资源管理、数据访问对象 (DAO) 实现和事务策略。例如,对于 Hibernate,它具有一流的支持,并提供了一些方便的 IoC 功能,可解决许多典型的 Hibernate 集成问题。你可以通过依赖注入为 OR(对象关系)映射工具配置所有受支持的功能。它们可以参与 Spring 的资源和事务管理,并且符合 Spring 的通用事务和 DAO 异常层次结构。推荐的集成方式是针对普通的 Hibernate 或 JPA API 编写 DAO。
当你创建数据访问应用程序时,Spring 会为所选的 ORM 层添加重要的增强功能。你可以根据需要利用尽可能多的集成支持,并且应将这种集成工作与内部构建类似基础设施的成本和风险进行比较。你可以像使用库一样使用大部分 ORM 支持,而无需考虑技术,因为所有内容都设计为一组可重用的 JavaBeans。Spring IoC 容器中的 ORM 有助于配置和部署。因此,本节中的大多数示例都显示了 Spring 容器内部的配置。
使用 Spring 框架创建 ORM DAO 的好处包括:
- 更轻松的测试。 Spring 的 IoC 方法可以轻松地交换 Hibernate
SessionFactory
实例、JDBCDataSource
实例、事务管理器和映射对象实现(如果需要)的实现和配置位置。反过来,这使得隔离测试每个持久性相关代码变得更加容易。 - 通用数据访问异常。 Spring 可以包装来自 ORM 工具的异常,将其从专有(可能已检查)的异常转换为通用的运行时
DataAccessException
层次结构。此功能使你只需在适当的层中处理大多数不可恢复的持久性异常,而无需烦人的样板捕获、抛出和异常声明。你仍然可以根据需要捕获和处理异常。请记住,JDBC 异常(包括特定于数据库的方言)也会转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行某些操作。 - 通用资源管理。 Spring 应用程序上下文可以处理 Hibernate
SessionFactory
实例、JPAEntityManagerFactory
实例、JDBCDataSource
实例和其他相关资源的位置和配置。这使得这些值易于管理和更改。Spring 提供高效、简单和安全的持久性资源处理。例如,使用 Hibernate 的相关代码通常需要使用相同的 HibernateSession
,以确保效率和正确的事务处理。Spring 通过 HibernateSessionFactory
公开当前的Session
,从而可以轻松地创建Session
并将其透明地绑定到当前线程。因此,对于任何本地或 JTA 事务环境,Spring 解决了典型 Hibernate 使用的许多长期存在的问题。 - 集成事务管理。 你可以通过
@Transactional
注解或通过在 XML 配置文件中显式配置事务 AOP 通知,使用声明式面向切面编程 (AOP) 样式的拦截器来包装 ORM 代码。在这两种情况下,事务语义和异常处理(回滚等)都由你处理。正如事务管理中所讨论的,你也可以交换各种事务管理器,而不会影响与 ORM 相关的代码。例如,你可以在本地事务和 JTA 之间进行交换,并且在这两种情况下都可以使用相同的完整服务(例如声明式事务)。此外,与 JDBC 相关的代码可以与用于执行 ORM 的代码完全以事务方式集成。这对于不适合 ORM 的数据访问(例如,批量处理和 BLOB 流式传输)但仍需要与 ORM 操作共享通用事务的数据访问非常有用。
提示: 要获得更全面的 ORM 支持,包括对 MongoDB 等替代数据库技术的支持,你可能需要查看 Spring Data (opens new window) 项目套件。如果你是 JPA 用户,通过 JPA 访问数据入门 (opens new window) 指南(来自 spring.io (opens new window))提供了一个很好的介绍。
# ORM 集成通用注意事项
本节重点介绍适用于所有 ORM 技术的注意事项。Hibernate 部分提供了更多详细信息,并在具体上下文中展示了这些特性和配置。
Spring 的 ORM 集成的主要目标是清晰的应用程序分层(使用任何数据访问和事务技术)以及应用程序对象的松散耦合——业务服务不再依赖于数据访问或事务策略,不再有硬编码的资源查找,不再有难以替换的单例,不再有自定义服务注册表。目标是采用一种简单且一致的方法来连接应用程序对象,使其尽可能可重用且不受容器依赖的影响。所有单独的数据访问功能都可以单独使用,但可以与 Spring 的应用程序上下文概念很好地集成,从而提供基于 XML 的配置和对不需要了解 Spring 的普通 JavaBean 实例的交叉引用。在典型的 Spring 应用程序中,许多重要的对象都是 JavaBeans:数据访问模板、数据访问对象、事务管理器、使用数据访问对象和事务管理器的业务服务、Web 视图解析器、使用业务服务的 Web 控制器等等。
# 资源和事务管理
典型的业务应用程序充斥着重复的资源管理代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。Spring 提倡针对正确资源处理的简单解决方案,即在 JDBC 的情况下通过模板进行 IoC,并为 ORM 技术应用 AOP 拦截器。
该基础设施提供正确的资源处理以及将特定 API 异常适当转换为未检查的基础设施异常层次结构。Spring 引入了一个 DAO 异常层次结构,适用于任何数据访问策略。对于直接 JDBC,上一节中提到的 JdbcTemplate
类提供连接处理并将 SQLException
正确转换为 DataAccessException
层次结构,包括将特定于数据库的 SQL 错误代码转换为有意义的异常类。
在事务管理方面,JdbcTemplate
类挂钩到 Spring 事务支持,并通过各自的 Spring 事务管理器支持 JTA 和 JDBC 事务。对于受支持的 ORM 技术,Spring 通过 Hibernate 和 JPA 事务管理器以及 JTA 支持提供 Hibernate 和 JPA 支持。
# 异常转换
在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久性技术的本机异常类。DAO 抛出 HibernateException
或 PersistenceException
的子类,具体取决于技术。这些异常都是运行时异常,不需要声明或捕获。你可能还需要处理 IllegalArgumentException
和 IllegalStateException
。这意味着调用者只能将异常视为通常是致命的,除非他们想依赖持久性技术自己的异常结构。如果不将调用者绑定到实现策略,则无法捕获特定原因(例如,乐观锁定失败)。对于基于 ORM 的应用程序或不需要任何特殊异常处理的应用程序(或两者兼而有之),这种权衡可能是可以接受的。但是,Spring 允许通过 @Repository
注解透明地应用异常转换。以下示例(一个用于 Java 配置,一个用于 XML 配置)展示了如何做到这一点:
@Repository
public class ProductDaoImpl implements ProductDao {
// class body here...
}
<beans>
<!-- Exception translation bean post processor -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
<bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>
后处理器会自动查找所有异常转换器(PersistenceExceptionTranslator
接口的实现),并通知所有标有 @Repository
注解的 bean,以便发现的转换器可以拦截并对抛出的异常应用适当的转换。
总而言之,你可以基于普通持久性技术的 API 和注解来实现 DAO,同时仍然受益于 Spring 管理的事务、依赖注入和透明的异常转换(如果需要)到 Spring 的自定义异常层次结构。
# Hibernate
我们首先介绍 Spring 环境中的 Hibernate 5 (opens new window),并使用它来演示 Spring 采用的集成 ORM 映射器的方法。本节详细介绍许多问题,并展示 DAO 实现和事务划分的不同变体。这些模式中的大多数都可以直接转换为所有其他受支持的 ORM 工具。本章后面的部分将介绍其他 ORM 技术,并展示简短的示例。
注意: 从 Spring Framework 6.0 开始,Spring 要求 Hibernate ORM 5.5+ 用于 Spring 的
HibernateJpaVendorAdapter
以及原生 HibernateSessionFactory
设置。我们建议使用 Hibernate ORM 5.6 作为该 Hibernate 生成中的最后一个特性分支。Hibernate ORM 6.x 仅支持作为 JPA 提供程序(
HibernateJpaVendorAdapter
)。不再支持使用orm.hibernate5
包的普通SessionFactory
设置。我们建议为新的开发项目使用带有 JPA 样式设置的 Hibernate ORM 6.1/6.2。
# Spring 容器中的 SessionFactory
设置
为避免将应用程序对象绑定到硬编码的资源查找,你可以将资源(例如 JDBC DataSource
或 Hibernate SessionFactory
)定义为 Spring 容器中的 bean。需要访问资源的应用程序对象通过 bean 引用接收对这些预定义实例的引用,如下一节中的 DAO 定义所示。
以下 XML 应用程序上下文定义摘录显示了如何在 JDBC DataSource
之上设置 JDBC DataSource
和 Hibernate SessionFactory
:
<beans>
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>
<property name="mappingResources">
<list>
<value>product.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=org.hibernate.dialect.HSQLDialect
</value>
</property>
</bean>
</beans>
从本地 Jakarta Commons DBCP BasicDataSource
切换到 JNDI 定位的 DataSource
(通常由应用程序服务器管理)只是配置问题,如以下示例所示:
<beans>
<jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>
你还可以使用 Spring 的 JndiObjectFactoryBean
/ <jee:jndi-lookup>
访问 JNDI 定位的 SessionFactory
,以检索和公开它。但是,这通常在 EJB 上下文之外并不常见。
注意: Spring 还提供了一个
LocalSessionFactoryBuilder
变体,可以与@Bean
样式配置和编程设置无缝集成(不涉及FactoryBean
)。
LocalSessionFactoryBean
和LocalSessionFactoryBuilder
都支持后台引导,Hibernate 初始化与给定引导执行程序(例如SimpleAsyncTaskExecutor
)上的应用程序引导线程并行运行。在LocalSessionFactoryBean
上,这可以通过bootstrapExecutor
属性获得。在编程LocalSessionFactoryBuilder
上,有一个重载的buildSessionFactory
方法,该方法接受引导执行程序参数。这种原生 Hibernate 设置还可以公开 JPA
EntityManagerFactory
,用于标准的 JPA 交互以及原生 Hibernate 访问。
# 基于普通 Hibernate API 实现 DAO
Hibernate 具有一个称为上下文会话的功能,其中 Hibernate 本身管理每个事务的一个当前 Session
。这大致相当于 Spring 每个事务同步一个 Hibernate Session
。相应的 DAO 实现类似于以下示例,该示例基于普通的 Hibernate API:
public class ProductDaoImpl implements ProductDao {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public Collection loadProductsByCategory(String category) {
return this.sessionFactory.getCurrentSession()
.createQuery("from test.Product product where product.category=?")
.setParameter(0, category)
.list();
}
}
这种风格与 Hibernate 参考文档和示例的风格相似,除了在实例变量中保存 SessionFactory
之外。我们强烈建议使用这种基于实例的设置,而不是 Hibernate 的 CaveatEmptor 示例应用程序中的旧式 static
HibernateUtil
类。(通常,除非绝对必要,否则不要将任何资源保存在 static
变量中。)
前面的 DAO 示例遵循依赖注入模式。它非常适合 Spring IoC 容器,就像针对 Spring 的 HibernateTemplate
编写代码一样。你也可以在普通的 Java 中设置这样的 DAO(例如,在单元测试中)。为此,实例化它并使用所需的工厂引用调用 setSessionFactory(..)
。作为 Spring bean 定义,DAO 将类似于以下内容:
<beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
</beans>
这种 DAO 风格的主要优点是它仅依赖于 Hibernate API。不需要导入任何 Spring 类。从非侵入性的角度来看,这很有吸引力,并且对于 Hibernate 开发人员来说可能感觉更自然。
但是,DAO 抛出普通的 HibernateException
(它是未检查的,因此不需要声明或捕获),这意味着调用者只能将异常视为通常是致命的 - 除非他们想依赖 Hibernate 自己的异常层次结构。如果不将调用者绑定到实现策略,则无法捕获特定原因(例如,乐观锁定失败)。对于基于 Hibernate 的应用程序、不需要任何特殊异常处理或两者兼而有之的应用程序,这种权衡可能是可以接受的。
幸运的是,Spring 的 LocalSessionFactoryBean
支持 Hibernate 的 SessionFactory.getCurrentSession()
方法用于任何 Spring 事务策略,即使使用 HibernateTransactionManager
,也会返回当前 Spring 管理的事务 Session
。该方法的标准行为仍然是返回与正在进行的 JTA 事务关联的当前 Session
(如果有)。无论你使用 Spring 的 JtaTransactionManager
、EJB 容器管理的事务 (CMT) 还是 JTA,此行为都适用。
总而言之,你可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与 Spring 管理的事务。
# 声明式事务划分
我们建议你使用 Spring 的声明式事务支持,它允许你使用 AOP 事务拦截器替换 Java 代码中的显式事务划分 API 调用。你可以在 Spring 容器中使用 Java 注解或 XML 配置此事务拦截器。这种声明式事务功能使你可以使业务服务免于重复的事务划分代码,并将精力集中在添加业务逻辑上,这才是应用程序的真正价值所在。
你可以使用 @Transactional
注解来注解服务层,并指示 Spring 容器查找这些注解并为这些注解方法提供事务语义。以下示例展示了如何做到这一点:
public class ProductServiceImpl implements ProductService {
private ProductDao productDao;
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
@Transactional
public void increasePriceOfAllProductsInCategory(final String category) {
List productsToChange = this.productDao.loadProductsByCategory(category);
// ...
}
@Transactional(readOnly = true)
public List<Product> findAllProducts() {
return this.productDao.findAllProducts();
}
}
在容器中,你需要设置 PlatformTransactionManager
实现(作为一个 bean)和一个 <tx:annotation-driven/>
条目,选择在运行时进行 @Transactional
处理。以下示例展示了如何做到这一点:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- SessionFactory, DataSource, etc. omitted -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven/>
<bean id="myProductService" class="product.SimpleProductService">
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
# 编程式事务划分
你可以在应用程序的更高级别上划分事务,在跨越任意数量操作的较低级别的数据访问服务之上。对于周围业务服务的实现也没有任何限制。它只需要一个 Spring PlatformTransactionManager
。同样,后者可以来自任何地方,但最好是通过 setTransactionManager(..)
方法作为 bean 引用。此外,productDAO
应该由 setProductDao(..)
方法设置。以下代码段对显示了 Spring 应用程序上下文中的事务管理器和业务服务定义以及业务方法实现的示例:
<beans>
<bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
<bean id="myProductService" class="product.ProductServiceImpl">
<property name="transactionManager" ref="myTxManager"/>
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
public class ProductServiceImpl implements ProductService {
private TransactionTemplate transactionTemplate;
private ProductDao productDao;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
public void increasePriceOfAllProductsInCategory(final String category) {
this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
List productsToChange = this.productDao.loadProductsByCategory(category);
// do the price increase...
}
});
}
}
Spring 的 TransactionInterceptor
允许使用回调代码抛出任何检查的应用程序异常,而 TransactionTemplate
仅限于回调中的未检查异常。TransactionTemplate
在出现未检查的应用程序异常或事务被应用程序标记为仅回滚的情况下(通过设置 TransactionStatus
)触发回滚。默认情况下,TransactionInterceptor
的行为方式相同,但允许每个方法配置可回滚策略。
# 事务管理策略
TransactionTemplate
和 TransactionInterceptor
都将实际的事务处理委托给 PlatformTransactionManager
实例(可以是 HibernateTransactionManager
(对于单个 Hibernate SessionFactory
),在底层使用 ThreadLocal
Session
)或 JtaTransactionManager
(委托给容器的 JTA 子系统)用于 Hibernate 应用程序。你甚至可以使用自定义 PlatformTransactionManager
实现。从原生 Hibernate 事务管理切换到 JTA(例如,在面对应用程序某些部署的分布式事务需求时)只是配置问题。你可以使用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器。事务划分和数据访问代码都可以正常工作,因为它们使用通用的事务管理 API。
对于跨多个 Hibernate 会话工厂的分布式事务,你可以将 JtaTransactionManager
作为事务策略与多个 LocalSessionFactoryBean
定义结合使用。然后,每个 DAO 都会将一个特定的 SessionFactory
引用传递到其相应的 bean 属性中。如果所有底层 JDBC 数据源都是事务容器数据源,则业务服务可以跨任意数量的 DAO 和任意数量的会话工厂划分事务,而无需特别注意,只要它使用 JtaTransactionManager
作为策略即可。
HibernateTransactionManager
和 JtaTransactionManager
都允许使用 Hibernate 正确处理 JVM 级别的缓存,而无需特定于容器的事务管理器查找或 JCA 连接器(如果你不使用 EJB 来启动事务)。
HibernateTransactionManager
可以将 Hibernate JDBC Connection
导出到特定 DataSource
的普通 JDBC 访问代码。如果你仅访问一个数据库,则此功能允许使用混合 Hibernate 和 JDBC 数据访问进行高级事务划分,而完全无需 JTA。如果你已通过 LocalSessionFactoryBean
类的 dataSource
属性使用 DataSource
设置传入的 SessionFactory
,则 HibernateTransactionManager
会自动将 Hibernate 事务公开为 JDBC 事务。或者,你可以通过 HibernateTransactionManager
类的 dataSource
属性显式指定应为其公开事务的 DataSource
。
对于 JTA 样式的实际资源连接的延迟检索,Spring 为目标连接池提供了一个相应的 DataSource
代理类:请参阅 LazyConnectionDataSourceProxy
(opens new window)。这对于 Hibernate 只读事务尤其有用,这些事务通常可以从本地缓存而不是访问数据库进行处理。
# 比较容器管理和本地定义的资源
你可以在容器管理的 JNDI SessionFactory
和本地定义的 SessionFactory
之间切换,而无需更改任何一行应用程序代码。是否将资源定义保留在容器中或本地保留在应用程序中主要取决于你使用的事务策略。与 Spring 定义的本地 SessionFactory
相比,手动注册的 JNDI SessionFactory
不提供任何好处。通过 Hibernate 的 JCA 连接器部署 SessionFactory
提供了参与 Jakarta EE 服务器管理基础设施的附加价值,但除此之外没有增加实际价值。
Spring 的事务支持不绑定到容器。当配置为 JTA 以外的任何策略时,事务支持也可以在独立或测试环境中工作。尤其是在单数据库事务的典型情况下,Spring 的单资源本地事务支持是 JTA 的轻量级且功能强大的替代方案。当你使用本地 EJB 无状态会话 bean 来驱动事务时,你既依赖于 EJB 容器,也依赖于 JTA,即使你仅访问单个数据库并仅使用无状态会话 bean 通过容器管理的事务提供声明式事务。以编程方式直接使用 JTA 也需要 Jakarta EE 环境。
Spring 驱动的事务可以与本地定义的 Hibernate SessionFactory
以及本地 JDBC DataSource
一起使用,前提是它们访问单个数据库。因此,只有在你有分布式事务需求时,才需要使用 Spring 的 JTA 事务策略。JCA 连接器需要特定于容器的部署步骤,并且(显然)首先需要 JCA 支持。此配置比使用本地资源定义和 Spring 驱动的事务部署简单的 Web 应用程序需要更多的工作。
考虑到所有因素,如果你不使用 EJB,请坚持使用本地 SessionFactory
设置和 Spring 的 HibernateTransactionManager
或 JtaTransactionManager
。你可以获得所有好处,包括正确的事务 JVM 级别缓存和分布式事务,而无需容器部署带来的不便。仅当与 EJB 结合使用时,通过 JCA 连接器注册 Hibernate SessionFactory
的 JNDI 才有价值。
# Hibernate 出现伪造的应用程序服务器警告
在某些具有非常严格的 XADataSource
实现的 JTA 环境中(目前是一些 WebLogic Server 和 WebSphere 版本),当配置 Hibernate 时未考虑该环境的 JTA 事务管理器,则应用程序服务器日志中可能会出现伪造的警告或异常。这些警告或异常指示正在访问的连接不再有效或 JDBC 访问不再有效,可能是因为事务不再处于活动状态。例如,这是 WebLogic 中的一个实际异常:
java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.
另一个常见问题是 JTA 事务之后的连接泄漏,Hibernate 会话(以及可能的基础 JDBC 连接)没有正确关闭。
你可以通过使 Hibernate 了解 JTA 事务管理器(它与 Spring 一起同步)来解决此类问题。你有两个选择来做到这一点:
- 将你的 Spring
JtaTransactionManager
bean 传递给你的 Hibernate 设置。最简单的方法是将 bean 引用到你的LocalSessionFactoryBean
bean 的jtaTransactionManager
属性中。然后,Spring 将相应的 JTA 策略提供给 Hibernate。 - 你还可以显式配置 Hibernate 的 JTA 相关属性,特别是
hibernate.transaction.coordinator_class
、hibernate.connection.handling_mode
以及LocalSessionFactoryBean
的hibernateProperties
中的hibernate.transaction.jta.platform
(有关这些属性的详细信息,请参阅 Hibernate 的手册)。
本节的其余部分描述了在 Hibernate 了解和不了解 JTA PlatformTransactionManager
的情况下发生的事件序列。
当 Hibernate 未配置为了解任何 JTA 事务管理器时,当 JTA 事务提交时会发生以下事件:
- JTA 事务提交。
- Spring 的
JtaTransactionManager
与 JTA 事务同步,因此 JTA 事务管理器通过afterCompletion
回调调用它。 - 在其他活动中,此同步可以通过 Hibernate 的
afterTransactionCompletion
回调触发 Spring 对 Hibernate 的回调(用于清除 Hibernate 缓存),然后显式调用 Hibernate 会话上的close()
,这会导致 Hibernate 尝试close()
JDBC 连接。 - 在某些环境中,此
Connection.close()
调用然后会触发警告或错误,因为应用程序服务器不再认为Connection
可用,因为事务已提交。
当 Hibernate 配置为了解 JTA 事务管理器时,当 JTA 事务提交时会发生以下事件:
- JTA 事务已准备好提交。
- Spring 的
JtaTransactionManager
与 JTA 事务同步,因此 JTA 事务管理器通过beforeCompletion
回调调用该事务。 - Spring 知道 Hibernate 本身已与 JTA 事务同步,并且行为与之前的方案不同。特别是,它与 Hibernate 的事务资源管理保持一致。
- JTA 事务提交。
- Hibernate 与 JTA 事务同步,因此 JTA 事务管理器通过
afterCompletion
回调调用该事务,并且可以正确清除其缓存。
# JPA
Spring JPA(位于 org.springframework.orm.jpa
包下)以类似于与 Hibernate 集成的方式,为 Java Persistence API (opens new window) 提供全面的支持,同时了解底层实现,以提供额外的功能。
# 三种在 Spring 环境中设置 JPA 的方式
Spring JPA 支持提供三种设置 JPA EntityManagerFactory
的方式,应用程序使用该工厂来获取实体管理器。
# 使用 LocalEntityManagerFactoryBean
你只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试。
LocalEntityManagerFactoryBean
创建一个适用于简单部署环境的 EntityManagerFactory
,在该环境中,应用程序仅使用 JPA 进行数据访问。该工厂 bean 使用 JPA PersistenceProvider
自动检测机制(根据 JPA 的 Java SE 引导),并且在大多数情况下,只需要你指定持久性单元名称。以下 XML 示例配置了这样一个 bean:
<beans>
<bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="myPersistenceUnit"/>
</bean>
</beans>
这种形式的 JPA 部署是最简单也是最受限制的。你不能引用现有的 JDBC DataSource
bean 定义,并且不支持全局事务。此外,持久类的织入(字节码转换)是提供程序特定的,通常需要在启动时指定特定的 JVM 代理。此选项仅适用于独立应用程序和测试环境,JPA 规范就是为此设计的。
# 从 JNDI 获取 EntityManagerFactory
你可以在部署到 Jakarta EE 服务器时使用此选项。请查看服务器的文档,了解如何将自定义 JPA 提供程序部署到服务器中,从而允许使用与服务器默认提供程序不同的提供程序。
从 JNDI(例如在 Jakarta EE 环境中)获取 EntityManagerFactory
只需要更改 XML 配置,如以下示例所示:
<beans>
<jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>
此操作假定标准的 Jakarta EE 引导。Jakarta EE 服务器自动检测持久性单元(实际上是应用程序 jar 中的 META-INF/persistence.xml
文件)和 Jakarta EE 部署描述符(例如,web.xml
)中的 persistence-unit-ref
条目,并为这些持久性单元定义环境命名上下文位置。
在这种情况下,整个持久性单元部署,包括持久类的织入(字节码转换),都由 Jakarta EE 服务器负责。JDBC DataSource
通过 META-INF/persistence.xml
文件中的 JNDI 位置定义。EntityManager
事务与服务器的 JTA 子系统集成。Spring 仅使用获得的 EntityManagerFactory
,通过依赖注入将其传递给应用程序对象,并管理持久性单元的事务(通常通过 JtaTransactionManager
)。
如果在同一应用程序中使用多个持久性单元,则从此类 JNDI 检索的持久性单元的 bean 名称应与应用程序用于引用它们的持久性单元名称匹配(例如,在 @PersistenceUnit
和 @PersistenceContext
注释中)。
# 使用 LocalContainerEntityManagerFactoryBean
你可以在基于 Spring 的应用程序环境中使用此选项来获得完整的 JPA 功能。这包括 Web 容器(如 Tomcat)、独立应用程序以及具有复杂持久性要求的集成测试。
LocalContainerEntityManagerFactoryBean
让你完全控制 EntityManagerFactory
配置,并且适用于需要细粒度自定义的环境。LocalContainerEntityManagerFactoryBean
基于 persistence.xml
文件、提供的 dataSourceLookup
策略和指定的 loadTimeWeaver
创建 PersistenceUnitInfo
实例。因此,可以在 JNDI 之外使用自定义数据源并控制织入过程。以下示例显示了 LocalContainerEntityManagerFactoryBean
的典型 bean 定义:
<beans>
<bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="someDataSource"/>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
</property>
</bean>
</beans>
以下示例显示了典型的 persistence.xml
文件:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
<persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
<mapping-file>META-INF/orm.xml</mapping-file>
<exclude-unlisted-classes/>
</persistence-unit>
</persistence>
注意:
<exclude-unlisted-classes/>
快捷方式表示不应发生对带注释的实体类的扫描。显式的 'true' 值 (<exclude-unlisted-classes>true</exclude-unlisted-classes/>
) 也表示不扫描。<exclude-unlisted-classes>false</exclude-unlisted-classes/>
会触发扫描。但是,如果你希望进行实体类扫描,我们建议省略exclude-unlisted-classes
元素。
使用 LocalContainerEntityManagerFactoryBean
是最强大的 JPA 设置选项,允许在应用程序中进行灵活的本地配置。它支持链接到现有的 JDBC DataSource
,支持本地和全局事务等等。但是,它也对运行时环境提出了要求,例如,如果持久性提供程序需要字节码转换,则需要提供具有织入功能的类加载器。
此选项可能与 Jakarta EE 服务器的内置 JPA 功能冲突。在完整的 Jakarta EE 环境中,请考虑从 JNDI 获取 EntityManagerFactory
。或者,在 LocalContainerEntityManagerFactoryBean
定义上指定自定义 persistenceXmlLocation
(例如,META-INF/my-persistence.xml),并在应用程序 jar 文件中仅包含具有该名称的描述符。由于 Jakarta EE 服务器仅查找默认的 META-INF/persistence.xml
文件,因此它会忽略此类自定义持久性单元,从而避免了与 Spring 驱动的 JPA 设置的冲突。
何时需要加载时织入?
并非所有 JPA 提供程序都需要 JVM 代理。Hibernate 是一个不需要的例子。如果你的提供程序不需要代理,或者你有其他替代方案,例如通过自定义编译器或 Ant 任务在构建时应用增强,则不应使用加载时织入。
LoadTimeWeaver
接口是 Spring 提供的类,它允许以特定的方式插入 JPA ClassTransformer
实例,具体取决于环境是 Web 容器还是应用程序服务器。通过 agent (opens new window) 连接 ClassTransformers
通常效率不高。代理针对整个虚拟机工作并检查加载的每个类,这在生产服务器环境中通常是不希望的。
Spring 为各种环境提供了许多 LoadTimeWeaver
实现,允许仅为每个类加载器而不是每个 VM 应用 ClassTransformer
实例。
有关 LoadTimeWeaver
实现及其设置的更多信息,请参阅 AOP 章节中的 Spring configuration (opens new window),无论是通用的还是针对各种平台(如 Tomcat、JBoss 和 WebSphere)定制的。
如 Spring configuration (opens new window) 中所述,你可以使用 @EnableLoadTimeWeaving
注释或 context:load-time-weaver
XML 元素配置上下文范围的 LoadTimeWeaver
。所有 JPA LocalContainerEntityManagerFactoryBean
实例都会自动选择此类全局织入器。以下示例显示了设置加载时织入器的首选方法,它提供对平台的自动检测(例如,Tomcat 的具有织入功能的类加载器或 Spring 的 JVM 代理)以及将织入器自动传播到所有具有织入器感知的 bean:
<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>
但是,如果需要,你可以通过 loadTimeWeaver
属性手动指定专用织入器,如以下示例所示:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</property>
</bean>
无论如何配置 LTW,通过使用此技术,依赖于检测的 JPA 应用程序都可以在目标平台(例如,Tomcat)中运行,而无需代理。当托管应用程序依赖于不同的 JPA 实现时,这一点尤其重要,因为 JPA 转换器仅在类加载器级别应用,因此彼此隔离。
# 处理多个持久性单元
对于依赖于多个持久性单元位置(例如,存储在类路径中的各种 JAR 中)的应用程序,Spring 提供了 PersistenceUnitManager
作为中央存储库,以避免持久性单元发现过程,这可能会很昂贵。默认实现允许指定多个位置。解析这些位置,然后通过持久性单元名称检索它们。(默认情况下,在类路径中搜索 META-INF/persistence.xml
文件。)以下示例配置多个位置:
<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
<property name="persistenceXmlLocations">
<list>
<value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
<value>classpath:/my/package/**/custom-persistence.xml</value>
<value>classpath*:META-INF/persistence.xml</value>
</list>
</property>
<property name="dataSources">
<map>
<entry key="localDataSource" value-ref="local-db"/>
<entry key="remoteDataSource" value-ref="remote-db"/>
</map>
</property>
<!-- 如果未指定数据源,则使用此数据源 -->
<property name="defaultDataSource" ref="remoteDataSource"/>
</bean>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitManager" ref="pum"/>
<property name="persistenceUnitName" value="myCustomUnit"/>
</bean>
默认实现允许自定义 PersistenceUnitInfo
实例(在将它们馈送到 JPA 提供程序之前),可以通过声明方式(通过其属性,这会影响所有托管单元)或以编程方式(通过 PersistenceUnitPostProcessor
,允许选择持久性单元)。如果未指定 PersistenceUnitManager
,则由 LocalContainerEntityManagerFactoryBean
在内部创建和使用一个。
# 后台引导
LocalContainerEntityManagerFactoryBean
通过 bootstrapExecutor
属性支持后台引导,如以下示例所示:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="bootstrapExecutor">
<bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
</property>
</bean>
实际的 JPA 提供程序引导程序被移交给指定的执行程序,然后并行运行到应用程序引导程序线程。公开的 EntityManagerFactory
代理可以注入到其他应用程序组件中,甚至能够响应 EntityManagerFactoryInfo
配置检查。但是,一旦其他组件访问实际的 JPA 提供程序(例如,调用 createEntityManager
),这些调用就会阻塞,直到后台引导完成。特别是,当你使用 Spring Data JPA 时,请确保也为其存储库设置延迟引导。
从 6.2 开始,JPA 初始化在上下文刷新完成之前强制执行,等待异步引导在那时完成。这使得完全初始化的数据库基础架构的可用性可预测,并允许在 ContextRefreshedEvent
侦听器等中进行自定义的后初始化逻辑。不建议将此类应用程序级别的数据库初始化放入 @PostConstruct
方法或类似方法中;最好将其放在 Lifecycle.start
(如果适用)或 ContextRefreshedEvent
侦听器中。
# 基于 JPA 实现 DAO:EntityManagerFactory
和 EntityManager
注意: 虽然
EntityManagerFactory
实例是线程安全的,但EntityManager
实例不是。注入的 JPAEntityManager
的行为类似于从应用程序服务器的 JNDI 环境中获取的EntityManager
,如 JPA 规范中所定义。它将所有调用委托给当前的事务EntityManager
(如果有)。否则,它会回退到每个操作新创建的EntityManager
,从而有效地使其用法线程安全。
可以通过使用注入的 EntityManagerFactory
或 EntityManager
,针对普通的 JPA 编写代码,而无需任何 Spring 依赖项。如果启用了 PersistenceAnnotationBeanPostProcessor
,则 Spring 可以理解字段和方法级别的 @PersistenceUnit
和 @PersistenceContext
注释。以下示例显示了一个普通的 JPA DAO 实现,该实现使用 @PersistenceUnit
注释:
public class ProductDaoImpl implements ProductDao {
private EntityManagerFactory emf;
@PersistenceUnit
public void setEntityManagerFactory(EntityManagerFactory emf) {
this.emf = emf;
}
public Collection loadProductsByCategory(String category) {
EntityManager em = this.emf.createEntityManager();
try {
Query query = em.createQuery("from Product as p where p.category = ?1");
query.setParameter(1, category);
return query.getResultList();
}
finally {
if (em != null) {
em.close();
}
}
}
}
前面的 DAO 没有对 Spring 的依赖性,并且仍然可以很好地适应 Spring 应用程序上下文。此外,DAO 利用注释来要求注入默认的 EntityManagerFactory
,如以下示例 bean 定义所示:
<beans>
<!-- JPA 注释的 bean 后处理器 -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
<bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>
作为显式定义 PersistenceAnnotationBeanPostProcessor
的替代方法,请考虑在应用程序上下文配置中使用 Spring context:annotation-config
XML 元素。这样做会自动注册所有基于注释的配置的 Spring 标准后处理器,包括 CommonAnnotationBeanPostProcessor
等。
考虑以下示例:
<beans>
<!-- 所有标准配置注释的后处理器 -->
<context:annotation-config/>
<bean id="myProductDao" class="product.ProductDaoImpl"/>
</beans>
这种 DAO 的主要问题是它始终通过工厂创建一个新的 EntityManager
。你可以通过请求注入事务 EntityManager
(也称为“共享 EntityManager”,因为它是一个共享的、线程安全的代理,用于实际的事务 EntityManager)来避免这种情况,而不是注入工厂。以下示例显示了如何执行此操作:
public class ProductDaoImpl implements ProductDao {
@PersistenceContext
private EntityManager em;
public Collection loadProductsByCategory(String category) {
Query query = em.createQuery("from Product as p where p.category = :category");
query.setParameter("category", category);
return query.getResultList();
}
}
@PersistenceContext
注释有一个名为 type
的可选属性,默认为 PersistenceContextType.TRANSACTION
。你可以使用此默认值来接收共享的 EntityManager
代理。另一种选择 PersistenceContextType.EXTENDED
则完全不同。这会导致所谓的扩展 EntityManager
,它不是线程安全的,因此不得在并发访问的组件中使用,例如 Spring 管理的单例 bean。扩展 EntityManager
实例仅应在有状态组件中使用,例如,这些组件位于会话中,EntityManager
的生命周期不与当前事务相关联,而是完全取决于应用程序。
方法级别和字段级别注入
你可以将指示依赖项注入的注释(例如
@PersistenceUnit
和@PersistenceContext
)应用于类中的字段或方法,因此表达式为“方法级别注入”和“字段级别注入”。字段级别注释简洁且易于使用,而方法级别注释允许进一步处理注入的依赖项。在这两种情况下,成员可见性(公共、受保护或私有)都无关紧要。那么类级别注释呢?
在 Jakarta EE 平台上,它们用于依赖项声明,而不是用于资源注入。
注入的 EntityManager
由 Spring 管理(了解正在进行的事务)。即使新的 DAO 实现使用 EntityManager
的方法级别注入而不是 EntityManagerFactory
,由于使用了注释,因此 bean 定义中也不需要进行任何更改。
这种 DAO 风格的主要优点是它仅依赖于 Java Persistence API。不需要导入任何 Spring 类。此外,由于 JPA 注释是可以理解的,因此 Spring 容器会自动应用注入。从非侵入性的角度来看,这是很有吸引力的,并且对于 JPA 开发人员来说感觉更自然。
# 基于 @Autowired
实现 DAO(通常使用基于构造函数的注入)
@PersistenceUnit
和 @PersistenceContext
只能在方法和字段上声明。那么如何通过构造函数和其他 @Autowired
注入点提供 JPA 资源呢?
只要目标定义为 bean(例如,通过 LocalContainerEntityManagerFactoryBean
),就可以通过构造函数和 @Autowired
字段/方法轻松注入 EntityManagerFactory
。注入点按类型与原始 EntityManagerFactory
定义完全匹配。
但是,@PersistenceContext
样式的共享 EntityManager
引用不能直接用于常规的依赖项注入。为了使其可用于 @Autowired
所需的基于类型的匹配,请考虑定义 SharedEntityManagerBean
作为 EntityManagerFactory
定义的补充:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>
<bean id="em" class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
<property name="entityManagerFactory" ref="emf"/>
</bean>
或者,你可以定义一个基于 SharedEntityManagerCreator
的 @Bean
方法:
@Bean("em")
public static EntityManager sharedEntityManager(EntityManagerFactory emf) {
return SharedEntityManagerCreator.createSharedEntityManager(emf);
}
如果存在多个持久性单元,则每个 EntityManagerFactory
定义都需要附带一个相应的 EntityManager
bean 定义,理想情况下,使用与不同的 EntityManagerFactory
定义匹配的限定符,以便通过 @Autowired @Qualifier("…")
区分持久性单元。
# Spring 驱动的 JPA 事务
JPA 的推荐策略是通过 JPA 的本机事务支持进行本地事务。Spring 的 JpaTransactionManager
针对任何常规 JDBC 连接池提供了许多从本地 JDBC 事务中已知的功能(例如,特定于事务的隔离级别和资源级别的只读优化),而无需 JTA 事务协调器和支持 XA 的资源。
Spring JPA 还允许配置的 JpaTransactionManager
将 JPA 事务公开给访问同一 DataSource
的 JDBC 访问代码,前提是注册的 JpaDialect
支持检索底层 JDBC Connection
。Spring 为 EclipseLink 和 Hibernate JPA 实现提供方言。
对于 JTA 样式的实际资源连接的延迟检索,Spring 为目标连接池提供了一个相应的 DataSource
代理类:请参阅 LazyConnectionDataSourceProxy (opens new window)。这对于 JPA 只读事务尤其有用,这些事务通常可以从本地缓存中处理,而无需访问数据库。
# 了解 JpaDialect
和 JpaVendorAdapter
作为一项高级功能,JpaTransactionManager
和 AbstractEntityManagerFactoryBean
的子类允许将自定义 JpaDialect
传递到 jpaDialect
bean 属性中。JpaDialect
实现可以启用 Spring 支持的以下高级功能,通常以供应商特定的方式:
- 应用特定的事务语义(例如,自定义隔离级别或事务超时)
- 检索事务 JDBC
Connection
(用于暴露给基于 JDBC 的 DAO) - 将
PersistenceException
高级转换为 Spring 的DataAccessException
这对于特殊事务语义和异常的高级转换尤其有价值。默认实现 (DefaultJpaDialect
) 不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。
提示: 作为主要用于 Spring 的全功能
LocalContainerEntityManagerFactoryBean
设置的更广泛的提供程序适配工具,JpaVendorAdapter
将JpaDialect
的功能与其他提供程序特定的默认值相结合。指定HibernateJpaVendorAdapter
或EclipseLinkJpaVendorAdapter
是为 Hibernate 或 EclipseLink 自动配置EntityManagerFactory
设置的最便捷方式。请注意,这些提供程序适配器主要设计用于 Spring 驱动的事务管理(即,与JpaTransactionManager
一起使用)。
有关其操作以及如何在 Spring 的 JPA 支持中使用的更多详细信息,请参阅 JpaDialect (opens new window) 和 JpaVendorAdapter (opens new window) javadoc。
# 使用 JTA 事务管理设置 JPA
作为 JpaTransactionManager
的替代方案,Spring 还允许通过 JTA 进行多资源事务协调,无论是在 Jakarta EE 环境中还是在使用独立的事务协调器(例如,Atomikos)的情况下。除了选择 Spring 的 JtaTransactionManager
而不是 JpaTransactionManager
之外,你还需要执行一些其他步骤:
- 底层 JDBC 连接池需要支持 XA,并且需要与你的事务协调器集成。这在 Jakarta EE 环境中通常很简单,可以通过 JNDI 公开不同类型的
DataSource
。有关详细信息,请参阅你的应用程序服务器文档。类似地,独立的事务协调器通常带有特殊的 XA 集成DataSource
变体。同样,请查看其文档。 - 需要为 JTA 配置 JPA
EntityManagerFactory
设置。这是特定于提供程序的,通常是通过在LocalContainerEntityManagerFactoryBean
上指定为jpaProperties
的特殊属性。对于 Hibernate,这些属性甚至是特定于版本的。有关详细信息,请参阅你的 Hibernate 文档。 - Spring 的
HibernateJpaVendorAdapter
强制执行某些面向 Spring 的默认值,例如连接释放模式on-close
,该模式与 Hibernate 5.0 中 Hibernate 自己的默认值匹配,但在 Hibernate 5.1+ 中不再匹配。对于 JTA 设置,请确保将持久性单元事务类型声明为“JTA”。或者,将 Hibernate 5.2 的hibernate.connection.handling_mode
属性设置为DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
以恢复 Hibernate 自己的默认值。有关相关说明,请参阅 Hibernate 的虚假应用程序服务器警告。 - 或者,考虑从你的应用程序服务器本身获取
EntityManagerFactory
(即,通过 JNDI 查找而不是本地声明的LocalContainerEntityManagerFactoryBean
)。服务器提供的EntityManagerFactory
可能需要在你的服务器配置中进行特殊定义(使部署的可移植性降低),但已为服务器的 JTA 环境设置。
# 用于 JPA 交互的本机 Hibernate 设置和本机 Hibernate 事务
本机 LocalSessionFactoryBean
设置与 HibernateTransactionManager
结合使用,允许与 @PersistenceContext
和其他 JPA 访问代码进行交互。Hibernate SessionFactory
现在以本机方式实现 JPA 的 EntityManagerFactory
接口,并且 Hibernate Session
句柄以本机方式是一个 JPA EntityManager
。Spring 的 JPA 支持工具会自动检测本机 Hibernate 会话。
因此,在这种情况下,本机 Hibernate 设置可以替代标准的 JPA LocalContainerEntityManagerFactoryBean
和 JpaTransactionManager
组合,从而允许在同一本地事务中与 SessionFactory.getCurrentSession()
(以及 HibernateTemplate
)以及 @PersistenceContext EntityManager
进行交互。这种设置还提供了更强大的 Hibernate 集成和更大的配置灵活性,因为它不受 JPA 引导合同的约束。
在这种情况下,你不需要 HibernateJpaVendorAdapter
配置,因为 Spring 的本机 Hibernate 设置提供了更多的功能(例如,自定义 Hibernate Integrator 设置、Hibernate 5.3 bean 容器集成以及对只读事务的更强大的优化)。最后但并非最不重要的一点是,你还可以通过 LocalSessionFactoryBuilder
表示本机 Hibernate 设置,从而与 @Bean
样式的配置无缝集成(不涉及 FactoryBean
)。
注意:
LocalSessionFactoryBean
和LocalSessionFactoryBuilder
支持后台引导,就像 JPALocalContainerEntityManagerFactoryBean
一样。在
LocalSessionFactoryBean
上,这可以通过bootstrapExecutor
属性获得。在编程的LocalSessionFactoryBuilder
上,重载的buildSessionFactory
方法采用引导执行程序参数。
# 使用 Object-XML 映射器编组 XML
# 简介
本章介绍 Spring 的 Object-XML 映射支持。Object-XML 映射(简称 O-X 映射)是将 XML 文档与对象相互转换的过程。此转换过程也称为 XML 编组或 XML 序列化。本章交替使用这些术语。
在 O-X 映射领域中,编组器负责将对象(图)序列化为 XML。类似地,解组器将 XML 反序列化为对象图。此 XML 可以采用 DOM 文档、输入或输出流或 SAX 处理程序的形式。
使用 Spring 进行 O/X 映射的一些好处是:
# 易于配置
Spring 的 bean 工厂使配置编组器变得容易,而无需构造 JAXB 上下文、JiBX 绑定工厂等。你可以像配置应用程序上下文中的任何其他 bean 一样配置编组器。此外,XML 基于命名空间的配置可用于许多编组器,从而使配置更加简单。
# 一致的接口
Spring 的 O-X 映射通过两个全局接口运行:Marshaller
(opens new window) 和 Unmarshaller
(opens new window)。这些抽象使你可以相对轻松地切换 O-X 映射框架,而对执行编组的类几乎不需要更改或根本不需要更改。这种方法还有一个额外的好处,即可以使用混合匹配方法(例如,一些编组使用 JAXB 执行,一些使用 XStream 执行)以非侵入方式进行 XML 编组,从而使你可以使用每种技术的优势。
# 一致的异常层次结构
Spring 提供从底层 O-X 映射工具的异常到其自身的异常层次结构的转换,其中 XmlMappingException
作为根异常。这些运行时异常包装原始异常,以便不会丢失任何信息。
# Marshaller
和 Unmarshaller
如简介中所述,编组器将对象序列化为 XML,而解组器将 XML 流反序列化为对象。本节介绍用于此目的的两个 Spring 接口。
# 理解 Marshaller
Spring 将所有编组操作抽象在 org.springframework.oxm.Marshaller
接口之后,其主要方法如下:
public interface Marshaller {
/**
* Marshal the object graph with the given root into the provided Result.
*/
void marshal(Object graph, Result result) throws XmlMappingException, IOException;
}
Marshaller
接口有一个主要方法,该方法将给定对象编组到给定的 javax.xml.transform.Result
。结果是一个标记接口,基本上表示 XML 输出抽象。具体实现包装各种 XML 表示形式,如下表所示:
Result 实现 | 包装 XML 表示形式 |
---|---|
DOMResult | org.w3c.dom.Node |
SAXResult | org.xml.sax.ContentHandler |
StreamResult | java.io.File 、java.io.OutputStream 或 java.io.Writer |
注意: 尽管
marshal()
方法接受一个普通对象作为其第一个参数,但大多数Marshaller
实现无法处理任意对象。相反,必须在映射文件中映射对象类,用注释标记,向编组器注册或具有公共基类。请参阅本章后面的部分,以确定你的 O-X 技术如何管理此问题。
# 理解 Unmarshaller
与 Marshaller
类似,我们有 org.springframework.oxm.Unmarshaller
接口,如下面的清单所示:
public interface Unmarshaller {
/**
* Unmarshal the given provided Source into an object graph.
*/
Object unmarshal(Source source) throws XmlMappingException, IOException;
}
此接口也只有一个方法,该方法从给定的 javax.xml.transform.Source
(XML 输入抽象)读取并返回读取的对象。与 Result
一样,Source
是一个标记接口,它有三个具体的实现。每个都包装不同的 XML 表示形式,如下表所示:
Source 实现 | 包装 XML 表示形式 |
---|---|
DOMSource | org.w3c.dom.Node |
SAXSource | org.xml.sax.InputSource 和 org.xml.sax.XMLReader |
StreamSource | java.io.File 、java.io.InputStream 或 java.io.Reader |
即使有两个单独的编组接口(Marshaller
和 Unmarshaller
),Spring-WS 中的所有实现都在一个类中实现两者。这意味着你可以连接一个编组器类,并在 applicationContext.xml
中将其同时称为编组器和解组器。
# 理解 XmlMappingException
Spring 将底层 O-X 映射工具的异常转换为其自身的异常层次结构,其中 XmlMappingException
作为根异常。这些运行时异常包装原始异常,以便不会丢失任何信息。
此外,MarshallingFailureException
和 UnmarshallingFailureException
提供了编组和解组操作之间的区别,即使底层 O-X 映射工具没有这样做。
O-X 映射异常层次结构如下图所示:
# 使用 Marshaller
和 Unmarshaller
你可以将 Spring 的 OXM 用于各种情况。在以下示例中,我们使用它将 Spring 管理的应用程序的设置编组为 XML 文件。在以下示例中,我们使用一个简单的 JavaBean 来表示设置:
public class Settings {
private boolean fooEnabled;
public boolean isFooEnabled() {
return fooEnabled;
}
public void setFooEnabled(boolean fooEnabled) {
this.fooEnabled = fooEnabled;
}
}
应用程序类使用此 bean 来存储其设置。除了 main 方法外,该类还有两个方法:saveSettings()
将设置 bean 保存到名为 settings.xml
的文件中,loadSettings()
再次加载这些设置。以下 main()
方法构造一个 Spring 应用程序上下文并调用这两个方法:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
public class Application {
private static final String FILE_NAME = "settings.xml";
private Settings settings = new Settings();
private Marshaller marshaller;
private Unmarshaller unmarshaller;
public void setMarshaller(Marshaller marshaller) {
this.marshaller = marshaller;
}
public void setUnmarshaller(Unmarshaller unmarshaller) {
this.unmarshaller = unmarshaller;
}
public void saveSettings() throws IOException {
try (FileOutputStream os = new FileOutputStream(FILE_NAME)) {
this.marshaller.marshal(settings, new StreamResult(os));
}
}
public void loadSettings() throws IOException {
try (FileInputStream is = new FileInputStream(FILE_NAME)) {
this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is));
}
}
public static void main(String[] args) throws IOException {
ApplicationContext appContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
Application application = (Application) appContext.getBean("application");
application.saveSettings();
application.loadSettings();
}
}
Application
需要设置 marshaller
和 unmarshaller
属性。我们可以使用以下 applicationContext.xml
来做到这一点:
<beans>
<bean id="application" class="Application">
<property name="marshaller" ref="xstreamMarshaller" />
<property name="unmarshaller" ref="xstreamMarshaller" />
</bean>
<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>
</beans>
此应用程序上下文使用 XStream,但我们可以使用本章后面描述的任何其他编组器实例。请注意,默认情况下,XStream 不需要任何进一步的配置,因此 bean 定义非常简单。另请注意,XStreamMarshaller
同时实现了 Marshaller
和 Unmarshaller
,因此我们可以在应用程序的 marshaller
和 unmarshaller
属性中引用 xstreamMarshaller
bean。
此示例应用程序生成以下 settings.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>
# XML 配置命名空间
你可以通过使用 OXM 命名空间中的标签来更简洁地配置编组器。要使这些标签可用,你必须首先在 XML 配置文件的前导中引用适当的模式。以下示例显示了如何执行此操作:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oxm="http://www.springframework.org/schema/oxm" <!-- (1) -->
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/oxm
https://www.springframework.org/schema/oxm/spring-oxm.xsd"> <!-- (2) -->
- 引用
oxm
模式。 - 指定
oxm
模式位置。
每个标签都在其各自的编组器的部分中进行了解释。但是,作为一个例子,JAXB2 编组器的配置可能类似于以下内容:
<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>
# JAXB
JAXB 绑定编译器将 W3C XML 模式转换为一个或多个 Java 类、一个 jaxb.properties
文件,以及可能的一些资源文件。JAXB 还提供了一种从带注释的 Java 类生成模式的方法。
Spring 支持 JAXB 2.0 API 作为 XML 编组策略,遵循Marshaller 和 Unmarshaller中描述的 Marshaller
和 Unmarshaller
接口。相应的集成类位于 org.springframework.oxm.jaxb
包中。
# 使用 Jaxb2Marshaller
Jaxb2Marshaller
类实现了 Spring 的 Marshaller
和 Unmarshaller
接口。它需要一个上下文路径才能运行。你可以通过设置 contextPath
属性来设置上下文路径。上下文路径是包含模式派生类的以冒号分隔的 Java 包名称列表。它还提供了一个 classesToBeBound
属性,该属性允许你设置一个编组器支持的类数组。通过将一个或多个模式资源指定给 bean 来执行模式验证,如以下示例所示:
<beans>
<bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="classesToBeBound">
<list>
<value>org.springframework.oxm.jaxb.Flight</value>
<value>org.springframework.oxm.jaxb.Flights</value>
</list>
</property>
<property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/>
</bean>
...
</beans>
# XML 配置命名空间
jaxb2-marshaller
元素配置一个 org.springframework.oxm.jaxb.Jaxb2Marshaller
,如以下示例所示:
<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>
或者,你可以通过使用 class-to-be-bound
子元素来提供要绑定到编组器的类列表:
<oxm:jaxb2-marshaller id="marshaller">
<oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Airport"/>
<oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Flight"/>
...
</oxm:jaxb2-marshaller>
下表描述了可用的属性:
属性 | 描述 | 必需 |
---|---|---|
id | 编组器的 ID | 否 |
contextPath | JAXB 上下文路径 | 否 |
# JiBX
JiBX 框架提供了一种类似于 Hibernate 为 ORM 提供的解决方案:绑定定义定义了 Java 对象与 XML 相互转换的规则。在准备好绑定并编译类之后,JiBX 绑定编译器会增强类文件并添加代码以处理将类的实例从 XML 转换或转换为 XML。
有关 JiBX 的更多信息,请参见 JiBX 网站 (opens new window)。Spring 集成类位于 org.springframework.oxm.jibx
包中。
# 使用 JibxMarshaller
JibxMarshaller
类实现了 Marshaller
和 Unmarshaller
接口。要运行,它需要要编组到的类的名称,你可以使用 targetClass
属性设置该名称。可选地,你可以通过设置 bindingName
属性来设置绑定名称。在以下示例中,我们绑定 Flights
类:
<beans>
<bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
<property name="targetClass">org.springframework.oxm.jibx.Flights</property>
</bean>
...
</beans>
JibxMarshaller
配置为单个类。如果要编组多个类,则必须配置多个具有不同 targetClass
属性值的 JibxMarshaller
实例。
# XML 配置命名空间
jibx-marshaller
标签配置一个 org.springframework.oxm.jibx.JibxMarshaller
,如以下示例所示:
<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>
下表描述了可用的属性:
属性 | 描述 | 必需 |
---|---|---|
id | 编组器的 ID | 否 |
target-class | 此编组器的目标类 | 是 |
bindingName | 此编组器使用的绑定名称 | 否 |
# XStream
XStream 是一个简单的库,用于将对象序列化为 XML 并再次序列化回来。它不需要任何映射并生成干净的 XML。
有关 XStream 的更多信息,请参见 XStream 网站 (opens new window)。Spring 集成类位于 org.springframework.oxm.xstream
包中。
# 使用 XStreamMarshaller
XStreamMarshaller
不需要任何配置,并且可以直接在应用程序上下文中配置。要进一步自定义 XML,你可以设置一个别名映射,该映射由映射到类的字符串别名组成,如以下示例所示:
<beans>
<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="aliases">
<props>
<prop key="Flight">org.springframework.oxm.xstream.Flight</prop>
</props>
</property>
</bean>
...
</beans>
警告: 默认情况下,XStream 允许解组任意类,这可能导致不安全的 Java 序列化效果。因此,我们不建议使用 XStreamMarshaller
从外部来源(即 Web)解组 XML,因为这可能导致安全漏洞。
如果你选择使用 XStreamMarshaller
从外部来源解组 XML,请在 XStreamMarshaller
上设置 supportedClasses
属性,如以下示例所示:
<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/>
...
</bean>
这样做可确保只有注册的类才有资格进行解组。
此外,你可以注册自定义转换器 (opens new window),以确保只能解组你支持的类。除了显式支持应支持的域类的转换器之外,你可能希望将 CatchAllConverter
作为列表中的最后一个转换器添加。结果,优先级较低且可能存在安全漏洞的默认 XStream 转换器不会被调用。
注意: 请注意,XStream 是一个 XML 序列化库,而不是一个数据绑定库。因此,它对命名空间的支持有限。因此,它不太适合在 Web 服务中使用。
# 总结
本文介绍了 Spring 对于数据库访问的支持,你可以通过JDBC、R2DBC、ORM等不通方式来访问数据库。
此外,还介绍了 Spring 的 OXM 支持。你已经了解了 Spring 如何通过 Marshaller
和 Unmarshaller
接口提供 O/X 映射支持。
希望本文能帮助你更好地理解 Spring 的数据库访问和 O/X 映射支持。
祝你变得更强!
- 01
- Spring中的Web访问:响应式栈 WebFlux06-28
- 02
- Spring中的Web访问:WebSocket支持06-19
- 03
- Spring中的Web访问:Servlet API支持06-02