On this page
19. 使用 JDBC 进行数据访问
19.1 Spring Framework JDBC 简介
下表概述的动作序列可能最好地显示了 Spring Framework JDBC 抽象提供的增值。该表显示了 Spring 将采取哪些操作,以及哪些操作是您(应用程序开发人员)的责任。
表 19.1. Spring JDBC-谁做什么?
Action | Spring | You |
---|---|---|
定义连接参数。 | X | |
打开连接。 | X | |
指定 SQL 语句。 | X | |
声明参数并提供参数值 | X | |
准备并执行该语句。 | X | |
设置循环以遍历结果(如果有)。 | X | |
进行每次迭代的工作。 | X | |
处理任何异常。 | X | |
Handle transactions. | X | |
关闭连接,语句和结果集。 | X |
Spring 框架负责所有可能使 JDBC 成为乏味的 API 的底层细节。
19.1.1 选择用于 JDBC 数据库访问的方法
您可以选择几种方法来构成 JDBC 数据库访问的基础。除了三种形式的 JdbcTemplate 外,新的 SimpleJdbcInsert 和 SimplejdbcCall 方法还优化了数据库元数据,并且 RDBMS Object 样式采用了一种与 JDO Query 设计类似的面向对象的方法。一旦开始使用这些方法之一,您仍然可以混合搭配以包含来自其他方法的功能。所有方法都需要兼容 JDBC 2.0 的驱动程序,某些高级功能需要 JDBC 3.0 驱动程序。
-
- JdbcTemplate *是经典的 Spring JDBC 方法,也是最受欢迎的方法。这种“最低级别”的方法以及其他所有方法都在幕后使用了 JdbcTemplate。
-
- NamedParameterJdbcTemplate *包装
JdbcTemplate
来提供命名参数,而不是传统的 JDBC“?”占位符。当您有多个 SQL 语句参数时,此方法可提供更好的文档编制和易用性。
- NamedParameterJdbcTemplate *包装
-
- SimpleJdbcInsert 和 SimpleJdbcCall *优化数据库元数据以限制必要的配置量。这种方法简化了编码,因此您只需要提供表或过程的名称,并提供与列名称匹配的参数 Map 即可。仅当数据库提供足够的元数据时,此方法才有效。如果数据库不提供此元数据,则必须提供参数的显式配置。
-
- RDBMS 对象,包括 MappingSqlQuery,SqlUpdate 和 StoredProcedure *,要求您在数据访问层初始化期间创建可重用且线程安全的对象。此方法以 JDO Query 为模型,其中您定义查询字符串,声明参数并编译查询。完成此操作后,可以传入各种参数值来多次调用 execute 方法。
19.1.2 程序包层次结构
Spring 框架的 JDBC 抽象框架由四个不同的包组成,即core
,datasource
,object
和support
。
org.springframework.jdbc.core
包包含JdbcTemplate
类及其各种回调接口,以及各种相关类。名为org.springframework.jdbc.core.simple
的子包包含SimpleJdbcInsert
和SimpleJdbcCall
类。另一个名为org.springframework.jdbc.core.namedparam
的子程序包包含NamedParameterJdbcTemplate
类和相关的支持类。参见第 19.2 节“使用 JDBC 核心类控制基本的 JDBC 处理和错误处理”,第 19.4 节“ JDBC 批处理操作”和第 19.5 节“使用 SimpleJdbc 类简化 JDBC 操作”。
org.springframework.jdbc.datasource
软件包包含一个用于DataSource
轻松访问的 Util 类,以及各种简单的DataSource
实现,可用于在 Java EE 容器外部测试和运行未修改的 JDBC 代码。名为org.springfamework.jdbc.datasource.embedded
的子程序包支持使用 Java 数据库引擎(例如 HSQL,H2 和 Derby)创建嵌入式数据库。参见第 19.3 节“控制数据库连接”和第 19.8 节“嵌入式数据库支持”。
org.springframework.jdbc.object
软件包包含将 RDBMS 查询,更新和存储过程表示为线程安全的可重用对象的类。参见第 19.6 节“将 JDBC 操作建模为 Java 对象”。这种方法是由 JDO 建模的,尽管查询返回的对象自然会与数据库断开连接。较高级别的 JDBC 抽象取决于org.springframework.jdbc.core
包中的较低级别的抽象。
org.springframework.jdbc.support
软件包提供SQLException
转换功能和一些 Util 类。 JDBC 处理期间引发的异常将转换为org.springframework.dao
包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换的异常均未选中,这使您可以选择捕获可从中恢复的异常,同时允许将其他异常传播到调用方。参见第 19.2.3 节“ SQLExceptionTranslator”。
19.2 使用 JDBC 核心类控制基本的 JDBC 处理和错误处理
19.2.1 JdbcTemplate
JdbcTemplate
类是 JDBC 核心软件包中的中心类。它处理资源的创建和释放,这有助于您避免常见的错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务,例如语句创建和执行,而使应用程序代码提供 SQL 并提取结果。 JdbcTemplate
类执行 SQL 查询,更新语句和存储过程调用,在ResultSet
上执行迭代并提取返回的参数值。它还捕获 JDBC 异常,并将其转换为org.springframework.dao
包中定义的通用,信息量更大的异常层次结构。
在代码中使用JdbcTemplate
时,只需实现回调接口,即可为它们明确定义 Contract。 PreparedStatementCreator
回调接口根据此类提供的Connection
创建准备好的语句,并提供 SQL 和任何必要的参数。 CallableStatementCreator
接口(创建可调用语句)也是如此。 RowCallbackHandler
接口从ResultSet
的每一行提取值。
JdbcTemplate
可以通过直接使用DataSource
引用实例化在 DAO 实现中使用,也可以在 Spring IoC 容器中进行配置并作为 Bean 引用提供给 DAO。
Note
DataSource
应该始终配置为 Spring IoC 容器中的 bean。在第一种情况下,将 Bean 直接提供给服务。在第二种情况下,将其提供给准备好的模板。
此类发出的所有 SQL 都记录在DEBUG
级别下的类别下,该类别对应于模板实例的完全限定的类名称(通常为JdbcTemplate
,但是如果您使用JdbcTemplate
类的自定义子类,则可能会有所不同)。
JdbcTemplate 类用法示例
本节提供了JdbcTemplate
类用法的一些示例。这些示例不是JdbcTemplate
公开的所有功能的详尽列表;请参阅相关的 javadocs。
Querying (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 = ?",
new Object[]{1212L}, String.class);
查询并填充单个域对象:
Actor actor = this.jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
new Object[]{1212L},
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
查询和填充多个域对象:
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
如果最后两个代码段确实存在于同一应用程序中,则有必要删除两个RowMapper
匿名内部类中存在的重复项,并将它们提取到单个类(通常是static
嵌套类)中,然后可以根据需要由 DAO 方法引用。例如,最好编写如下的最后一个代码片段:
public List<Actor> findAllActors() {
return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}
private static final class ActorMapper implements RowMapper<Actor> {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
使用 JdbcTemplate 更新(插入/更新/删除)
您使用update(..)
方法执行插入,更新和删除操作。参数值通常以 var args 或对象数组的形式提供。
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 actor where id = ?",
Long.valueOf(actorId));
其他 JdbcTemplate 操作
您可以使用execute(..)
方法执行任意 SQL,因此该方法通常用于 DDL 语句。带有回调接口,绑定变量数组等的变体严重地超载了它。
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
下面的示例调用一个简单的存储过程。 covered later是更复杂的存储过程支持。
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
JdbcTemplate 最佳做法
JdbcTemplate
类的实例一旦配置便是* threadsafe 。这很重要,因为这意味着您可以配置JdbcTemplate
的单个实例,然后安全地将此“共享”引用注入多个 DAO(或存储库)中。 JdbcTemplate
是有状态的,因为它维护对DataSource
的引用,但是此状态为非*会话状态。
使用JdbcTemplate
类(和相关的NamedParameterJdbcTemplate类)的一种常见做法是在 Spring 配置文件中配置DataSource
,然后将共享的DataSource
bean 依赖注入到 DAO 类中。 JdbcTemplate
在DataSource
的设置器中创建。这导致 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 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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://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>
显式配置的替代方法是使用组件扫描和 Comments 支持进行依赖项注入。在这种情况下,您可以使用@Repository
Comments 类(这使其成为组件扫描的候选对象),并使用@Autowired
CommentsDataSource
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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://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
实例是线程安全的。如果您的应用程序访问多个数据库(这需要多个DataSources
,随后需要多个不同配置的JdbcTemplates
),则可能需要多个JdbcTemplate
实例。
19.2.2 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate
类增加了对使用命名参数编程 JDBC 语句的支持,这与仅使用经典占位符('?'
)参数进行编程的 JDBC 相反。 NamedParameterJdbcTemplate
类包装JdbcTemplate
,并委派给包装的JdbcTemplate
以完成其大部分工作。本节仅描述NamedParameterJdbcTemplate
类中与JdbcTemplate
本身不同的区域。即,使用命名参数对 JDBC 语句进行编程。
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from T_ACTOR where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
请注意,在分配给sql
变量的值和插入到namedParameters
变量(类型MapSqlParameterSource
)中的相应值中使用了命名参数符号。
或者,您可以使用基于Map
的样式将命名参数及其对应的值传递给NamedParameterJdbcTemplate
实例。NamedParameterJdbcOperations
公开并由NamedParameterJdbcTemplate
类实现的其余方法遵循类似的模式,此处不再赘述。
下面的示例说明基于Map
的样式的用法。
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from T_ACTOR where first_name = :first_name";
Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
与NamedParameterJdbcTemplate
相关联(并且存在于同一 Java 包中)的一项不错的功能是SqlParameterSource
接口。您已经在前面的代码片段之一(MapSqlParameterSource
类)中看到了此接口的实现示例。 SqlParameterSource
是NamedParameterJdbcTemplate
的命名参数值的来源。 MapSqlParameterSource
类是一个非常简单的实现,它只是java.util.Map
周围的适配器,其中键是参数名称,值是参数值。
另一个SqlParameterSource
实现是BeanPropertySqlParameterSource
类。此类包装任意 JavaBean(即,遵循JavaBean 约定的类的实例),并将包装的 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...
}
// 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 最佳做法”的部分,以获取有关在应用程序上下文中使用NamedParameterJdbcTemplate
类的准则。
19.2.3 SQLExceptionTranslator
SQLExceptionTranslator
是要由可以在SQLExceptions
和 Spring 自己的org.springframework.dao.DataAccessException
之间进行转换的类实现的接口,而在数据访问策略方面则不可知。为了提高精度,实现可以是通用的(例如,使用 SQLState 代码用于 JDBC)或专有的(例如,使用 Oracle 错误代码)。
SQLErrorCodeSQLExceptionTranslator
是SQLExceptionTranslator
的实现,默认情况下使用。此实现使用特定的供应商代码。它比SQLState
实现更为精确。错误代码转换基于 JavaBean 类型类SQLErrorCodes
中保存的代码。此类由SQLErrorCodesFactory
创建和填充,顾名思义,该类是用于基于名为sql-error-codes.xml
的配置文件的内容创建SQLErrorCodes
的工厂。该文件使用供应商代码填充,并且基于DatabaseMetaData
中的DatabaseProductName
填充。使用您正在使用的实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator
按以下 Sequences 应用匹配规则:
Note
SQLErrorCodesFactory
默认用于定义错误代码和自定义异常转换。在 Classpath 的名为sql-error-codes.xml
的文件中查找它们,并根据使用中数据库的数据库元数据中的数据库名称找到匹配的SQLErrorCodes
实例。
子类实现的任何自定义转换。通常情况下,将使用提供的具体
SQLErrorCodeSQLExceptionTranslator
,因此该规则不适用。仅当您确实提供了子类实现时才适用。作为
SQLErrorCodes
类的customSqlExceptionTranslator
属性提供的SQLExceptionTranslator
接口的任何自定义实现。搜索为
SQLErrorCodes
类的customTranslations
属性提供的CustomSQLErrorCodesTranslation
类的实例列表。错误代码匹配被应用。
使用后备翻译器。
SQLExceptionSubclassTranslator
是默认的后备翻译器。如果此翻译不可用,则下一个后备翻译器是SQLStateSQLExceptionTranslator
。
您可以扩展SQLErrorCodeSQLExceptionTranslator:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) {
if (sqlex.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlex);
}
return null;
}
}
在此示例中,特定的错误代码-12345
被转换,而其他错误则由默认转换器实现转换。若要使用此自定义转换器,必须通过方法setExceptionTranslator
将其传递给JdbcTemplate
,并将此JdbcTemplate
用于需要该转换器的所有数据访问处理。这是如何使用此自定义转换器的示例:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
自定义转换器会传递一个数据源,以便在sql-error-codes.xml
中查找错误代码。
19.2.4 执行语句
执行一条 SQL 语句需要很少的代码。您需要DataSource
和JdbcTemplate
,包括JdbcTemplate
随附的便捷方法。以下示例显示了创建一个新表的最小但功能齐全的类需要包含的内容:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAStatement {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void doExecute() {
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
}
}
19.2.5 运行查询
一些查询方法返回单个值。要从一行中检索计数或特定值,请使用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
,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}]
19.2.6 更新数据库
以下示例显示为特定主键更新的列。在此示例中,SQL 语句具有用于行参数的占位符。参数值可以作为 varargs 或作为对象数组传递。因此,应将 Primitives 显式地或使用自动装箱包装在 Primitives 包装类中。
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);
}
}
19.2.7 检索自动生成的密钥
update()
便捷方法支持检索由数据库生成的主键。该支持是 JDBC 3.0 标准的一部分。有关详细信息,请参见规范的第 13.6 章。该方法以PreparedStatementCreator
作为其第一个参数,这是指定所需插入语句的方式。另一个参数是KeyHolder
,它包含从更新成功返回时生成的密钥。没有创建合适的PreparedStatement
的标准单一方法(这说明了为什么方法签名就是这样)。以下示例在 Oracle 上有效,但在其他平台上可能不适用:
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"});
ps.setString(1, name);
return ps;
}
},
keyHolder);
// keyHolder.getKey() now contains the generated key
19.3 控制数据库连接
19.3.1 DataSource
Spring 通过DataSource
获得与数据库的连接。 DataSource
是 JDBC 规范的一部分,是通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务 Management 问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息。这是设置数据源的 Management 员的责任。您很可能在开发和测试代码时同时担当这两个角色,但是不必一定要知道如何配置生产数据源。
当使用 Spring 的 JDBC 层时,您可以从 JNDI 获取数据源,或者使用第三方提供的连接池实现来配置自己的数据源。流行的实现是 Apache Jakarta Commons DBCP 和 C3P0. Spring 发行版中的实现仅用于测试目的,不提供池化。
本节使用 Spring 的DriverManagerDataSource
实现,稍后将介绍其他一些实现。
Note
仅使用DriverManagerDataSource
类应仅用于测试目的,因为它不提供池化,并且在发出多个连接请求时性能会很差。
您通常会获得 JDBC 连接,因此获得与DriverManagerDataSource
的连接。指定 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 和 C3P0 的基本连接和配置。要了解更多有助于控制池功能的选项,请参阅相应连接池实现的产品文档。
DBCP configuration:
<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 configuration:
<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"/>
19.3.2 DataSourceUtils
DataSourceUtils
类是一种方便且功能强大的帮助器类,它提供static
个方法以从 JNDI 获取连接并在必要时关闭连接。它支持例如DataSourceTransactionManager
的线程绑定连接。
19.3.3 SmartDataSource
SmartDataSource
接口应该由可以提供与关系数据库的连接的类来实现。它扩展了DataSource
接口,以允许使用它的类查询在给定操作后是否应关闭连接。当您知道将重用连接时,此用法非常有效。
19.3.4 AbstractDataSource
AbstractDataSource
是 Spring 的DataSource
实现的abstract
Base Class,该类实现所有DataSource
实现的通用代码。如果要编写自己的DataSource
实现,则可以扩展AbstractDataSource
类。
19.3.5 SingleConnectionDataSource
SingleConnectionDataSource
类是SmartDataSource
接口的实现,该接口包装了**单个* Connection
,每次使用后*都未关闭。显然,这不是多线程功能。
如果使用共享工具,假设任何 Client 端代码在构建池连接的情况下都调用close
,请将suppressClose
属性设置为true
。此设置返回一个封闭物理连接的封闭代理。请注意,您将无法再将其强制转换为本机 Oracle Connection
等。
这主要是测试类。例如,它结合简单的 JNDI 环境,可以在应用服务器外部轻松测试代码。与DriverManagerDataSource
相比,它始终重用同一连接,从而避免了过多的物理连接创建。
19.3.6 DriverManagerDataSource
DriverManagerDataSource
类是标准DataSource
接口的实现,该接口通过 bean 属性配置纯 JDBC 驱动程序,并每次返回一个新的Connection
。
此实现对于 Java EE 容器外部的测试和独立环境非常有用,可以作为 Spring IoC 容器中的DataSource
bean 或与简单的 JNDI 环境结合使用。池假设Connection.close()
调用将简单地关闭连接,因此任何DataSource
感知的持久性代码都应起作用。但是,即使在测试环境中,使用commons-dbcp
之类的 JavaBean 风格的连接池也是如此容易,以至于总是总是比DriverManagerDataSource
使用这种连接池更可取。
19.3.7 TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy
是目标DataSource
的代理,该代理包装该目标DataSource
以增加对 SpringManagement 的事务的了解。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource
。
Note
很少需要使用此类,除非必须调用并传递标准 JDBC DataSource
接口实现的现有代码。在这种情况下,可能仍然可以使用此代码,同时使该代码参与 Spring 托管的事务。通常最好使用更高级别的资源 Management 抽象来编写自己的新代码,例如JdbcTemplate
或DataSourceUtils
。
(有关更多详细信息,请参见TransactionAwareDataSourceProxy
javadocs.)
19.3.8 DataSourceTransactionManager
DataSourceTransactionManager
类是单个 JDBC 数据源的PlatformTransactionManager
实现。它将 JDBC 连接从指定的数据源绑定到当前正在执行的线程,可能允许每个数据源一个线程连接。
需要应用程序代码才能通过DataSourceUtils.getConnection(DataSource)
而不是 Java EE 的标准DataSource.getConnection
检索 JDBC 连接。它引发未检查的org.springframework.dao
异常,而不是已检查的SQLExceptions
。 JdbcTemplate
之类的所有框架类均暗含使用此策略。如果不与该事务 Management 器一起使用,则查找策略的行为与普通策略完全相同-因此可以在任何情况下使用。
DataSourceTransactionManager
类支持自定义隔离级别,以及作为适当的 JDBC 语句查询超时应用的超时。为了支持后者,应用程序代码必须对每个创建的语句使用JdbcTemplate
或调用DataSourceUtils.applyTransactionTimeout(..)
方法。
在单个资源的情况下,可以使用此实现代替JtaTransactionManager
,因为它不需要容器支持 JTA。如果您坚持要求的连接查找模式,则仅在配置之间进行切换。 JTA 不支持自定义隔离级别!
19.3.9 NativeJdbcExtractor
有时,您需要访问与标准 JDBC API 不同的特定于供应商的 JDBC 方法。如果您在应用程序服务器中运行,或者使用DataSource
来将Connection
,Statement
和ResultSet
对象包装自己的包装对象,则可能会出现问题。要访问本机对象,可以将JdbcTemplate
或OracleLobHandler
配置为NativeJdbcExtractor
。
NativeJdbcExtractor
具有多种风格以匹配您的执行环境:
SimpleNativeJdbcExtractor
C3P0NativeJdbcExtractor
CommonsDbcpNativeJdbcExtractor
JBossNativeJdbcExtractor
WebLogicNativeJdbcExtractor
WebSphereNativeJdbcExtractor
XAPoolNativeJdbcExtractor
通常,在大多数环境中,SimpleNativeJdbcExtractor
足以解开Connection
对象。有关更多详细信息,请参见 javadocs。
19.4 JDBC 批处理操作
如果将多个调用批处理到同一条准备好的语句,则大多数 JDBC 驱动程序都会提高性能。通过将更新分组,可以限制到数据库的往返次数。
19.4.1 使用 JdbcTemplate 的基本批处理操作
通过实现一个特殊接口BatchPreparedStatementSetter
的两个方法并将其作为batchUpdate
方法调用中的第二个参数传入,可以完成JdbcTemplate
批处理。使用getBatchSize
方法提供当前批次的大小。使用setValues
方法设置准备好的语句的参数值。该方法将被调用您在getBatchSize
调用中指定的次数。以下示例根据列表中的条目更新 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 {
ps.setString(1, actors.get(i).getFirstName());
ps.setString(2, actors.get(i).getLastName());
ps.setLong(3, actors.get(i).getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
如果您正在处理更新流或正在从文件中读取文件,则可能具有首选的批处理大小,但最后一批可能没有该数量的条目。在这种情况下,您可以使用InterruptibleBatchPreparedStatementSetter
界面,一旦 Importing 源用尽,您就可以中断批处理。 isBatchExhausted
方法使您可以发出批处理结束的 signal。
19.4.2 具有对象列表的批处理操作
JdbcTemplate
和NamedParameterJdbcTemplate
都提供了另一种提供批处理更新的方式。无需实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值,并使用内部准备好的语句设置器。 API 取决于您是否使用命名参数。对于命名参数,您提供一个SqlParameterSource
数组,每个批次的成员一个条目。您可以使用SqlParameterSourceUtils.createBatch
便捷方法来创建此数组,并传入一个由 bean 样式的对象(具有与参数对应的 getter 方法)和/或由 String-keyed Maps(包含相应的参数作为值)组成的数组。
此示例显示使用命名参数的批量更新:
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.toArray()));
}
// ... additional methods
}
对于使用经典“?”的 SQL 语句占位符,则传入包含包含更新值的对象数组的列表。该对象数组在 SQL 语句中的每个占位符必须具有一个条目,并且它们的 Sequences 必须与 SQL 语句中定义的 Sequences 相同。
使用经典 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 值。
19.4.3 具有多个批次的批次操作
批处理更新的最后一个示例处理的批处理太大,以至于您想将它们分成几个较小的批处理。当然,您可以通过多次调用batchUpdate
方法来使用上述方法来执行此操作,但是现在有了更方便的方法。除 SQL 语句外,此方法还包含一个对象集合,其中包含参数,每个批处理要进行的更新次数以及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,
new ParameterizedPreparedStatementSetter<Actor>() {
public void setValues(PreparedStatement ps, Actor argument) throws SQLException {
ps.setString(1, argument.getFirstName());
ps.setString(2, argument.getLastName());
ps.setLong(3, argument.getId().longValue());
}
});
return updateCounts;
}
// ... additional methods
}
此调用的批处理更新方法返回一个 int 数组数组,该数组包含每个批处理的数组条目以及每个更新受影响的行数的数组。顶级数组的长度指示已执行的批处理数,第二级数组的长度指示该批处理中的更新数。每个批次中的更新数量应该是为所有批次提供的批次大小,但最后一个数量可能会少一些,这取决于所提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的计数。如果该计数不可用,则 JDBC 驱动程序将返回-2 值。
19.5 使用 SimpleJdbc 类简化 JDBC 操作
SimpleJdbcInsert
和SimpleJdbcCall
类通过利用可通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。这意味着无需进行过多的配置,尽管如果您愿意在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。
19.5.1 使用 SimpleJdbcInsert 插入数据
让我们从使用最少配置选项的SimpleJdbcInsert
类开始。您应该在数据访问层的初始化方法中实例化SimpleJdbcInsert
。对于此示例,初始化方法是setDataSource
方法。您不需要将SimpleJdbcInsert
类子类化;只需创建一个新实例并使用withTableName
方法设置表名。此类的配置方法遵循“ fluid”样式,该样式返回SimpleJdbcInsert
的实例,该实例允许您链接所有配置方法。本示例仅使用一种配置方法。稍后您将看到多个示例。
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(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.utils.Map
作为其唯一参数。这里要注意的重要一点是,用于 Map 的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据是为了构造实际的插入语句。
19.5.2 使用 SimpleJdbcInsert 检索自动生成的密钥
本示例使用与前面相同的插入方式,但是它没有传递 id,而是检索自动生成的键并将其设置在新的 Actor 对象上。创建SimpleJdbcInsert
时,除了指定表名之外,还可以使用usingGeneratedKeyColumns
方法指定生成的键列的名称。
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(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
对象,您可以使用该对象创建在我们的域类中使用的数字类型的实例。您不能在这里依靠所有数据库来返回特定的 Java 类。 java.lang.Number
是您可以依赖的 Base Class。如果您有多个自动生成的列,或者生成的值是非数字的,则可以使用从executeAndReturnKeyHolder
方法返回的KeyHolder
。
19.5.3 为 SimpleJdbcInsert 指定列
您可以通过使用usingColumns
方法指定列名列表来限制插入的列:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(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
}
插入的执行与依靠元数据确定要使用的列的执行相同。
19.5.4 使用 SqlParameterSource 提供参数值
使用Map
提供参数值可以很好地工作,但这不是最方便使用的类。 Spring 提供了SqlParameterSource
接口的几种实现方式,可以替代使用。第一个是BeanPropertySqlParameterSource
,如果您有一个包含值的 JavaBean 兼容类,这是一个非常方便的类。它将使用相应的 getter 方法提取参数值。这是一个例子:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(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
}
另一个选项是类似于 Map 的MapSqlParameterSource
,但提供了一种更方便的addValue
方法,可以将其链接。
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(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
}
如您所见,配置是相同的。只有执行代码才能更改为使用这些替代 Importing 类。
19.5.5 使用 SimpleJdbcCall 调用存储过程
SimpleJdbcCall
类利用数据库中的元数据来查找in
和out
参数的名称,因此您不必显式声明它们。如果愿意,可以声明参数,或者声明诸如ARRAY
或STRUCT
之类的参数没有自动 Map 到 Java 类的参数。第一个示例显示了一个简单过程,该过程仅从 MySQL 数据库返回VARCHAR
和DATE
格式的标量值。示例过程读取指定的 actor 条目,并以out
参数的形式返回first_name
,last_name
和birth_date
列。
CREATE PROCEDURE read_actor (
IN in_id INTEGER,
OUT out_first_name VARCHAR(100),
OUT out_last_name VARCHAR(100),
OUT out_birth_date DATE)
BEGIN
SELECT first_name, last_name, birth_date
INTO out_first_name, out_last_name, out_birth_date
FROM t_actor where id = in_id;
END;
in_id
参数包含您要查找的演员的id
。 out
参数返回从表读取的数据。
SimpleJdbcCall
的声明方式与SimpleJdbcInsert
相似。您应该在数据访问层的初始化方法中实例化并配置该类。与 StoredProcedure 类相比,您不必创建子类,也不必声明可以在数据库元数据中查找的参数。以下是使用上述存储过程的 SimpleJdbcCall 配置示例。除了DataSource
之外,唯一的配置选项是存储过程的名称。
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(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
。将 Importing 值提供的名称与存储过程中声明的参数名称的名称进行匹配很重要。大小写不必匹配,因为您使用元数据来确定在存储过程中应如何引用数据库对象。源中为存储过程指定的内容不一定是存储过程在数据库中存储的方式。一些数据库将名称转换为全部大写,而另一些数据库使用小写或指定的大小写。
execute
方法采用 IN 参数,并返回一个 Map,该 Map 包含以存储过程中指定的名称为关键字的任何out
参数。在这种情况下,它们是out_first_name, out_last_name
和out_birth_date
。
execute
方法的最后一部分创建一个 Actor 实例,用于返回检索到的数据。同样,重要的是使用在存储过程中声明的out
参数的名称。同样,结果 Map 中存储的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
参数名称的情况下发生冲突。
19.5.6 明确声明要用于 SimpleJdbcCall 的参数
您已经了解了如何基于元数据推导参数,但是如果需要,可以显式声明。为此,您可以使用declareParameters
方法创建并配置SimpleJdbcCall
,该方法将可变数量的SqlParameter
对象作为 Importing。有关如何定义SqlParameter
的详细信息,请参见下一部分。
Note
如果您使用的数据库不是 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
}
这两个示例的执行结果和最终结果相同。这一细节明确地指定了所有细节,而不是依赖于元数据。
19.5.7 如何定义 SqlParameters
要为第 19.6 节“将 JDBC 操作建模为 Java 对象”中介绍的 SimpleJdbc 类以及 RDBMS 操作类定义参数,请使用SqlParameter
或其子类之一。您通常在构造函数中指定参数名称和 SQL 类型。使用java.sql.Types
常量指定 SQL 类型。我们已经看到如下声明:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
带有SqlParameter
的第一行声明一个 IN 参数。 IN 参数既可用于存储过程调用,也可用于使用SqlQuery
及其下一节介绍的子类的查询。
带有SqlOutParameter
的第二行声明一个out
参数,该参数将在存储过程调用中使用。还有一个SqlInOutParameter
表示InOut
参数,这些参数为过程提供IN
值并返回一个值。
Note
仅声明为SqlParameter
和SqlInOutParameter
的参数将用于提供 Importing 值。这与StoredProcedure
类不同,后者出于向后兼容性的原因,允许为声明为SqlOutParameter
的参数提供 Importing 值。
对于 IN 参数,除了名称和 SQL 类型之外,还可以为数字数据指定比例,或者为自定义数据库类型指定类型名称。对于out
参数,您可以提供RowMapper
来处理从REF
游标返回的行的 Map。另一个选择是指定一个SqlReturnType
,它提供了一个机会来定义返回值的自定义处理。
19.5.8 使用 SimpleJdbcCall 调用存储的函数
调用存储函数的方式几乎与调用存储过程的方式相同,只是提供的是函数名而不是过程名。您将withFunctionName
方法用作配置的一部分,以表明我们要调用函数,并生成了函数调用的相应字符串。专门的执行调用executeFunction,
用于执行函数,它以指定类型的对象的形式返回函数的返回值,这意味着您不必从结果图中检索返回值。对于只有一个out
参数的存储过程,也可以使用名为executeObject
的类似便利方法。以下示例基于名为get_actor_name
的存储函数,该函数返回演员的全名。这是此功能的 MySQL 来源:
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
}
所使用的 execute 方法返回一个String
,其中包含该函数调用的返回值。
19.5.9 从 SimpleJdbcCall 返回 ResultSet/REF 游标
调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则需要显式注册的特定类型的out
参数。两种方法都需要进行额外的处理才能遍历结果集并处理返回的行。对于SimpleJdbcCall
,您可以使用returningResultSet
方法,并声明RowMapper
实现用于特定参数。在结果处理期间返回结果集的情况下,没有定义名称,因此返回的结果必须与声明RowMapper
实现的 Sequences 匹配。指定的名称仍用于将处理后的结果列表存储在由 execute 语句返回的结果图中。
下一个示例使用一个不带 IN 参数的存储过程,并从 t_actor 表返回所有行。这是此过程的 MySQL 源:
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
要调用此过程,请声明RowMapper
。因为要 Map 的类遵循 JavaBean 规则,所以可以使用BeanPropertyRowMapper
,该BeanPropertyRowMapper
是通过在newInstance
方法中传入要 Map 的必需类而创建的。
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,因为此调用不带任何参数。然后从结果图中检索 Actor 列表,并将其返回给调用方。
19.6 将 JDBC 操作建模为 Java 对象
org.springframework.jdbc.object
软件包包含允许您以更加面向对象的方式访问数据库的类。例如,您可以执行查询并以包含业务对象的列表的形式返回结果,其中关系列数据 Map 到业务对象的属性。您还可以执行存储过程并运行 update,delete 和 insert 语句。
Note
许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类(但StoredProcedure类除外)通常可以被直接JdbcTemplate
调用替换。通常,编写直接在JdbcTemplate
上直接调用方法的 DAO 方法(与将查询封装为完整的类相对)更简单。
但是,如果您通过使用 RDBMS 操作类获得可衡量的价值,请 continue 使用这些类。
19.6.1 SqlQuery
SqlQuery
是可重用的线程安全类,它封装了 SQL 查询。子类必须实现newRowMapper(..)
方法来提供RowMapper
实例,该实例可以为通过在查询执行期间创建的ResultSet
进行迭代而获得的每一行创建一个对象。 SqlQuery
类很少直接使用,因为MappingSqlQuery
子类为将行 Map 到 Java 类提供了更为方便的实现。扩展SqlQuery
的其他实现是MappingSqlQueryWithParameters
和UpdatableSqlQuery
。
19.6.2 MappingSqlQuery
MappingSqlQuery
是可重用的查询,其中具体子类必须实现抽象mapRow(..)
方法,以将提供的ResultSet
的每一行转换为指定类型的对象。下面的示例显示一个自定义查询,该查询将t_actor
关系中的数据 Map 到Actor
类的实例。
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
public ActorMappingQuery(DataSource ds) {
super(ds, "select id, first_name, last_name from t_actor where id = ?");
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}
@Override
protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
Actor actor = new Actor();
actor.setId(rs.getLong("id"));
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
该类扩展了用Actor
类型参数化的MappingSqlQuery
。此 Client 查询的构造函数将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 的 Client。由于我们只希望返回一个对象,因此我们只需调用 id 为参数的便捷方法findObject
即可。相反,如果我们有一个返回对象列表并采用其他参数的查询,那么我们将使用其中一种执行方法,该方法采用以 varargs 形式传入的参数值数组。
public List<Actor> searchForActors(int age, String namePattern) {
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
19.6.3 SqlUpdate
SqlUpdate
类封装了 SQL 更新。像查询一样,更新对象是可重用的,并且像所有RdbmsOperation
类一样,更新可以具有参数并在 SQL 中定义。此类提供了许多update(..)
方法,类似于查询对象的execute(..)
方法。 SQLUpdate
类是具体的。例如,可以将其子类化以添加自定义更新方法,如下面的代码片段中简称为execute
。但是,不必继承SqlUpdate
类,因为可以通过设置 SQL 和声明参数来轻松对其进行参数化。
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);
}
}
19.6.4 StoredProcedure
StoredProcedure
类是 RDBMS 存储过程的对象抽象的超类。此类是abstract
,并且其各种execute(..)
方法具有protected
访问权限,除了通过提供更严格的键入的子类之外,其他都禁止使用。
继承的sql
属性将是 RDBMS 中存储过程的名称。
要为StoredProcedure
类定义参数,请使用SqlParameter
或其子类之一。您必须像下面的代码片段中那样在构造函数中指定参数名称和 SQL 类型。使用java.sql.Types
常量指定 SQL 类型。
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
带有SqlParameter
的第一行声明一个 IN 参数。 IN 参数既可用于存储过程调用,也可用于使用SqlQuery
及其下一节介绍的子类的查询。
第二行带有SqlOutParameter
的行声明了要在存储过程调用中使用的out
参数。还有一个SqlInOutParameter
表示I
nOut
参数,这些参数为过程提供in
值并且还返回一个值。
对于i
n
参数,除了名称和 SQL 类型外,还可以为数字数据指定小数位,或者为自定义数据库类型指定类型名。对于out
参数,您可以提供RowMapper
来处理从 REF 游标返回的行的 Map。另一个选择是指定SqlReturnType
,使您可以定义返回值的自定义处理。
这是一个简单的 DAO 的示例,该 DAO 使用StoredProcedure
调用任何 Oracle 数据库附带的函数sysdate()
。要使用存储过程功能,您必须创建一个扩展StoredProcedure
的类。在此示例中,StoredProcedure
类是一个内部类,但是如果您需要重用StoredProcedure
,则可以将其声明为顶级类。此示例没有 Importing 参数,但是使用SqlOutParameter
类将输出参数声明为日期类型。 execute()
方法执行该过程,并从结果Map
中提取返回的日期。结果Map
使用参数名称作为键,为每个声明的输出参数都有一个条目,在这种情况下,只有一个。
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>());
}
}
注意如何在RowMapper
实现实例中传递在TitlesAndGenresStoredProcedure
构造函数中使用的declareParameter(..)
方法的重载变体。这是重用现有功能的非常方便且强大的方法。下面提供了两种RowMapper
实现的代码。
对于提供的ResultSet
中的每一行,TitleMapper
类将ResultSet
Map 到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;
}
}
对于所提供的ResultSet
中的每一行,GenreMapper
类将ResultSet
Map 到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 中定义中具有一个或多个 Importing 参数的存储过程,可以编写一个强类型的execute(..)
方法,该方法将委派给超类的未类型的execute(Map parameters)
方法(具有protected
访问权限);例如:
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);
}
}
19.7 参数和数据值处理的常见问题
Spring Framework JDBC 提供的不同方法存在参数和数据值的常见问题。
19.7.1 提供参数的 SQL 类型信息
通常,Spring 根据传入的参数类型确定参数的 SQL 类型。可以在设置参数值时显式提供要使用的 SQL 类型。有时需要正确设置 NULL 值。
您可以通过几种方式提供 SQL 类型信息:
JdbcTemplate
的许多更新和查询方法都采用int
数组形式的附加参数。该数组用于使用java.sql.Types
类中的常量值指示相应参数的 SQL 类型。为每个参数提供一个条目。您可以使用
SqlParameterValue
类包装需要此附加信息的参数值。为每个值创建一个新实例,然后在构造函数中传入 SQL 类型和参数值。您还可以为数字值提供可选的比例参数。对于使用命名参数的方法,请使用
SqlParameterSource
类BeanPropertySqlParameterSource
或MapSqlParameterSource
。它们都具有用于为任何命名参数值注册 SQL 类型的方法。
19.7.2 处理 BLOB 和 CLOB 对象
您可以在数据库中存储图像,其他二进制数据和大块文本。这些大对象称为二进制数据的 BLOB(二进制大型对象),而字符数据称为 CLOB(字符大型对象)。在 Spring 中,您可以直接使用JdbcTemplate
来处理这些大对象,也可以使用 RDBMS Objects 和SimpleJdbc
类提供的更高抽象来处理这些大对象。所有这些方法都将LobHandler
接口的实现用于 LOB(大对象)数据的实际 Management。 LobHandler
通过getLobCreator
方法提供对LobCreator
类的访问,该类用于创建要插入的新 LOB 对象。
LobCreator/LobHandler
为 LOBImporting 和输出提供以下支持:
BLOB
byte[]
—getBlobAsBytes
和setBlobAsBytes
InputStream
—getBlobAsBinaryStream
和setBlobAsBinaryStream
CLOB
String
—getClobAsString
和setClobAsString
InputStream
—getClobAsAsciiStream
和setClobAsAsciiStream
Reader
—getClobAsCharacterStream
和setClobAsCharacterStream
下一个示例显示了如何创建和插入 BLOB。稍后,您将看到如何从数据库读回它。
本示例使用JdbcTemplate
和AbstractLobCreatingPreparedStatementCallback
的实现。它实现了一种方法setValues
。此方法提供LobCreator
,可用于设置 SQL 插入语句中的 LOB 列的值。
对于此示例,我们假设存在一个变量lobHandler
,该变量已设置为DefaultLobHandler
的实例。通常,您可以通过依赖注入来设置此值。
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) { (1)
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); (2)
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); (3)
}
}
);
blobIs.close();
clobReader.close();
- (1) Importing
lobHandler
,在此示例中为纯DefaultLobHandler
。 - (2) 使用方法
setClobAsCharacterStream
传入 CLOB 的内容。 - (3) 使用方法
setBlobAsBinaryStream
传入 BLOB 的内容。
Note
如果在从DefaultLobHandler.getLobCreator()
返回的LobCreator
上调用setBlobAsBinaryStream
,setClobAsAsciiStream
或setClobAsCharacterStream
方法,则可以选择为contentLength
参数指定负值。如果指定的内容长度为负,则DefaultLobHandler
将使用不带 length 参数的 set-stream 方法的 JDBC 4.0 变体;否则,它将把指定的长度传递给驱动程序。
请查阅所用 JDBC 驱动程序的文档,以在不提供内容长度的情况下验证对流式 LOB 的支持。
现在是时候从数据库中读取 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"); (1)
results.put("CLOB", clobText); byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); (2)
results.put("BLOB", blobBytes); return results; } });
- (1) 使用方法
getClobAsString
检索 CLOB 的内容。 - (2) 使用方法
getBlobAsBytes
检索 BLOB 的内容。
19.7.3 传入 IN 子句的值列表
SQL 标准允许根据包含变量值列表的表达式选择行。一个典型的例子是select * from T_ACTOR where id in (1, 2, 3)
。 JDBC 标准不直接为准备好的语句支持此变量列表。您不能声明可变数量的占位符。您需要准备好所需数量的占位符的多种变体,或者一旦知道需要多少个占位符,就需要动态生成 SQL 字符串。 NamedParameterJdbcTemplate
和JdbcTemplate
中提供的命名参数支持采用后一种方法。将值作为原始对象的java.util.List
传入。该列表将用于插入所需的占位符,并在语句执行期间传递值。
Note
传递许多值时要小心。 JDBC 标准不能保证in
表达式列表可以使用 100 个以上的值。各种数据库都超过了此数目,但是它们通常对允许多少个值有硬性限制。 Oracle 的上限为 1000.
除了值列表中的原始值之外,您还可以创建java.util.List
对象数组。该列表将支持为in
子句定义的多个表达式,例如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'\))
。当然,这需要您的数据库支持此语法。
19.7.4 处理存储过程调用的复杂类型
调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,Spring 提供了一个SqlReturnType
来处理它们(从存储过程调用中返回),并提供SqlTypeValue
并将它们作为参数传递给存储过程。
这是返回声明为类型ITEM_TYPE
的用户的 Oracle STRUCT
对象的值的示例。 SqlReturnType
接口有一个必须实现的名为getTypeValue
的方法。此接口用作SqlOutParameter
声明的一部分。
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
new SqlReturnType() {
public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String typeName) throws SQLException {
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
s(如以下示例所示)或ArrayDescriptor
s。
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 调用的 Importing 参数的 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;
}
};
19.8 嵌入式数据库支持
org.springframework.jdbc.datasource.embedded
软件包提供对嵌入式 Java 数据库引擎的支持。本地提供对HSQL,H2和Derby的支持。您还可以使用可扩展的 API 来插入新的嵌入式数据库类型和DataSource
实现。
19.8.1 为什么要使用嵌入式数据库?
嵌入式数据库由于其轻量级的特性,因此在项目的开发阶段非常有用。好处包括易于配置,启动时间短,可测试性以及在开发过程中快速演化 SQL 的能力。
19.8.2 使用 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>
前面的配置从 Classpath 的根中的schema.sql
和test-data.sql
资源中创建一个用 SQL 填充的嵌入式 HSQL 数据库。另外,作为最佳实践,将为嵌入式数据库分配一个唯一生成的名称。嵌入式数据库作为javax.sql.DataSource
类型的 bean 对于 Spring 容器可用,然后可以根据需要将其注入到数据访问对象中。
19.8.3 以编程方式创建嵌入式数据库
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()
有关所有受支持选项的更多详细信息,请向 Javadoc 查询EmbeddedDatabaseBuilder
。
如下例所示,EmbeddedDatabaseBuilder
也可用于使用 Java Config 创建嵌入式数据库。
@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();
}
}
19.8.4 选择嵌入式数据库类型
Using HSQL
Spring 支持 HSQL 1.8.0 及更高版本。如果未明确指定类型,则 HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将embedded-database
标记的type
属性设置为HSQL
。如果使用的是构建器 API,请使用EmbeddedDatabaseType.HSQL
调用setType(EmbeddedDatabaseType)
方法。
Using H2
Spring 也支持 H2 数据库。要启用 H2,请将embedded-database
标签的type
属性设置为H2
。如果使用的是构建器 API,请使用EmbeddedDatabaseType.H2
调用setType(EmbeddedDatabaseType)
方法。
Using Derby
Spring 还支持 Apache Derby 10.5 及更高版本。要启用 Derby,请将embedded-database
标签的type
属性设置为DERBY
。如果使用的是构建器 API,请使用EmbeddedDatabaseType.DERBY
调用setType(EmbeddedDatabaseType)
方法。
19.8.5 使用嵌入式数据库测试数据访问逻辑
嵌入式数据库提供了一种轻量级的方法来测试数据访问代码。以下是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在测试类之间重用时,使用这样的模板可以一次性完成。但是,如果要创建在测试套件中共享的嵌入式数据库,请考虑使用Spring TestContext 框架并将嵌入式数据库配置为 Spring ApplicationContext
中的 Bean,如第 19.8.2 节“使用 Spring XML 创建嵌入式数据库”和第 19.8.3 节“以编程方式创建嵌入式数据库”中所述。
public class DataAccessIntegrationTestTemplate {
private EmbeddedDatabase db;
@Before
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( /* ... */ );
}
@After
public void tearDown() {
db.shutdown();
}
}
19.8.6 为嵌入式数据库生成唯一名称
如果开发团队的测试套件无意中尝试重新创建同一数据库的其他实例,则开发团队经常会遇到错误。如果 XML 配置文件或@Configuration
类负责创建嵌入式数据库,然后在同一测试套件(即,同一 JVM 进程)内的多个测试场景中重用相应的配置,则这很容易发生-例如,对嵌入式数据库的集成测试,该嵌入式数据库的ApplicationContext
配置仅在哪些 bean 定义配置文件处于活动状态方面有所不同。
此类错误的根本原因是,如果没有另外指定,Spring 的EmbeddedDatabaseFactory
(由<jdbc:embedded-database>
XML 名称空间元素和EmbeddedDatabaseBuilder
用于 Java Config 内部使用)会将嵌入式数据库的名称设置为"testdb"
。对于<jdbc:embedded-database>
,通常为嵌入式数据库分配一个与 Bean 的id
相同的名称(即,通常类似于"dataSource"
)。因此,随后尝试创建嵌入式数据库将不会产生新的数据库。相反,相同的 JDBC 连接 URL 将被重用,并且尝试创建新的嵌入式数据库实际上将指向通过相同配置创建的现有嵌入式数据库。
为了解决这个常见问题,Spring Framework 4.2 提供了对生成嵌入式数据库的“唯一”名称的支持。要启用生成名称的使用,请使用以下选项之一。
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
EmbeddedDatabaseBuilder.generateUniqueName()
<jdbc:embedded-database generate-name="true" … >
19.8.7 扩展嵌入式数据库支持
Spring JDBC 嵌入式数据库支持可以通过两种方式扩展:
实现
EmbeddedDatabaseConfigurer
以支持新的嵌入式数据库类型。实施
DataSourceFactory
以支持新的DataSource
实施,例如用于 Management 嵌入式数据库连接的连接池。
鼓励您向jira.spring.io的 Spring 社区回馈扩展。
19.9 初始化数据源
org.springframework.jdbc.datasource.init
软件包为初始化现有的DataSource
提供支持。嵌入式数据库支持提供了一个为应用程序创建和初始化DataSource
的选项,但是有时您需要初始化在某处的服务器上运行的实例。
19.9.1 使用 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 或文件名的词法 Sequences 执行。
数据库初始化程序的默认行为是无条件执行所提供的脚本。例如,如果您正在针对已经具有测试数据的数据库执行脚本,那么这将不一定是您想要的。通过遵循首先创建表然后插入数据的通用模式(如上所示),可以减少意外删除数据的可能性。如果表已经存在,则第一步将失败。
但是,为了更好地控制现有数据的创建和删除,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
语句,因此可能会失败。因此,失败的 SQL DROP
语句将被忽略,但其他失败将导致异常。如果您的 SQL 方言不支持DROP … IF EXISTS
(或类似值),但是您希望在重新创建它之前无条件地删除所有测试数据,这将很有用。在这种情况下,第一个脚本通常是一组DROP
语句,然后是一组CREATE
语句。
ignore-failures
选项可以设置为NONE
(默认设置),DROPS
(忽略失败的放置)或ALL
(忽略所有失败)。
如果脚本中根本没有;
字符,则每个语句应用;
或换行符分隔。您可以全局控制或逐个脚本控制,例如:
<jdbc:initialize-database data-source="dataSource" separator="@@">
<jdbc:script location="classpath:com/foo/sql/db-schema.sql" separator=";"/>
<jdbc:script location="classpath:com/foo/sql/db-test-data-1.sql"/>
<jdbc:script location="classpath:com/foo/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
在此示例中,两个test-data
脚本使用@@
作为语句分隔符,只有db-schema.sql
使用;
。此配置指定默认分隔符为@@
并覆盖db-schema
脚本的默认分隔符。
如果您需要比从 XML 名称空间获得更多控制权,则可以直接使用DataSourceInitializer
并将其定义为应用程序中的组件。
初始化依赖于数据库的其他组件
大量的应用程序可以使用数据库初始化程序,而不会带来更多的复杂性:那些在 Spring 上下文启动之后才使用数据库的应用程序。如果您的应用程序不是其中之一,那么您可能需要阅读本节的其余部分。
数据库初始化程序取决于DataSource
实例,并执行其初始化回调中提供的脚本(类似于 XML Bean 定义中的init-method
,组件中的@PostConstruct
方法或实现InitializingBean
的组件中的afterPropertiesSet()
方法)。如果其他 bean 依赖于相同的数据源,并且还在初始化回调中使用该数据源,则可能存在问题,因为尚未初始化数据。一个常见的示例是一个高速缓存,它会在应用程序启动时急于初始化并从数据库加载数据。
要解决此问题,您有两种选择:将高速缓存初始化策略更改为下一个阶段,或者确保首先初始化数据库初始化程序。
如果应用程序在您的控制之下,则第一个选项可能很容易,否则就不容易。有关如何实现此目的的一些建议包括:
使缓存在首次使用时延迟初始化,从而缩短了应用程序的启动时间。
让您的缓存或单独的组件初始化缓存实现
Lifecycle
或SmartLifecycle
。当应用程序上下文启动时,如果设置了autoStartup
标志,则可以自动启动SmartLifecycle
,并且可以通过在封闭上下文中调用ConfigurableApplicationContext.start()
来手动启动Lifecycle
。使用 Spring
ApplicationEvent
或类似的自定义观察器机制来触发缓存初始化。ContextRefreshedEvent
随时可供使用(在初始化所有 bean 之后)由上下文发布,因此通常是一个有用的钩子(默认情况下SmartLifecycle
的工作方式)。
第二种选择也很容易。关于如何实现这一点的一些建议包括:
依靠 Spring
BeanFactory
的默认行为,即按注册 Sequences 初始化 bean。通过采用在 XML 配置中对应用程序模块进行排序的一组<import/>
元素的常规做法,并确保首先列出数据库和数据库初始化,您可以轻松地进行安排。分开
DataSource
和使用它的业务组件,并通过将它们放在单独的ApplicationContext
实例中来控制其启动 Sequences(例如,父上下文包含DataSource
,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更广泛地应用。