20220507 3. Data Access - Data Access with JDBC
前言
Spring Framework JDBC 抽象提供的价值可能最好通过下表中列出的操作序列来展示。该表显示了 Spring 负责哪些操作以及您负责哪些操作。
操作 | Spring | You |
---|---|---|
定义连接参数 | Y | |
打开连接 | Y | |
指定 SQL 语句 | Y | |
声明参数并提供参数值 | Y | |
准备并运行 statement | Y | |
设置循环以遍历结果(如果有) | Y | |
定义每次迭代需要做的工作 | Y | |
处理任何异常 | Y | |
处理事务 | Y | |
关闭连接(connection)、语句(statement)和结果集(resultset) | Y |
选择 JDBC 数据库的访问方法
您可以从多种方法中进行选择,以构成 JDBC 数据库访问的基础。除了三种 JdbcTemplate
风格,新的 SimpleJdbcInsert
和 SimpleJdbcCall
方法优化数据库的元数据和 RDBMS 对象样式采用类似于 JDO 的查询设计的一个更面向对象的方法。一旦您开始使用这些方法之一,您仍然可以混合搭配以包含来自不同方法的功能。所有方法都需要兼容 JDBC 2.0 的驱动程序,一些高级功能需要 JDBC 3.0 驱动程序。
JdbcTemplate
是经典且最流行的 Spring JDBC 方法。它是“最低级别”的,所有其他方式都在底层使用JdbcTemplate
NamedParameterJdbcTemplate
包装JdbcTemplate
以提供命名参数而不是传统的 JDBC?
占位符。当 SQL 语句有多个参数时,此方法可提供更好的易用性SimpleJdbcInsert
和SimpleJdbcCall
优化数据库元数据以限制必要配置的数量。这种方式简化了编码,因此您只需提供表或存储过程的名称,并提供与列名称匹配的参数映射。这仅在数据库提供足够的元数据时才有效。如果数据库不提供此元数据,则必须提供参数的显式配置- RDBMS 对象(包括
MappingSqlQuery
、SqlUpdate
和StoredProcedure
)要求您在数据访问层初始化期间创建可重用和线程安全的对象。此方式类似 JDO 查询,在其中定义查询字符串、声明参数并编译查询。这种方式下,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
类和相关的支持类。请参阅 使用 JDBC 核心类控制基本 JDBC 处理和错误处理 、JDBC 批处理操作 和 使用SimpleJdbc
类简化 JDBC 操作datasource
:org.springframework.jdbc.datasource
包中包含易于访问DataSource
的实用程序类和各种简单的DataSource
实现,可用于在 Java EE 容器之外测试和运行未经修改的 JDBC 代码。名为org.springfamework.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
使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理
使用 JdbcTemplate
JdbcTemplate
是 JDBC 核心包中的中心类。它处理资源的创建和释放,帮助您避免常见错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务(如语句创建和执行),让应用程序代码来提供 SQL 和提取结果。JdbcTemplate
类提供以下功能:
- 运行 SQL 查询
- 更新语句和存储过程调用
- 对
ResultSet
执行迭代并提取返回的参数值 - 捕获 JDBC 异常并将它们转换为
org.springframework.dao
包中定义的通用的异常层次结构(请参阅一致的异常层次结构)
使用 JdbcTemplate
时,你只需要实现回调接口,给它们一个明确定义的契约。给定由 JdbcTemplate
类提供的 Connection
,PreparedStatementCreator
回调接口创建一个 prepared 语句,提供 SQL 和任何必要的参数。对于创建可调用语句的 CallableStatementCreator
接口也是如此 。 RowCallbackHandler
接口从 ResultSet
的每一行中提取值。
您可以通过 DataSource
引用直接实例化在 DAO 实现中使用 JdbcTemplate
,或者您可以在 Spring IoC 容器中配置它并将其作为 bean 引用提供给 DAO 。
DataSource
应该始终被配置为 Spring IoC 容器的 bean 。其一,bean 直接提供给服务;其二,它被提供给准备好的模板。
此类发出的所有 SQL 都被日志记录在与模板实例的完全限定类名对应的类别下的 DEBUG
级别(通常为 JdbcTemplate
,但如果您使用 JdbcTemplate
类的自定义子类,则可能会有所不同 )。
org.springframework.jdbc.core.JdbcTemplate
查询 ( 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);
查询返回对象列表:
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;
});
抽取 RowMapper
:
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);
}
使用 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
类(和关联的 NamedParameterJdbcTemplate
类)时的常见做法是在 Spring 配置文件中配置 DataSource
,然后将该共享 DataSource
bean 依赖注入到 DAO 类中。
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...
}
对等的 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>
显式配置的替代方法是使用组件扫描和注解支持进行依赖项注入。可以使用 @Repository
注解类 (这使其成为组件扫描的候选对象)并使用 @Autowired
注解 DataSource
setter 方法。以下示例显示了如何执行此操作:
@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...
}
对等的 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>
如果您使用 Spring 的 JdbcDaoSupport
类并且您的各种 JDBC 支持的 DAO 类从它扩展而来,您的子类将从 JdbcDaoSupport
类继承 setDataSource(..)
方法 。您可以选择是否继承该类。提供 JdbcDaoSupport
类只是为了方便。
无论您选择是否使用上述哪种模板初始化样式,每次要运行 SQL 时都很少需要创建 JdbcTemplate
类的新实例。配置后,JdbcTemplate
实例是线程安全的。如果您的应用程序访问多个数据库,您可能需要多个 JdbcTemplate
实例,这需要多个 DataSources
以及随后多个不同配置的 JdbcTemplate
实例。
使用 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate
类通过使用命名参数来支持 JDBC 语句,而不是只使用常规的占位符 ?
。NamedParameterJdbcTemplate
类包装并委托 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);
}
SqlParameterSource
是 NamedParameterJdbcTemplate
的命名参数值的来源。MapSqlParameterSource
类是一个简单的实现,它是一个围绕 java.util.Map
的适配器,其中的键是参数名称,值是参数值。
另一个 SqlParameterSource
实现是 BeanPropertySqlParameterSource
类。此类包装任意 JavaBean(即,遵守JavaBean 约定的类的实例)并使用包装的 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
实例。
JdbcTemplate
实现了 JdbcOperations
接口
org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
org.springframework.jdbc.core.JdbcOperations
使用 SQLExceptionTranslator
SQLExceptionTranslator
可以在 SQLExceptions
和 Spring 的 org.springframework.dao.DataAccessException
之间进行转换,这与数据访问策略无关。实现可以是通用的(例如,对 JDBC 使用 SQLState
代码)或专有的(例如,使用 Oracle 错误代码)以获得更高的准确性。
SQLErrorCodeSQLExceptionTranslator
是默认使用的 SQLExceptionTranslator
实现。此实现使用特定的供应商代码。它比 SQLState
实现更精确。错误代码转换基于 JavaBean 类型类中保存的代码,称为 SQLErrorCodes
。此类由 SQLErrorCodesFactory
创建和填充,它是基于名为 sql-error-codes.xml
的配置文件的内容创建 SQLErrorCodes
的工厂。该文件填充了供应商代码并基于从 DatabaseMetaData
获取的 DatabaseProductName
,将使用您正在使用的实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator
按照下列顺序应用匹配规则:
- 由子类实现的任何自定义转换。通常使用提供的具体
SQLErrorCodeSQLExceptionTranslator
,因此此规则不适用。仅当您实际提供了子类实现时,它才适用 - 作为
SQLErrorCodes
类的customSqlExceptionTranslator
属性提供的SQLExceptionTranslator
接口的任何自定义实现 - 搜索
CustomSQLErrorCodesTranslation
类的实例列表(为SQLErrorCodes
类的customTranslations
属性提供 )以查找匹配项 - 应用错误代码匹配
- 使用后背翻译器。
SQLExceptionSubclassTranslator
是默认的后备翻译器。如果此翻译不可用,则下一个后备翻译器是SQLStateSQLExceptionTranslator
默认情况下,使用
SQLErrorCodesFactory
定义Error
代码和自定义异常转换。它们从类路径中命名为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);
}
org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
org.springframework.jdbc.support.SQLExceptionTranslator
org.springframework.jdbc.support.SQLErrorCodes
sql-error-codes.xml
运行语句( Statement )
运行 SQL 语句只需要 DataSource
和 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 语句具有行参数的占位符。您可以将参数值作为可变参数或作为对象数组传递。
获取自动生成的主键值
update()
便利方法支持获取数据库生成的主键。这种支持是 JDBC 3.0 标准的一部分。该方法将 PreparedStatementCreator
作为其第一个参数,这就是指定所需插入语句的方式。另一个参数是 KeyHolder
,它包含从更新成功返回时生成的 key 。没有标准的单一方法来创建适当的 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
控制数据库连接( Connection )
使用 DataSource
Spring 通过 DataSource
获取连接。DataSource
是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架对应用程序代码隐藏连接池和事务管理问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息。
在使用 Spring 的 JDBC 层时,可以从 JNDI 获取数据源,也可以使用第三方提供的连接池实现来配置自己的数据源。传统的选择是带有 bean 样式的 DataSource
类的 Apache Commons DBCP 和 C3P0 ;对于现代 JDBC 连接池,请考虑使用 Builder 样式的 API 的 HikariCP 。
您应该仅将
DriverManagerDataSource
和SimpleDriverDataSource
类用于测试目的!当对一个连接发出多个请求时,这些变体不提供池化并且性能不佳。
以下部分使用 Spring 的 DriverManagerDataSource
实现。
配置 DriverManagerDataSource
:
- 通过
DriverManagerDataSource
获得连接,就像通常获取 JDBC 连接一样 - 指定 JDBC 驱动程序的完全限定类名,以便
DriverManager
可以加载驱动程序类 - 提供因 JDBC 驱动程序而不同的的 URL
- 提供连接到数据库的用户名和密码
以下示例显示了如何在 Java 中配置 DriverManagerDataSource
:
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
以下示例显示了相应的 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"/>
以下示例显示了 DBCP 配置:
<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"/>
以下示例显示了 C3P0 配置:
<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
实现 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
。
此实现对于 Java EE 容器之外的测试和独立环境非常有用,可以作为 Spring IoC 容器中的 DataSource
bean 或与简单的 JNDI 环境结合使用。使用 JavaBean 样式的连接池(例如 commons-dbcp
)非常容易,即使在测试环境中也是如此,因此使用此类连接池几乎总是优于 DriverManagerDataSource
使用 TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy
是目标 DataSource
的代理。代理包装该目标 DataSource
以添加对 Spring 管理的事务的感知。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource
。
很少需要使用此类,除非必须调用现有代码并传递标准 JDBC
DataSource
接口实现。在这种情况下,您仍然可以使用此代码,同时让此代码参与 Spring 托管事务。通常最好使用更高级别的资源管理抽象来编写自己的新代码,例如JdbcTemplate
或DataSourceUtils
。
使用 DataSourceTransactionManager
DataSourceTransactionManager
类是针对单个 JDBC 数据源的 PlatformTransactionManager
实现。它将来自指定数据源的 JDBC 连接绑定到当前正在执行的线程,允许一个线程有一个数据源连接。
需要 DataSourceUtils.getConnection(DataSource)
来检索 JDBC 连接, 而不是通过 Java EE 的标准 DataSource.getConnection
。它抛出非检查的 org.springframework.dao
异常而不是检查异常 SQLExceptions
。所有框架类(例如 JdbcTemplate
)都隐式地使用此策略。如果没有与此事务管理器一起使用,则查找策略的行为完全类似于常见的策略。因此,它可以在任何情况下使用。
DataSourceTransactionManager
类支持自定义隔离级别和超时,这些级别和超时将在适当的 JDBC 语句查询超时时应用。为了支持后者,应用程序代码必须使用 JdbcTemplate
或为每个语句调用 DataSourceUtils.applyTransactionTimeout(..)
方法。
在单数据源的情况下,您可以使用此实现而不是 JtaTransactionManager
,因为它不需要容器支持 JTA 。如果您坚持所需的连接查找模式,则在两者之间切换只是配置问题。JTA 不支持自定义隔离级别。
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());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
如果您处理更新流或读取文件,您可能有一个批次大小,但最后一批可能没有那么多数量的条目。在这种情况下,您可以使用 InterruptibleBatchPreparedStatementSetter
接口,一旦输入源耗尽,您就可以中断批处理。isBatchExhausted
方法可让您发出批处理结束的信号。
使用对象列表的批处理操作
JdbcTemplate
与 NamedParameterJdbcTemplate
都提供了批处理更新的替代方式。您没有实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值并使用内部准备好的语句设置器。API 会有所不同,具体取决于您是否使用命名参数。对于命名参数,您提供一个 SqlParameterSource
数组,为批处理的每个成员提供一个条目。您可以使用方便的方法 SqlParameterSourceUtils.createBatch
来创建这个数组,传入一个 bean 样式的对象数组(具有对应于参数的 getter 方法)、键类型为 String
的 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));
}
// ... additional methods
}
对于使用经典 ?
占位符的 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<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
}
我们之前描述的所有批处理更新方法都返回一个 int
数组,其中包含每个批处理条目的受影响行数。此计数由 JDBC 驱动程序报告。如果计数不可用,则 JDBC 驱动程序返回值 -2
。
在这种情况下,通过在底层 PreparedStatement
上自动设置值,需要从给定的 Java 类型派生每个值的相应 JDBC 类型。虽然这通常效果很好,但也有可能出现问题(例如,包含 null
值的 Map )。默认情况下,Spring 会在这种情况下调用 ParameterMetaData.getParameterType
,这对于您的 JDBC 驱动程序来说可能代价很高。如果遇到性能问题(如 Oracle 12c、JBoss 和 PostgreSQL 报告的那样),您应该使用最新的驱动程序版本并考虑将 spring.jdbc.getParameterType.ignore
属性设置为 true
(作为 JVM 系统属性或通过 Spring Properties
机制)。
或者,您可以考虑显式指定相应的 JDBC 类型,可以通过 BatchPreparedStatementSetter
(如前面所示)、通过为基于 List<Object[]>
调用提供的显式类型数组、通过对自定义 MapSqlParameterSource
实例上的 registerSqlType
调用,或通过甚至为 null 值从 Java 声明的属性类型派生 SQL 类型的 BeanPropertySqlParameterSource
。
多批次的批处理操作
前面的批量更新示例处理的批次太大,以至于您希望将它们分成几个较小的批次。您可以使用前面提到的方法通过多次调用 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;
}
// ... additional methods
}
此调用的批处理更新方法返回一个 int
二维数组,其中包含每个批处理的数组条目,以及每个更新受影响的行数的数组。顶级数组的长度表示运行的批次数,第二级数组的长度表示该批次中的更新次数。每个批次中的更新数量应该是为所有批次提供的批次大小(除了最后一个可能更少),具体取决于提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的计数。如果计数不可用,则 JDBC 驱动程序返回 -2
。
使用 SimpleJdbc
简化 JDBC 操作
SimpleJdbcInsert
和 SimpleJdbcCall
类通过利用可通过 JDBC 驱动被检索的数据库元数据提供了一个简化的配置。这意味着您预先配置很少,但如果您希望在代码中提供所有详细信息,您可以覆盖或关闭元数据处理。
org.springframework.jdbc.core.simple.SimpleJdbcInsert
org.springframework.jdbc.core.simple.SimpleJdbcCall
使用 SimpleJdbcInsert
插入数据
我们首先查看具有最少配置选项的 SimpleJdbcInsert
类。您应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert
。对于本例,初始化方法是 setDataSource
方法。您不需要子类化 SimpleJdbcInsert
。相反,您可以使用 withTableName
方法创建一个新实例并设置表名。此类的配置方法遵循流式(fluid)样式,返回 SimpleJdbcInsert
实例,它允许您链接所有配置方法。以下示例仅使用一个配置方法:
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
}
这里使用的 execute
方法将一个普通的 java.util.Map
作为它的唯一参数。这里要注意的重要一点是,用于 Map
的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的插入语句。
使用 SimpleJdbcInsert
获取自动生成的主键值
下一个示例使用与前一个相同的插入,但它不是传入id
,而是检索自动生成的主键并将其设置在新的 Actor
对象上。创建 SimpleJdbcInsert
的时候,除了指定表名外,还指定了 usingGeneratedKeyColumns
方法生成的 key 列的名称。以下清单显示了它的工作原理:
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
}
主要区别是,你不需要添加 id
到 Map
,而是调用 executeAndReturnKey
方法。这将返回一个 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<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
}
插入的执行与依赖元数据来确定要使用哪些列相同。
使用 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());
}
// ... additional methods
}
另一种选择是 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())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
SimpleJdbcCall
调用存储过程
SimpleJdbcCall
类使用数据库中的元数据查找名称为 in
和 out
的参数,使您不必明确声明他们。如果您愿意这样做,或者如果存在没有自动映射到 Java 类的参数(例如 ARRAY
或 STRUCT
),可以声明参数。
第一个示例显示了一个简单的存储过程,该过程仅返回 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;
以类似于声明 SimpleJdbcInsert
的方式声明 SimpleJdbcCall
。您应该在数据访问层的初始化方法中实例化和配置 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);
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
}
您为执行调用编写的代码涉及创建包含 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
。以下示例显示了此配置:
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
}
通过执行此操作,您可以避免在用于返回 out
参数名称的情况下发生冲突。
显式声明用于 SimpleJdbcCall
的参数
在本章前面,我们描述了如何从元数据中推导出参数,但如果您愿意,也可以显式声明它们。可以通过创建和配置 SimpleJdbcCall
与 declareParameters
方法,该方法采用可变数目的 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)
);
}
// ... additional methods
}
两个例子的执行和最终结果是一样的。第二个示例明确指定所有详细信息,而不是依赖元数据。
如何定义 SqlParameters
要为 SimpleJdbc
类以及 RDBMS 操作类(在 将 JDBC 操作建模为 Java 对象中介绍 )定义参数,您可以使用 SqlParameter
或其子类。为此,您通常在构造函数中指定参数名称和 SQL 类型。SQL 类型是通过使用 java.sql.Types
常量指定的。在本章前面,我们看到了类似于以下内容的声明:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
带有 SqlParameter
的第一行声明了一个 IN 参数。通过使用 SqlQuery
及其子类,您可以将 IN 参数用于存储过程调用和查询。
第二行(带有 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 JdbcTemplate jdbcTemplate;
private SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(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
}
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");
}
// ... additional methods
}
execute
调用传递一个空的 Map
,因为此调用不带任何参数。然后从结果映射中检索参与者列表并返回给调用者。
将 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
。
org.springframework.jdbc.object.SqlQuery
使用 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;
}
}
示例类扩展了 MappingSqlQuery
,泛型类型是 Actor
。构造函数将 DataSource
作为唯一参数。在这个构造函数中,您可以使用 DataSource
和 SQL 调用超类上的构造函数来检索此查询的行。此 SQL 用于创建 PreparedStatement
,因此它可能包含在执行期间传入的任何参数的占位符。您必须通过使用 declareParameter
方法来声明每个参数,并传入一个 SqlParameter
。SqlParameter
需要一个名称和在 java.sql.Types
所限定的 JDBC 类型。定义所有参数后,您可以调用 compile()
方法,以便可以准备语句并稍后运行。这个类编译后是线程安全的,所以只要在 DAO 初始化的时候创建了这些实例,就可以作为实例变量保存起来,重复使用。下面的例子展示了如何定义这样一个类:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
return actorMappingQuery.findObject(id);
}
前面示例中的方法使用作为唯一参数传入的 id
来检索客户。由于我们只希望返回一个对象,因此我们调用了以 id
为参数的便捷方法 findObject
。如果我们有一个返回对象列表并接受其他参数的查询,我们将使用其中一种 execute
方法,该方法接受作为可变参数传入的参数值数组。以下示例显示了这样的方法:
public List<Actor> searchForActors(int age, String namePattern) {
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
org.springframework.jdbc.object.MappingSqlQuery
使用 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);
}
}
org.springframework.jdbc.object.SqlUpdate
使用 StoredProcedure
StoredProcedure
类是一个用于 RDBMS 存储过程的对象的抽象超类。
继承的 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
来调用任何 Oracle 数据库附带的函数 ( sysdate()
)。要使用存储过程功能,您必须创建一个类继承 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.object.StoredProcedure
参数和数据值处理的常见问题
为参数提供 SQL 类型信息
通常,Spring 会根据传入的参数类型来确定参数的 SQL 类型。可以在设置参数值时显式提供要使用的 SQL 类型。这有时是正确设置 NULL
值所必需的。
您可以通过多种方式提供 SQL 类型信息:
-
JdbcTemplate
的许多 update 和 query 方法都以int
数组的形式接受额外的参数。该数组用于通过使用java.sql.Types
类中的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。 -
您可以使用
SqlParameterValue
类来包装需要此附加信息的参数值。为此,请为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。您还可以为数值提供可选的比例参数。 -
对于使用命名参数的方法,您可以使用
SqlParameterSource
,BeanPropertySqlParameterSource
或MapSqlParameterSource
。它们都有为任何命名参数值注册 SQL 类型的方法。 -
java.sql.Types
处理 BLOB 和 CLOB 对象
您可以在数据库中存储图像、其他二进制数据和大块文本。这些大对象对于二进制数据称为 BLOB(Binary Large OBject),对于字符数据称为 CLOB(Character Large OBject)。在 Spring 中,您可以直接使用 JdbcTemplate
,也可以使用 RDBMS 对象和 SimpleJdbc
类提供的更高抽象来处理这些大对象。所有这些方法都使用 LobHandler
接口的实现来实际管理 LOB(大对象)数据。 LobHandler
类通过 getLobCreator
方法提供对 LobCreator
类的访问,该类用于创建要插入的新 LOB 对象。
LobCreator
和 LobHandler
为 LOB 输入和输出提供以下支持:
- BLOB
byte[]
:getBlobAsBytes
和setBlobAsBytes
InputStream
:getBlobAsBinaryStream
和setBlobAsBinaryStream
- CLOB
String
:getClobAsString
和setClobAsString
InputStream
:getClobAsAsciiStream
和setClobAsAsciiStream
Reader
:getClobAsCharacterStream
和setClobAsCharacterStream
下一个示例显示如何创建和插入 BLOB 。
此示例使用 JdbcTemplate
和 AbstractLobCreatingPreparedStatementCallback
的实现 。它实现了 setValues
方法。此方法提供了我们用来设置 SQL 插入语句中 LOB 列的值的 LobCreator
。
对于此示例,我们假设有一个变量 ,lobHandler
,已设置为 DefaultLobHandler
的实例。通常通过依赖注入设置此值。
以下示例显示了如何创建和插入 BLOB :
final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());
}
}
);
blobIs.close();
clobReader.close();
如果您调用 DefaultLobHandler.getLobCreator()
返回的 LobCreator
里的 setBlobAsBinaryStream
,setClobAsAsciiStream
或 setClobAsCharacterStream
方法,您可以选择指定 contentLength
参数为负值。如果指定的内容长度为负数,则 DefaultLobHandler
使用不带长度参数的 set-stream 方法的 JDBC 4.0 变体。否则,它将指定的长度传递给驱动程序。
现在是时候从数据库中读取 LOB 数据了。同样,您将带有相同的实例变量 lobHandler
的 JdbcTemplate
和对 DefaultLobHandler
的引用一起使用。以下示例显示了如何执行此操作:
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
new RowMapper<Map<String, Object>>() {
public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
Map<String, Object> results = new HashMap<String, Object>();
String clobText = lobHandler.getClobAsString(rs, "a_clob");
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");
results.put("BLOB", blobBytes);
return results;
}
});
传入 IN 子句的值列表
SQL 标准允许基于包含变量值列表的表达式选择行。一个典型的例子是 select * from T_ACTOR where id in (1, 2, 3)
。JDBC 标准不直接为准备好的语句支持此变量列表。您不能声明可变数量的占位符。您需要准备一些具有所需占位符数量的变体,或者在知道需要多少个占位符后动态生成 SQL 字符串。命名参数支持,在 NamedParameterJdbcTemplate
和 JdbcTemplate
上采用后一种。您可以将值作为原始对象的 java.util.List
传递。此列表用于插入所需的占位符并在语句执行期间传入值。
传入多个值时要小心。JDBC 标准不保证您可以为 in
表达式列表使用 100 个以上的值。各种数据库都超过了这个数字,但它们通常对允许的值数量有一个硬性限制。例如,Oracle 的限制是 1000
除了值列表中的原始值之外,您还可以创建一个 java.util.List
对象数组。此列表可以支持为 in
子句定义多个表达式,例如 select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))
。当然,这要求您的数据库支持这种语法。
处理存储过程调用的复杂类型
调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,Spring 当从存储过程调用返回时提供了 SqlReturnType
用于处理它们,提供了 SqlTypeValue
当作为参数传递给存储过程时进行处理。
SqlReturnType
接口有一个必须实现的方法(名为 getTypeValue
)。此接口用作 SqlOutParameter
声明的一部分。以下示例显示返回用户声明类型 ITEM_TYPE
的 Oracle STRUCT
对象的值 :
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
// ...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
(CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
STRUCT struct = (STRUCT) cs.getObject(colIndx);
Object[] attr = struct.getAttributes();
TestItem item = new TestItem();
item.setId(((Number) attr[0]).longValue());
item.setDescription((String) attr[1]);
item.setExpirationDate((java.util.Date) attr[2]);
return item;
}));
// ...
}
您可以使用 SqlTypeValue
将 Java 对象(例如 TestItem
)的值传递给存储过程。SqlTypeValue
接口有一个必须实现的方法(名为 createTypeValue
)。传入活动连接,您可以使用它来创建特定于数据库的对象,例如 StructDescriptor
实例或 ArrayDescriptor
实例。以下示例创建一个 StructDescriptor
实例:
final TestItem testItem = new TestItem(123L, "A test item",
new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
Struct item = new STRUCT(itemDescriptor, conn,
new Object[] {
testItem.getId(),
testItem.getDescription(),
new java.sql.Date(testItem.getExpirationDate().getTime())
});
return item;
}
};
您现在可以将 SqlTypeValue
添加到包含 execute
调用存储过程的输入参数的 Map
。
SqlTypeValue
的另一个用途是将值数组传递给 Oracle 存储过程。在这种情况下,必须使用 Oracle 自己的内部类 ARRAY
,您可以使用 SqlTypeValue
来创建 Oracle ARRAY
的实例并使用来自 Java ARRAY
的值填充,如以下示例所示:
final Long[] ids = new Long[] {1L, 2L};
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
return idArray;
}
};
嵌入式数据库支持
org.springframework.jdbc.datasource.embedded
包提供对嵌入式 Java 数据库引擎的支持。提供对 HSQL 、 H2 和 Derby 的支持。您还可以使用可扩展的 API 来插入新的嵌入式数据库类型和 DataSource
实现。
为什么要使用嵌入式数据库?
由于其轻量级的特性,嵌入式数据库在项目的开发阶段非常有用。优点包括易于配置、启动时间短、可测试性以及在开发过程中快速改进 SQL 的能力。
使用 Spring XML 创建嵌入式数据库
如果要将嵌入式数据库实例公开为 Spring ApplicationContext
中的 bean ,可以使用 spring-jdbc
命名空间中的 embedded-database
标记:
<jdbc:embedded-database id="dataSource" generate-name="true">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
前面的配置创建了一个嵌入式 HSQL 数据库,该数据库填充了来自类路径根目录中的 schema.sql
和 test-data.sql
资源的SQL 。此外,作为最佳实践,为嵌入式数据库分配了一个唯一生成的名称。嵌入式数据库作为 javax.sql.DataSource
类型的 bean 提供给 Spring 容器 ,然后可以根据需要将其注入到数据访问对象中。
以编程方式创建嵌入式数据库
EmbeddedDatabaseBuilder
类提供链式 API 以编程方式构造嵌入式数据库。当您需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用它,如下例所示:
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
db.shutdown()
还可以使用 EmbeddedDatabaseBuilder
以 Java 配置来创建嵌入式数据库,如以下示例所示:
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
}
}
选择嵌入式数据库类型
使用 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)
使用嵌入式数据库测试数据访问逻辑
嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重复使用时,使用这样的模板对于一次性使用很有用。但是,如果您希望创建在测试套件中共享的嵌入式数据库,请考虑使用 Spring TestContext Framework 并将嵌入式数据库配置为 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}">
<jdbc:script location="..."/>
</jdbc:initialize-database>
控制现有数据发生的情况的第二个选项是容忍失败。为此,您可以控制初始化程序忽略它从脚本运行的 SQL 中的某些错误的能力,如以下示例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的示例中,我们说我们期望脚本有时针对空数据库运行,因此脚本中的某些 DROP
语句会失败。所以失败的 DROP
SQL 语句将被忽略,但其他失败将导致异常。如果您的 SQL 方言不支持 DROP … IF EXISTS
,但您想在重新创建之前无条件地删除所有测试数据,这将很有用。在这种情况下,第一个脚本通常是一组 DROP
语句,然后是一组 CREATE
语句。
ignore-failures
选项可以设置为 NONE
(默认)、DROPS
(忽略失败的丢弃)或 ALL
(忽略所有失败)。
如果脚本中根本不存在 ;
字符,则每个语句都应由 ;
或一个新行分隔。您可以通过脚本进行全局或脚本控制,如下例所示:
<jdbc:initialize-database data-source="dataSource" separator="@@">
<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/>
<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>
在此示例中,两个 test-data
脚本用 @@
作语句分隔符,并且仅 db-schema.sql
使用 ;
。 此配置指定默认分隔符是 @@
并覆盖 db-schema
脚本的默认值。
如果您需要比从 XML 命名空间获得更多的控制,您可以直接使用 DataSourceInitializer
并将其定义为应用程序中的组件。
依赖数据库的其他组件的初始化
数据库初始化器依赖于 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 应用程序中很常见