Appearance
Spring 数据访问:使用 JDBC 的数据访问 - Part1
Tip:基于 Spring Core 5.3.30 版本。
Spring 框架 JDBC 抽象提供的价值可能最好地体现在下面的表格中所概述的一系列操作中。该表格显示了 Spring 负责的操作和你需要负责的操作。
| 操作 | Spring | 你 |
|---|---|---|
| 定义连接参数。 | √ | |
| 打开连接。 | √ | |
| 指定 SQL 语句。 | √ | |
| 声明参数并提供参数值。 | √ | |
| 准备并运行语句。 | √ | |
| 设置循环以遍历结果(如果有)。 | √ | |
| 对每次迭代进行工作。 | √ | |
| 处理任何异常。 | √ | |
| 处理事务。 | √ | |
| 关闭连接,语句和结果集。 | √ |
Spring 框架负责处理所有可能使 JDBC 成为繁琐 API 的底层细节。
1. 选择 JDBC 数据库访问的方法
你可以在多种方法中选择一种作为你的 JDBC 数据库访问的基础。除了三种风格的 JdbcTemplate,新的 SimpleJdbcInsert 和 SimpleJdbcCall 方法优化了数据库元数据,而 RDBMS Object 风格采取了一种更接近 JDO Query 设计的面向对象的方法。一旦你开始使用其中一种方法,你仍然可以混合和匹配以包含来自不同方法的特性。所有的方法都需要一个符合 JDBC 2.0 的驱动,一些高级特性需要 JDBC 3.0 驱动。
JdbcTemplate是最经典和最受欢迎的 Spring JDBC 方法。这种 “最低级别” 的方法和所有其他方法都在底层使用JdbcTemplate。NamedParameterJdbcTemplate包装了一个JdbcTemplate来提供命名参数,而不是传统的 JDBC?占位符。当你有多个参数用于 SQL 语句时,这种方法提供了更好的文档和易用性。SimpleJdbcInsert和SimpleJdbcCall优化数据库元数据以限制必要的配置量。这种方法简化了编码,所以你只需要提供表或过程的名称,并提供一个与列名匹配的参数映射。这只有在数据库提供了足够的元数据时才有效。如果数据库没有提供这些元数据,你必须提供参数的显式配置。RDBMS 对象 —— 包括
MappingSqlQuery、SqlUpdate和StoredProcedure—— 要求你在初始化你的数据访问层时创建可重用和线程安全的对象。这种方法是根据 JDO Query 建模的,你在其中定义你的查询字符串,声明参数,并编译查询。一旦你做了这些,execute(…),update(…),和findObject(…)方法可以被多次调用,每次可以使用不同的参数值。
2. 包层次结构
Spring 框架的 JDBC 抽象框架由四个不同的包组成:
core:org.springframework.jdbc.core包含了JdbcTemplate类及其各种回调接口,以及各种相关类。一个名为org.springframework.jdbc.core.simple的子包包含了SimpleJdbcInsert和SimpleJdbcCall类。另一个名为org.springframework.jdbc.core.namedparam的子包包含了NamedParameterJdbcTemplate类和相关的支持类。参见使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理,JDBC 批量操作,以及使用SimpleJdbc类简化 JDBC 操作。datasource:org.springframework.jdbc.datasource包含了一个用于轻松访问DataSource的实用类,以及你可以用于测试和在 Java EE 容器外运行未修改的 JDBC 代码的各种简单DataSource实现。一个名为org.springframework.jdbc.datasource.embedded的子包提供了使用 Java 数据库引擎(如 HSQL,H2 和 Derby)创建嵌入式数据库的支持。参见控制数据库连接和嵌入式数据库支持。object:org.springframework.jdbc.object包含了将 RDBMS 查询、更新和存储过程表示为线程安全、可重用对象的类。参见将 JDBC 操作建模为 Java 对象。这种方法是由 JDO 建模的,尽管查询返回的对象自然地与数据库断开连接。这种更高级别的 JDBC 抽象依赖于org.springframework.jdbc.core包中的低级别抽象。support:org.springframework.jdbc.support包提供了SQLException转换功能和一些实用类。在 JDBC 处理过程中抛出的异常被转换为在org.springframework.dao包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换的异常都是未经检查的,这给你提供了捕获你可以恢复的异常的选项,同时让其他异常传播给调用者。参见使用SQLExceptionTranslator。
3. 使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理
这一部分介绍了如何使用 JDBC 核心类来控制基本的 JDBC 处理,包括错误处理。它包括以下主题:
3.1. 使用 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。
Note:
DataSource应始终在 Spring IoC 容器中配置为一个 Bean。在第一种情况下,Bean 直接给到服务;在第二种情况下,它被给到预备的模板。
此类发出的所有 SQL 都在与模板实例的完全限定类名对应的类别(Category)下的 DEBUG 级别进行记录(通常是 JdbcTemplate,但如果你使用 JdbcTemplate 类的自定义子类,那么可能会有所不同)。
以下部分提供了一些 JdbcTemplate 使用的示例。这些示例并非 JdbcTemplate 所公开的所有功能的详尽清单。有关详细信息,请参见随附的 JavaDoc。
3.1.1. 查询(SELECT)
以下查询获取关系中的行数:
Java
int rowCount = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor", Integer.class);1
2
2
以下查询使用了一个绑定变量:
Java
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");1
2
2
以下查询查找一个字符串:
Java
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class,
1212L);1
2
3
4
2
3
4
以下查询查找并填充一个单一的域对象:
Java
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);1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
以下查询查找并填充一个域对象的列表:
Java
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;
});1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
如果最后两段代码实际上存在于同一个应用程序中,那么有意义的做法是删除两个 RowMapper lambda 表达式中存在的重复部分,并将它们提取出来作为一个单独的字段,然后根据需要由 DAO 方法引用。例如,可能更好的写法是将前面的代码片段写成如下形式:
Java
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);
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
3.1.2. 使用 JdbcTemplate 进行更新(INSERT、UPDATE 和 DELETE)
你可以使用 update(..) 方法来执行插入、更新和删除操作。参数值通常作为可变参数提供,或者作为对象数组提供。
以下示例插入了一个新条目:
Java
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");1
2
3
2
3
以下示例更新了一个现有条目:
Java
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);1
2
3
2
3
以下示例删除了一个条目:
Java
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));1
2
3
2
3
3.1.3. 其他 JdbcTemplate 操作
你可以使用 execute(..) 方法来运行任意 SQL。因此,这个方法常常用于 DDL 语句。它有很多重载的变体,可以接受回调接口、绑定变量数组等等。以下示例创建了一个表:
Java
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");以下示例调用了一个存储过程:
Java
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));1
2
3
2
3
更复杂的存储过程支持将在后面介绍。
3.1.4. JdbcTemplate 最佳实践
一旦配置好,JdbcTemplate 类的实例是线程安全的。这一点很重要,因为这意味着你可以配置一个 JdbcTemplate 的实例,然后安全地将这个共享引用注入到多个 DAO(或仓库)中。JdbcTemplate 是有状态的,它维护着一个对 DataSource 的引用,但这个状态并不是会话状态。
使用 JdbcTemplate 类(和相关的 NamedParameterJdbcTemplate 类)的常见做法是在你的 Spring 配置文件中配置一个 DataSource,然后将这个共享的 DataSource Bean 依赖注入到你的 DAO 类中。JdbcTemplate 是在 DataSource 的 Setter 中创建的。这导致 DAO 如下所示:
Java
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
以下示例显示了相应的 XML 配置:
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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.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"/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
明确配置的另一种选择是使用组件扫描和注解支持进行依赖注入。在这种情况下,你可以使用 @Repository 注解类(这使它成为组件扫描的候选对象)并使用 @Autowired 注解 DataSource 的 Setter 方法。以下示例展示了如何做到这一点:
Java
@Repository
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
以下示例显示了相应的 XML 配置:
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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Scans within the base package of the application for @Component classes to configure as beans -->
<context:component-scan base-package="org.springframework.docs.test" />
<bean id="dataSource" class="org.apache.commons.dbcp.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"/>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
如果你使用 Spring 的 JdbcDaoSupport 类,并且你的各种基于 JDBC 的 DAO 类从它继承,那么你的子类将从 JdbcDaoSupport 类继承一个 setDataSource(..) 方法。你可以选择是否从这个类继承。JdbcDaoSupport 类只是为了方便提供的。
无论你选择使用哪种上述的模板初始化风格(或者不使用),每次想要运行 SQL 时创建一个新的 JdbcTemplate 类的实例都是很少需要的。一旦配置好,JdbcTemplate 实例是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个 JdbcTemplate 实例,这需要多个 DataSource,随后需要多个配置不同的 JdbcTemplate 实例。
3.2. 使用 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate 类增加了使用命名参数编程 JDBC 语句的支持,而不是仅使用经典占位符(?)参数编程 JDBC 语句。NamedParameterJdbcTemplate 类包装了一个 JdbcTemplate 并委托给包装的 JdbcTemplate 来完成大部分工作。本节只描述 NamedParameterJdbcTemplate 类与 JdbcTemplate 本身不同的地方 —— 即,使用命名参数编程 JDBC 语句。以下示例展示了如何使用 NamedParameterJdbcTemplate:
Java
// 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);
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
注意在赋值给 sql 变量的值中使用的命名参数表示法,以及插入到 namedParameters 变量(类型为 MapSqlParameterSource)的对应值。
或者,你也可以通过使用基于 Map 的风格将命名参数及其对应的值传递给 NamedParameterJdbcTemplate 实例。NamedParameterJdbcOperations 暴露的其余方法和 NamedParameterJdbcTemplate 类实现的方法遵循类似的模式,这里不再赘述。
以下示例展示了使用基于 Map 的风格:
Java
// 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);
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
与 NamedParameterJdbcTemplate 相关的一个很好的特性(并且存在于同一个 Java 包中)是 SqlParameterSource 接口。你已经在前面的代码片段中看到了这个接口的一个实现示例(MapSqlParameterSource 类)。SqlParameterSource 是 NamedParameterJdbcTemplate 的命名参数值的来源。MapSqlParameterSource 类是一个简单的实现,它是围绕 java.util.Map 的适配器,其中键是参数名,值是参数值。
另一个 SqlParameterSource 的实现是 BeanPropertySqlParameterSource 类。这个类包装了一个任意的 JavaBean(即,遵循 JavaBean 约定的类的实例),并使用包装的 JavaBean 的属性作为命名参数值的来源。
以下示例展示了一个典型的 JavaBean:
Java
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...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
以下示例使用 NamedParameterJdbcTemplate 来返回前面示例中所示类的成员的计数:
Java
// 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);
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
请记住,NamedParameterJdbcTemplate 类包装了一个经典的 JdbcTemplate 模板。如果你需要访问包装的 JdbcTemplate 实例以访问只在 JdbcTemplate 类中存在的功能,你可以使用 getJdbcOperations() 方法通过 JdbcOperations 接口访问包装的 JdbcTemplate。
另请参见 JdbcTemplate 最佳实践,以获取在应用程序上下文中使用 NamedParameterJdbcTemplate 类的指南。
3.3. 使用 SQLExceptionTranslator
SQLExceptionTranslator 是一个接口,由能够将 SQLExceptions 与 Spring 自己的 org.springframework.dao.DataAccessException 之间进行转换的类来实现,这对数据访问策略是不可知的。实现可以是通用的(例如,对于 JDBC 使用 SQLState 代码),也可以是专有的(例如,使用 Oracle 错误代码)以获得更高的精确度。这种异常转换机制被用于 JdbcTemplate 和 JdbcTransactionManager 这些常见的入口点的背后,这些入口点不传播 SQLException,而是传播 DataAccessException。
SQLErrorCodeSQLExceptionTranslator 是 SQLExceptionTranslator 的默认实现。它使用特定的供应商错误代码,比 SQLState 实现更为精确。错误代码的转换基于存储在名为 SQLErrorCodes 的 JavaBean 类型的类中的代码。此类由 SQLErrorCodesFactory 创建并填充,后者是一个根据名为 sql-error-codes.xml 的配置文件内容创建 SQLErrorCodes 的工厂。该文件根据从 DatabaseMetaData 获取的 DatabaseProductName 填充供应商代码。使用的是实际数据库中使用的代码。
SQLErrorCodeSQLExceptionTranslator 按以下顺序应用匹配规则:
任何由子类实现的自定义转换。通常,所提供的具体
SQLErrorCodeSQLExceptionTranslator被使用,所以这条规则不适用。只有当你实际提供了一个子类实现时,这条规则才适用。任何作为
SQLErrorCodes类的customSqlExceptionTranslator属性提供的SQLExceptionTranslator接口的自定义实现。搜索
CustomSQLErrorCodesTranslation类的实例列表(为SQLErrorCodes类的customTranslations属性提供)以寻找匹配项。应用错误代码匹配。
使用后备转换器。
SQLExceptionSubclassTranslator是默认的后备转换器。如果这个转换不可用,下一个后备转换器是SQLStateSQLExceptionTranslator。
Note:默认情况下使用
SQLErrorCodesFactory定义错误代码和自定义异常转换。它们在类路径中名为sql-error-codes.xml的文件中查找,并根据正在使用的数据库的数据库元数据中的数据库名称定位匹配的SQLErrorCodes实例。
你可以扩展 SQLErrorCodeSQLExceptionTranslator,如下面的示例所示:
Java
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;
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
在前面的示例中,特定的错误代码(-12345)被转换,而其他错误则留给默认的转换器实现进行转换。要使用这个自定义转换器,你必须通过 setExceptionTranslator 方法将它传递给 JdbcTemplate,并且你必须在所有需要这个转换器的数据访问处理中使用这个 JdbcTemplate。下面的示例展示了如何使用这个自定义转换器:
Java
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);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
自定义转换器被传递一个数据源,以便在 sql-error-codes.xml 中查找错误代码。
3.4. 运行语句
运行一个 SQL 语句需要非常少的代码。你需要一个 DataSource 和一个 JdbcTemplate,包括 JdbcTemplate 提供的便利方法。下面的示例展示了你需要包含什么来创建一个最小但功能完整的类,该类创建一个新的表:
Java
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))");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
3.5. 运行查询
一些查询方法返回一个单一的值。要检索一个计数或者一行中的特定值,使用 queryForObject(..)。后者将返回的 JDBC 类型转换为作为参数传入的 Java 类。如果类型转换无效,将抛出 InvalidDataAccessApiUsageException。下面的示例包含两个查询方法,一个用于 int,一个用于查询 String:
Java
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);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
除了单一结果查询方法外,还有几个方法返回一个列表,每一行查询返回的结果都有一个条目。最通用的方法是 queryForList(..),它返回一个 List,其中每个元素都是一个 Map,包含每一列的一个条目,使用列名作为键。如果你在前面的示例中添加一个方法来检索所有行的列表,可能如下所示:
Java
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");
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
返回的列表可能如下所示:
Text
[{name=Bob, id=1}, {name=Mary, id=2}]3.6. 更新数据库
以下示例更新了某个主键的列:
Java
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);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
在前面的示例中,一个 SQL 语句有行参数的占位符。你可以将参数值作为可变参数传入,或者,作为对象数组传入。因此,你应该明确地将原始类型包装在原始包装类中,或者你应该使用自动装箱。
3.7. 检索自动生成的键
update() 的便利方法支持检索由数据库生成的主键。这个支持是 JDBC 3.0 标准的一部分,具体细节请参见规范的第 13.6 章。该方法将 PreparedStatementCreator 作为其第一个参数,这就是指定所需插入语句的方式。另一个参数是 KeyHolder,它在成功返回更新后包含生成的键。没有标准的单一方式来创建适当的 PreparedStatement(这就解释了为什么方法签名是这样的)。以下示例在 Oracle 上工作,但可能在其他平台上不工作:
Java
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 key1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
4. 控制数据库连接
本节涵盖:
- 使用
DataSource - 使用
DataSourceUtils - 实现
SmartDataSource - 扩展
AbstractDataSource - 使用
SingleConnectionDataSource - 使用
DriverManagerDataSource - 使用
TransactionAwareDataSourceProxy - 使用
DataSourceTransactionManager/JdbcTransactionManager
4.1. 使用 DataSource
Spring 通过 DataSource 获取到数据库的连接。DataSource 是 JDBC 规范的一部分,是一个通用的连接工厂。它让容器或框架隐藏应用程序代码中的连接池和事务管理问题。作为开发者,你不需要知道如何连接到数据库的细节。这是设置数据源的管理员的责任。在你开发和测试代码的过程中,你很可能充当这两个角色,但你不必要知道生产数据源是如何配置的。
当你使用 Spring 的 JDBC 层时,你可以从 JNDI 获取数据源,或者你可以配置你自己的连接池实现,由第三方提供。传统的选择是 Apache Commons DBCP 和 C3P0,它们带有 Bean 风格的 DataSource 类;对于现代的 JDBC 连接池,可以考虑使用 HikariCP,它有 Builder 风格的 API。
Note 1:你应该只在测试目的下使用
DriverManagerDataSource和SimpleDriverDataSource类(包含在 Spring 分发中)!这些变体不提供池化,并且在多次请求连接时性能较差。
Note 2:DBCP 的全称是 DataBase Connection Pool。C3P0 连接池的作者是《星球大战》迷,C3P0 就是其中的一个机器人,而且这个名称中包含了 Connection 和 Pool 的单词字母。
以下部分使用 Spring 的 DriverManagerDataSource 实现。稍后将介绍其他几种 DataSource 变体。
配置 DriverManagerDataSource:
- 用
DriverManagerDataSource获取连接,就像你通常获取 JDBC 连接一样; - 指定 JDBC 驱动的完全限定类名,以便
DriverManager可以加载驱动类; - 提供一个 URL,不同的 JDBC 驱动会有所不同(查看你的驱动文档以获取正确的值);
- 提供一个用户名和密码来连接到数据库;
以下示例展示了如何在 Java 中配置 DriverManagerDataSource:
Java
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");1
2
3
4
5
2
3
4
5
以下示例展示了相应的 XML 配置:
XML
<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"/>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
接下来的两个示例展示了 DBCP 和 C3P0 的基本连接性和配置。要了解更多帮助控制池化特性的选项,请查看各自的连接池实现的产品文档。
以下示例展示了 DBCP 配置:
XML
<bean id="dataSource" class="org.apache.commons.dbcp.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"/>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
以下示例展示了 C3P0 配置:
XML
<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"/>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
4.2. 使用 DataSourceUtils
DataSourceUtils 是一个方便且强大的辅助类,提供了一些静态方法来从 JNDI 获取连接,并在必要时关闭这些连接。此外,DataSourceUtils 还支持与 DataSourceTransactionManager、JtaTransactionManager 和 JpaTransactionManager 一起使用的线程绑定的 JDBC 连接。
请注意,JdbcTemplate 隐式使用了 DataSourceUtils 连接访问,在每个 JDBC 操作背后使用它,隐式参与正在进行的事务。
4.3. 实现 SmartDataSource
SmartDataSource 接口应由能够提供关系数据库连接的类实现。它扩展了 DataSource 接口,让使用它的类可以查询在给定的操作后是否应该关闭连接。当你知道你需要重复使用一个连接时,这种用法很有效。
4.4. 扩展 AbstractDataSource
AbstractDataSource 是 Spring 的 DataSource 实现的抽象基类。它实现了所有 DataSource 实现共有的代码。如果你编写自己的 DataSource 实现,你应该扩展 AbstractDataSource 类。
4.5. 使用 SingleConnectionDataSource
SingleConnectionDataSource 类是 SmartDataSource 接口的一个实现,它包装了一个在每次使用后都不会关闭的单一连接。这不支持多线程。
如果任何客户端代码在假设有一个池化连接的情况下调用 close(如在使用持久化工具时),你应该将 suppressClose 属性设置为 true。这个设置返回一个包装物理连接的关闭抑制代理。注意,你不能再将这个转换为原生的 Oracle 连接或类似的对象。
SingleConnectionDataSource 主要是一个测试类。它通常使得在应用服务器外部的代码测试变得容易,与简单的 JNDI 环境一起使用。与 DriverManagerDataSource 相比,它一直重用同一个连接,避免了过度创建物理连接。
4.6. 使用 DriverManagerDataSource
DriverManagerDataSource 类是标准 DataSource 接口的一个实现,它通过 Bean 属性配置一个普通的 JDBC 驱动,并且每次都返回一个新的连接。
这个实现对于在 Java EE 容器之外的测试和独立环境非常有用,可以作为 Spring IoC 容器中的一个 DataSource Bean,或者与简单的 JNDI 环境一起使用。池假设 Connection.close() 调用关闭连接,所以任何 DataSource 感知的持久化代码都应该工作。然而,使用 JavaBean 风格的连接池(如 commons-dbcp)非常容易,即使在测试环境中,几乎总是更倾向于使用这样的连接池而不是 DriverManagerDataSource。
4.7. 使用 TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy 是一个目标 DataSource 的代理。该代理包装了目标 DataSource,以增加对 Spring 管理的事务的感知。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource。
Note:除非已经存在的代码必须被调用并传递一个标准的 JDBC
DataSource接口实现,否则很少有人希望使用这个类。在这种情况下,你仍然可以让这段代码可用,并且同时让这段代码参与 Spring 管理的事务。通常更倾向于使用更高级别的资源管理抽象,如JdbcTemplate或DataSourceUtils,来编写你自己的新代码。
有关更多详细信息,请参阅 TransactionAwareDataSourceProxy 的 JavaDoc。
4.8. 使用 DataSourceTransactionManager / JdbcTransactionManager
DataSourceTransactionManager 类是一个针对单个 JDBC DataSource 的 PlatformTransactionManager 实现。它将指定 DataSource 的 JDBC 连接绑定到当前执行的线程,可能允许每个 DataSource 有一个线程绑定的连接。
应用程序代码需要通过 DataSourceUtils.getConnection(DataSource) 而不是 Java EE 的标准 DataSource.getConnection 来检索 JDBC 连接。它抛出未经检查的 org.springframework.dao 异常,而不是已检查的 SQLExceptions。所有框架类(如 JdbcTemplate)都隐式使用这种策略。如果没有与事务管理器一起使用,查找策略的行为就完全像 DataSource.getConnection,因此可以在任何情况下使用。
DataSourceTransactionManager 类支持保存点(PROPAGATION_NESTED)、自定义隔离级别和适当的 JDBC 语句查询超时应用的超时。为了支持后者,应用程序代码必须使用 JdbcTemplate 或者对每个创建的语句调用 DataSourceUtils.applyTransactionTimeout(..) 方法。
在单资源情况下,你可以使用 DataSourceTransactionManager 而不是 JtaTransactionManager,因为它不需要容器支持 JTA 事务协调器。在这些事务管理器之间切换只是配置的问题,只要你坚持所需的连接查找模式。注意,JTA 不支持保存点或自定义隔离级别,并且有一个不同的超时机制,但在 JDBC 资源和 JDBC 提交/回滚管理方面,它则暴露出类似的行为。
Note
从 5.3 开始,Spring 提供了一个扩展的
JdbcTransactionManager变体,它在提交/回滚时添加了异常转换能力(与JdbcTemplate对齐)。而DataSourceTransactionManager只会抛出TransactionSystemException(类似于 JTA),JdbcTransactionManager将数据库锁定失败等转换为相应的DataAccessException子类。注意,应用程序代码需要为这样的异常做好准备,而不仅仅是期望TransactionSystemException。在这种情况下,JdbcTransactionManager是推荐的选择。在异常行为方面,
JdbcTransactionManager大致等同于JpaTransactionManager和R2dbcTransactionManager,作为彼此的直接伴侣/替代品。另一方面,DataSourceTransactionManager等同于JtaTransactionManager并可以作为直接的替代品。
5. JDBC 批量操作
大多数 JDBC 驱动如果将多个调用批量化到同一个预处理语句,会提供更好的性能。通过将更新分组成批次,你可以限制到数据库的往返次数。
5.1. 使用 JdbcTemplate 的基本批量操作
你可以通过实现一个特殊接口 BatchPreparedStatementSetter 的两个方法,然后将该实现作为第二个参数传入你的 batchUpdate 方法调用,来完成 JdbcTemplate 的批处理。你可以使用 getBatchSize 方法来提供当前批次的大小。你可以使用 setValues 方法来为预处理语句的参数设置值。这个方法被调用的次数是你在 getBatchSize 调用中指定的次数。以下示例根据列表中的条目更新 t_actor 表,整个列表被用作批处理:
Java
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());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
如果你处理更新流或从文件读取,你可能有一个首选的批次大小,但是最后一个批次可能没有那么多的条目。在这种情况下,你可以使用 InterruptibleBatchPreparedStatementSetter 接口,它允许你在输入源耗尽时中断一个批次。isBatchExhausted 方法让你能够标志批次的结束。
5.2. 使用对象列表的批量操作
JdbcTemplate 和 NamedParameterJdbcTemplate 都提供了一种替代的方式来提供批量更新。你不需要实现一个特殊的批处理接口,而是在调用中以列表的形式提供所有的参数值。框架会遍历这些值,并使用一个内部的预处理语句设定器。API 的变化取决于你是否使用命名参数。对于命名参数,你需要提供一个 SqlParameterSource 数组,每个批次的成员都有一个条目。你可以使用 SqlParameterSourceUtils.createBatch 便利方法来创建这个数组,传入一个 Bean 风格的对象数组(带有对应参数的 Getter 方法),键为字符串的 Map 实例(包含相应的参数作为值),或者两者的混合。
以下示例展示了使用命名参数进行批量更新:
Java
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));
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对于使用经典的 ? 占位符的 SQL 语句,你需要传入一个包含更新值的对象数组的列表。这个对象数组必须为 SQL 语句中的每一个占位符都有一个条目,并且它们必须按照在 SQL 语句中定义的顺序排列。
以下示例与前面的示例相同,只是它使用了经典的 JDBC ? 占位符:
Java
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<Object[]>();
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);
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
我们之前描述的所有批量更新方法都会返回一个 int 数组,该数组包含每个批次条目影响的行数。这个计数是由 JDBC 驱动报告的。如果计数不可用,JDBC 驱动会返回 -2 的值。
Tip
在这样的场景中,对底层
PreparedStatement自动设置值,每个值的相应 JDBC 类型需要从给定的 Java 类型中推导出来。虽然这通常工作得很好,但是有可能出现问题(例如,包含null值的Map)。默认情况下,Spring 在这种情况下会调用ParameterMetaData.getParameterType,这可能会让你的 JDBC 驱动负担过重。如果你遇到性能问题(如在 Oracle 12c、JBoss 和 PostgreSQL 上报告的问题),你应该使用最新版本的驱动,并考虑将spring.jdbc.getParameterType.ignore属性设置为true(作为 JVM 系统属性或通过SpringProperties机制)。或者,你也可以考虑显式地指定相应的 JDBC 类型,可以通过
BatchPreparedStatementSetter(如前面所示),通过给基于List<Object[]>的调用的显式类型数组,通过在自定义的MapSqlParameterSource实例上调用registerSqlType,或者通过BeanPropertySqlParameterSource,即使对于空值,也从 Java 声明的属性类型推导出 SQL 类型。
5.3. 使用多个批次的批量操作
前面的批量更新示例处理的是那些你希望将其分解为几个较小批次的大批次。你可以通过多次调用 batchUpdate 方法来实现这一点,但现在有了一个更方便的方法。这个方法除了 SQL 语句外,还接收一个包含参数的对象 Collection,每个批次要进行的更新数量,以及一个 ParameterizedPreparedStatementSetter 来为预编译语句的参数设置值。框架会遍历提供的值,并将更新调用分解为指定大小的批次。
下面的示例显示了一个使用 100 个批次大小的批量更新:
Java
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;
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这个调用的批量更新方法返回一个 int 数组的数组,其中包含每个批次的数组条目,每个更新的受影响行数的数组。顶级数组的长度表示运行的批次数量,第二级数组的长度表示该批次中的更新数量。每个批次中的更新数量应该是为所有批次(最后一个可能会少一些)提供的批次大小,这取决于提供的更新对象的总数。每个更新语句的更新计数是由 JDBC 驱动程序报告的。如果计数不可用,JDBC 驱动程序返回 -2 的值。
6. 使用 SimpleJdbc 类简化 JDBC 操作
SimpleJdbcInsert 和 SimpleJdbcCall 类通过利用可以通过 JDBC 驱动程序检索的数据库元数据,提供了简化的配置。这意味着你在前期需要配置的内容较少,尽管如果你更愿意在代码中提供所有细节,你也可以覆盖或关闭元数据处理。
6.1. 使用 SimpleJdbcInsert 插入数据
我们首先从具有最少配置选项的 SimpleJdbcInsert 类开始。你应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。对于这个示例,初始化方法是 setDataSource 方法。你不需要子类化 SimpleJdbcInsert 类。相反,你可以创建一个新的实例,并使用 withTableName 方法设置表名。此类的配置方法遵循流式风格,返回 SimpleJdbcInsert 的实例,这让你可以链式调用所有配置方法。以下示例只使用了一个配置方法(我们稍后会展示多个方法的示例):
Java
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<String, Object>(3);
parameters.put("id", actor.getId());
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
insertActor.execute(parameters);
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里使用的 execute 方法只接受一个普通的 java.util.Map 作为其唯一的参数。需要注意的重要一点是,用于 Map 的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的插入语句。
6.2. 使用 SimpleJdbcInsert 检索自动生成的键
下一个示例使用的插入与前一个示例相同,但是,它并没有传入 id,而是检索自动生成的键,并将其设置在新的 Actor 对象上。当它创建 SimpleJdbcInsert 时,除了指定表名外,它还使用 usingGeneratedKeyColumns 方法指定了生成的键列的名称。以下清单显示了它的工作方式:
Java
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<String, Object>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用这种第二种方法运行插入的主要区别是,你不会将 id 添加到 Map 中,而是调用 executeAndReturnKey 方法。这将返回一个 java.lang.Number 对象,你可以用它来创建一个在你的领域类中使用的数值类型的实例。你不能依赖所有的数据库在这里返回一个特定的 Java 类。java.lang.Number 是你可以依赖的基类。如果你有多个自动生成的列或者生成的值是非数字的,你可以使用从 executeAndReturnKeyHolder 方法返回的 KeyHolder。
6.3. 为 SimpleJdbcInsert 指定列
你可以通过使用 usingColumns 方法指定一列列名来限制插入的列,如下例所示:
Java
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<String, Object>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
插入的执行与你依赖元数据来确定使用哪些列的方式是相同的。
6.4. 使用 SqlParameterSource 提供参数值
使用 Map 来提供参数值是可行的,但它并不是最方便的类。Spring 提供了一些 SqlParameterSource 接口的实现,你可以使用它们来代替。第一个是 BeanPropertySqlParameterSource,如果你有一个符合 JavaBean 规范的类包含你的值,这是一个非常方便的类。它使用相应的 Getter 方法来提取参数值。以下示例展示了如何使用 BeanPropertySqlParameterSource:
Java
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());
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
另一个选项是 MapSqlParameterSource,它类似于 Map,但提供了一个更方便的 addValue 方法,可以进行链式调用。以下示例展示了如何使用它:
Java
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())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如你所见,配置是相同的。只有执行代码需要改变以使用这些替代的输入类。
6.5. 使用 SimpleJdbcCall 调用存储过程
SimpleJdbcCall 类使用数据库中的元数据来查找 in 和 out 参数的名称,因此你不必显式地声明它们。如果你更喜欢这样做,或者你有一些参数(如 ARRAY 或 STRUCT)没有自动映射到 Java 类,你可以声明参数。第一个示例展示了一个简单的过程,它只从 MySQL 数据库返回 VARCHAR 和 DATE 格式的标量值。示例过程读取指定的 actor 条目,并以 out 参数的形式返回 first_name、last_name 和 birth_date 列。以下清单显示了第一个示例:
SQL
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;1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
in_id 参数包含了你正在查找的 actor 的 id。out 参数返回从表中读取的数据。
你可以以类似于声明 SimpleJdbcInsert 的方式来声明 SimpleJdbcCall。你应该在数据访问层的初始化方法中实例化和配置这个类。与 StoredProcedure 类相比,你不需要创建一个子类,也不需要声明可以在数据库元数据中查找的参数。以下 SimpleJdbcCall 配置的示例使用了前面的存储过程(除了 DataSource 外,唯一的配置选项就是存储过程的名称):
Java
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);
Map out = procReadActor.execute(in);
Actor actor = new Actor();
actor.setId(id);
actor.setFirstName((String) out.get("out_first_name"));
actor.setLastName((String) out.get("out_last_name"));
actor.setBirthDate((Date) out.get("out_birth_date"));
return actor;
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
你编写执行调用的代码时,需要创建一个包含 IN 参数的 SqlParameterSource。你必须将输入值的名称与存储过程中声明的参数名称匹配。大小写不必匹配,因为你使用元数据来确定在存储过程中应如何引用数据库对象。存储过程的源代码中指定的内容并不一定是它在数据库中存储的方式。有些数据库将名称转换为全大写,而其他数据库使用小写或按照指定的大小写。
execute 方法接受 IN 参数,并返回一个包含任何由存储过程中指定的以 out 参数名称作为键的 Map。在这种情况下,它们是 out_first_name、out_last_name 和 out_birth_date。
execute 方法的最后一部分创建一个 Actor 实例,用于返回检索到的数据。同样,使用存储过程中声明的 out 参数的名称是很重要的。此外,结果映射中存储的 out 参数名称的大小写与数据库中的 out 参数名称的大小写匹配,这可能在不同的数据库之间有所不同。为了使你的代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用 LinkedCaseInsensitiveMap。为了实现后者,你可以创建你自己的 JdbcTemplate 并将 setResultsMapCaseInsensitive 属性设置为 true。然后,你可以将这个定制的 JdbcTemplate 实例传递到你的 SimpleJdbcCall 的构造函数中。以下示例显示了这种配置:
Java
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");
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
通过采取这种行动,你可以避免在返回的 out 参数的名称的大小写使用中产生冲突。
6.6. 明确声明用于 SimpleJdbcCall 的参数
在本章的前面部分,我们描述了如何从元数据中推导出参数,但如果你愿意,你可以显式地声明它们。你可以通过创建和配置 SimpleJdbcCall 并使用 declareParameters 方法来实现,该方法接受一个可变数量的 SqlParameter 对象作为输入。请参阅下一节以获取如何定义 SqlParameter 的详细信息。
Note:如果你使用的数据库不是 Spring 支持的数据库,那么显式声明是必要的。目前,Spring 支持以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数的元数据查找。
你可以选择显式声明一个、一些或所有的参数。在你没有显式声明参数的地方,仍然使用参数元数据。为了绕过所有潜在参数的元数据查找的处理,并且只使用声明的参数,你可以调用 withoutProcedureColumnMetaDataAccess 方法作为声明的一部分。假设你为数据库函数声明了两个或更多不同的调用签名。在这种情况下,你调用 useInParameterNames 来指定给定签名包含的 IN 参数名称列表。
以下示例显示了一个完全声明的过程调用,并使用了前面示例的信息:
Java
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)
);
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这两个示例的执行和最终结果是相同的。第二个示例明确指定了所有的细节,而不是依赖于元数据。
6.7. 如何定义 SqlParameters
要为 SimpleJdbc 类以及 RDBMS 操作类(在将 JDBC 操作建模为 Java 对象中有介绍)定义参数,你可以使用 SqlParameter 或其子类。通常,你会在构造函数中指定参数名称和 SQL 类型。SQL 类型是通过使用 java.sql.Types 常量来指定的。在本章的前面,我们看到了类似于以下的声明:
Java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),1
2
2
第一行中的 SqlParameter 声明了一个 IN 参数。你可以在存储过程调用和使用 SqlQuery 及其子类的查询中使用 IN 参数(在理解 SqlQuery 中有介绍)。
第二行(带有 SqlOutParameter)声明了一个用于存储过程调用的 out 参数。还有一个 SqlInOutParameter 用于 InOut 参数(为过程提供 IN 值并且也返回一个值)。
Tip:只有声明为
SqlParameter和SqlInOutParameter的参数才用于提供输入值。这与StoredProcedure类不同,后者(出于向后兼容性的原因)允许为声明为SqlOutParameter的参数提供输入值。
对于 IN 参数,除了名称和 SQL 类型,你还可以指定数字数据的刻度或自定义数据库类型的类型名称。对于 out 参数,你可以提供一个 RowMapper 来处理从 REF 游标返回的行的映射。另一个选项是指定一个 SqlReturnType,它提供了定义返回值的自定义处理的机会。
6.8. 使用 SimpleJdbcCall 调用存储函数
你可以像调用存储过程一样调用存储函数,只是你提供的是函数名而不是过程名。你可以使用 withFunctionName 方法作为配置的一部分,以指示你想要调用一个函数,并生成对应的函数调用字符串。一个专门的调用(executeFunction)被用来运行函数,并且它将函数返回值作为指定类型的对象返回,这意味着你不必从结果映射中检索返回值。对于只有一个 out 参数的存储过程,也有一个类似的便利方法(名为 executeObject)。以下示例(适用于 MySQL)基于一个名为 get_actor_name 的存储函数,该函数返回一个演员(Actor)的全名:
SQL
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;1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
要调用这个函数,我们再次在初始化方法中创建一个 SimpleJdbcCall,如下面的示例所示:
Java
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;
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用的 executeFunction 方法返回一个包含函数调用返回值的字符串。
6.9. 从 SimpleJdbcCall 返回一个 ResultSet 或 REF Cursor
调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而其他数据库需要明确注册特定类型的 out 参数。这两种方法都需要额外的处理来遍历结果集并处理返回的行。使用 SimpleJdbcCall,你可以使用 returningResultSet 方法,并声明一个用于特定参数的 RowMapper 实现。如果结果集在结果处理期间返回,那么没有定义名称,所以返回的结果必须与你声明 RowMapper 实现的顺序匹配。指定的名称仍然用于在从 execute 语句返回的结果映射中存储处理过的结果列表。
下一个示例(适用于 MySQL)使用一个不接受 IN 参数并返回 t_actor 表中所有行的存储过程:
SQL
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;1
2
3
4
2
3
4
要调用这个过程,你可以声明 RowMapper。因为你想要映射的类遵循 JavaBean 规则,所以你可以使用 BeanPropertyRowMapper,它是通过在 newInstance 方法中传入要映射的所需类来创建的。以下示例展示了如何做到这一点:
Java
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");
}
// ... additional methods
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Tip:当存储过程返回多个结果集时,你可以多次使用
returningResultSet方法,为每个结果集声明一个RowMapper。RowMapper实现的声明顺序必须与返回的结果集顺序匹配。
execute 调用传入一个空的 Map,因为这个调用不接受任何参数。然后从结果映射中检索出演员列表,并返回给调用者。