3.使用 JDBC 进行数据访问
下表中概述的操作序列可能最好地显示了 Spring Framework JDBC 抽象提供的值。该表显示了 Spring 负责哪些操作,哪些操作是您的责任。
表 4. Spring JDBC-谁做什么?
Action | Spring | You |
---|---|---|
定义连接参数。 | X | |
打开连接。 | X | |
指定 SQL 语句。 | X | |
声明参数并提供参数值 | X | |
准备并执行该语句。 | X | |
设置循环以遍历结果(如果有)。 | X | |
进行每次迭代的工作。 | X | |
处理任何异常。 | X | |
Handle transactions. | X | |
关闭连接,语句和结果集。 | X |
3.1. 选择一种用于 JDBC 数据库访问的方法
您可以选择几种方法来构成 JDBC 数据库访问的基础。除了JdbcTemplate
的三种风格之外,新的SimpleJdbcInsert
和SimpleJdbcCall
方法还优化了数据库元数据,并且 RDBMS Object 样式采用了一种更加面向对象的方法,类似于 JDO Query 设计。一旦开始使用这些方法之一,您仍然可以混合搭配以包含来自其他方法的功能。所有方法都需要兼容 JDBC 2.0 的驱动程序,某些高级功能需要 JDBC 3.0 驱动程序。
-
JdbcTemplate
是经典且最受欢迎的 Spring JDBC 方法。这种“最低级别”的方法以及所有其他方法都在幕后使用了 JdbcTemplate。 -
NamedParameterJdbcTemplate
包装JdbcTemplate
以提供命名参数,而不是传统的 JDBC?
占位符。当您有多个 SQL 语句参数时,此方法可提供更好的文档编制和易用性。 -
SimpleJdbcInsert
和SimpleJdbcCall
优化数据库元数据以限制必要的配置量。这种方法简化了编码,因此您只需要提供表或过程的名称,并提供与列名称匹配的参数映射即可。仅当数据库提供足够的元数据时,此方法才有效。如果数据库不提供此元数据,则必须提供参数的显式配置。 -
RDBMS 对象(包括
MappingSqlQuery
,SqlUpdate
和StoredProcedure
)要求您在初始化数据访问层期间创建可重用且线程安全的对象。此方法以 JDO Query 为模型,其中您定义查询字符串,声明参数并编译查询。完成后,可以使用各种参数值多次调用 execute 方法。
3.2. 包层次结构
Spring 框架的 JDBC 抽象框架由四个不同的软件包组成:
core
:org.springframework.jdbc.core
软件包包含JdbcTemplate
类及其各种回调接口,以及各种相关类。名为org.springframework.jdbc.core.simple
的子包包含SimpleJdbcInsert
和SimpleJdbcCall
类。另一个名为org.springframework.jdbc.core.namedparam
的子程序包包含NamedParameterJdbcTemplate
类和相关的支持类。
datasource
:org.springframework.jdbc.datasource
软件包包含一个 Util 类,用于方便的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
转换功能和一些 Util 类。 JDBC 处理期间引发的异常将转换为org.springframework.dao
包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有翻译的异常均未选中,这使您可以选择捕获可从中恢复的异常,同时将其他异常传播到调用方。
3.3. 使用 JDBC 核心类控制基本 JDBC 处理和错误处理
3.3.1. 使用 JdbcTemplate
-
运行 SQL 查询
-
更新语句和存储过程调用
-
对
ResultSet
个实例执行迭代并提取返回的参数值。 -
捕获 JDBC 异常,并将其转换为
org.springframework.dao
包中定义的通用,信息量更大的异常层次结构。
在代码中使用JdbcTemplate
时,只需实现回调接口,即可为它们明确定义 Contract。给定JdbcTemplate
类提供的Connection
,PreparedStatementCreator
回调接口将创建一条准备好的语句,提供 SQL 和任何必要的参数。 CallableStatementCreator
接口(创建可调用语句)也是如此。 RowCallbackHandler
接口从ResultSet
的每一行提取值。
您可以通过直接实例化DataSource
引用在 DAO 实现中使用JdbcTemplate
,也可以在 Spring IoC 容器中对其进行配置,并将其作为 Bean 引用提供给 DAO。
Note
DataSource
应该始终配置为 Spring IoC 容器中的 bean。在第一种情况下,将 Bean 直接提供给服务。在第二种情况下,将其提供给准备好的模板。
此类发出的所有 SQL 都以DEBUG
级别记录在与模板实例的标准类名相对应的类别下(通常为JdbcTemplate
,但是如果使用JdbcTemplate
类的自定义子类,则可能有所不同)。
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 更新(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 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
的单个实例,然后将该共享引用安全地注入到多个 DAO(或存储库)中。 JdbcTemplate
是有状态的,因为它维护对DataSource
的引用,但是此状态不是会话状态。
使用JdbcTemplate
类(和关联的NamedParameterJdbcTemplate类)的常见做法是在 Spring 配置文件中配置DataSource
,然后将共享的DataSource
bean 依赖注入到 DAO 类中。在DataSource
的设置器中创建JdbcTemplate
。
这将导致类似于以下内容的 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 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>
显式配置的替代方法是使用组件扫描和注解支持进行依赖项注入。在这种情况下,可以用@Repository注解
该类(这使其成为组件扫描的候选对象),并用@Autowired注解
DataSource
setter 方法。以下示例显示了如何执行此操作:
@Repository (1) public class JdbcCorporateEventDao implements CorporateEventDao { private JdbcTemplate jdbcTemplate; @Autowired (2) public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); (3) } // JDBC-backed implementations of the methods on the CorporateEventDao follow... }
- (1) 用
@Repository
注解类。 - (2) 用
@Autowired注解
DataSource
setter 方法。 - (3) 用
DataSource
创建一个新的JdbcTemplate
。
以下示例显示了相应的 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
实例是线程安全的。如果您的应用程序访问多个数据库,则可能需要多个JdbcTemplate
实例,这需要多个DataSources
实例,然后需要多个不同配置的JdbcTemplate
实例。
3.3.2. 使用 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate
类增加了对使用命名参数编程 JDBC 语句的支持,这与仅使用经典占位符('?'
)参数进行编程的 JDBC 相反。 NamedParameterJdbcTemplate
类包装JdbcTemplate
并委托包装的JdbcTemplate
完成其许多工作。本节仅描述NamedParameterJdbcTemplate
类的与JdbcTemplate
本身不同的区域,即使用命名参数对 JDBC 语句进行编程。以下示例显示了如何使用NamedParameterJdbcTemplate
:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from T_ACTOR where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
请注意,在分配给sql
变量的值和插入namedParameters
变量(类型MapSqlParameterSource
)的相应值中使用了命名参数符号。
或者,您可以使用基于映射
的样式将命名参数及其对应的值传递给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 的属性作为命名参数值的源。以下示例显示了典型的 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
。
3.3.3. 使用 SQLExceptionTranslator
SQLExceptionTranslator
是要由可以在SQLExceptions
和 Spring 自己的org.springframework.dao.DataAccessException
之间进行转换的类实现的接口,而在数据访问策略方面则不可知。为了提高精度,实现可以是通用的(例如,使用 SQLState 代码用于 JDBC)或专有的(例如,使用 Oracle 错误代码)。
SQLErrorCodeSQLExceptionTranslator
是SQLExceptionTranslator
的实现,默认情况下使用。此实现使用特定的供应商代码。它比SQLState
实现更为精确。错误代码转换基于 JavaBean 类型类SQLErrorCodes
中保存的代码。此类由SQLErrorCodesFactory
创建和填充,SQLErrorCodesFactory
(顾名思义)是基于名为sql-error-codes.xml
的配置文件的内容创建SQLErrorCodes
的工厂。该文件使用供应商代码填充,并且基于DatabaseMetaData
中的DatabaseProductName
填充。使用您正在使用的实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator
按以下 Sequences 应用匹配规则:
-
子类实现的任何自定义转换。通常,将使用提供的具体
SQLErrorCodeSQLExceptionTranslator
,因此该规则不适用。仅当您确实提供了子类实现时,它才适用。 -
作为
SQLErrorCodes
类的customSqlExceptionTranslator
属性提供的SQLExceptionTranslator
接口的任何自定义实现。 -
搜索
CustomSQLErrorCodesTranslation
类的实例列表(为SQLErrorCodes
类的customTranslations
属性提供),以查找匹配项。 -
错误代码匹配被应用。
-
使用后备翻译器。
SQLExceptionSubclassTranslator
是默认的后备翻译器。如果此翻译不可用,则下一个后备翻译器是SQLStateSQLExceptionTranslator
。
Note
默认情况下,使用SQLErrorCodesFactory
来定义Error
代码和自定义异常翻译。在 Classpath 的名为sql-error-codes.xml
的文件中查找它们,并根据使用中数据库的数据库元数据中的数据库名称找到匹配的SQLErrorCodes
实例。
您可以扩展SQLErrorCodeSQLExceptionTranslator
,如以下示例所示:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) {
if (sqlex.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlex);
}
return null;
}
}
在前面的示例中,特定的错误代码(-12345
)被转换,而其他错误则由默认转换器实现转换。若要使用此自定义转换器,必须通过setExceptionTranslator
方法将其传递给JdbcTemplate
,并且必须将JdbcTemplate
用于需要该转换器的所有数据访问处理。
以下示例显示了如何使用此自定义转换器:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
自定义转换器会传递一个数据源,以便在sql-error-codes.xml
中查找错误代码。
3.3.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))"); } }
3.3.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
,其中每一列都包含一个条目,并使用列名作为键。如果在前面的示例中添加一种方法来检索所有行的列表,则可能如下所示:
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}]
3.3.6. 更新数据库
下面的示例更新某个主键的列:
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAnUpdate { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void setName(int id, String name) { this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id); } }
在前面的示例中,SQL 语句具有用于行参数的占位符。您可以将参数值作为 varargs 或作为对象数组传递。因此,您应该在原始包装器类中显式包装原始器,或者应该使用自动装箱。
3.3.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
3.4. 控制数据库连接
3.4.1. 使用数据源
Spring 通过DataSource
获得与数据库的连接。 DataSource
是 JDBC 规范的一部分,是通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息。这是设置数据源的管理员的责任。您很可能在开发和测试代码时同时担当这两个角色,但是不必一定要知道如何配置生产数据源。
使用 Spring 的 JDBC 层时,您可以从 JNDI 获取数据源,也可以使用第三方提供的连接池实现来配置自己的数据源。
流行的实现是 Apache Jakarta Commons DBCP 和 C3P0。 Spring 发行版中的实现仅用于测试目的,不提供池化。
本节使用 Spring 的DriverManagerDataSource
实现,稍后将介绍其他一些实现。
Note
您只能将DriverManagerDataSource
类用于测试目的,因为它不提供缓冲池,并且在发出多个连接请求时性能不佳。
要配置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 配置:
<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"/>
3.4.2. 使用 DataSourceUtils
DataSourceUtils
类是一种方便且功能强大的帮助器类,它提供static
个方法以从 JNDI 获取连接并在必要时关闭连接。它支持例如DataSourceTransactionManager
的线程绑定连接。
3.4.3. 实施 SmartDataSource
SmartDataSource
接口应该由可以提供与关系数据库的连接的类来实现。它扩展了DataSource
接口,以使用它的类查询给定操作后是否应关闭连接。当您知道需要重用连接时,这种用法很有效。
3.4.4. 扩展 AbstractDataSource
AbstractDataSource
是 Spring 的DataSource
实现的abstract
基类。它实现了所有DataSource
实现通用的代码。如果编写自己的DataSource
实现,则应扩展AbstractDataSource
类。
3.4.5. 使用 SingleConnectionDataSource
SingleConnectionDataSource
类是SmartDataSource
接口的实现,该接口包装单个Connection
,每次使用后都不会关闭。这不是多线程功能。
如果假定共享连接(例如使用持久性工具)时,任何 Client 端代码都调用close
,则应将suppressClose
属性设置为true
。此设置将返回用于封装物理连接的关闭抑制代理。请注意,您不能再将此对象转换为本地 Oracle Connection
或类似的对象。
SingleConnectionDataSource
主要是测试版。例如,它结合简单的 JNDI 环境,可以在应用服务器外部轻松测试代码。与DriverManagerDataSource
相比,它始终重用同一连接,避免了过多的物理连接创建。
3.4.6. 使用 DriverManagerDataSource
DriverManagerDataSource
类是标准DataSource
接口的实现,该接口通过 bean 属性配置纯 JDBC 驱动程序,并每次返回一个新的Connection
。
此实现对于 Java EE 容器外部的测试和独立环境很有用,可以作为 Spring IoC 容器中的DataSource
bean 或与简单的 JNDI 环境结合使用。池假设Connection.close()
调用将关闭连接,因此任何DataSource
感知的持久性代码都应起作用。但是,即使在测试环境中,使用 JavaBean 风格的连接池(例如commons-dbcp
)也是如此容易,以至总是比DriverManagerDataSource
更好地使用这样的连接池。
3.4.7. 使用 TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy
是目标DataSource
的代理。代理包装该目标DataSource
以增加对 SpringManagement 的事务的认识。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource
。
Note
除非需要调用已经存在的代码并通过标准的 JDBC DataSource
接口实现,否则很少需要使用此类。在这种情况下,您仍然可以使该代码可用,同时使该代码参与 Spring 托管的事务。通常最好使用更高级别的资源 Management 抽象来编写自己的新代码,例如JdbcTemplate
或DataSourceUtils
。
3.4.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 不支持自定义隔离级别。
3.5. JDBC 批处理操作
如果将多个调用批处理到同一条准备好的语句,则大多数 JDBC 驱动程序都会提高性能。通过将更新分组,可以限制到数据库的往返次数。
3.5.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。
3.5.2. 具有对象列表的批处理操作
JdbcTemplate
和NamedParameterJdbcTemplate
都提供了另一种提供批处理更新的方式。无需实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值,并使用内部准备好的语句设置器。 API 会有所不同,具体取决于您是否使用命名参数。对于命名参数,您提供一个SqlParameterSource
数组,每个批次成员一个条目。您可以使用SqlParameterSourceUtils.createBatch
便捷方法创建此数组,传入一个 Bean 样式对象(带有与参数相对应的 getter 方法),String
-keyed 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 语句中的每个占位符必须具有一个条目,并且它们的 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
。
Note
在这种情况下,通过在基础PreparedStatement
上自动设置值,需要从给定的 Java 类型派生每个值的对应 JDBC 类型。尽管这通常效果很好,但存在潜在的问题(例如,包含 Map 的null
值)。在这种情况下,Spring 默认情况下会调用ParameterMetaData.getParameterType
,这对于 JDBC 驱动程序可能会很昂贵。如果遇到性能问题,您应该使用最新的驱动程序版本,并考虑将spring.jdbc.getParameterType.ignore
属性设置为true
(作为 JVM 系统属性或在 Classpath 根目录中的spring.properties
文件中),例如,如 Oracle 12c(SPR)所述-16139)。
或者,您可以考虑通过“ BatchPreparedStatementSetter”(如前所示),通过给基于“ List<Object[]>”的调用的显式类型数组,通过在自定义“ MapSqlParameterSource”上的“ registerSqlType”调用来显式指定相应的 JDBC 类型。 '实例,或者通过'BeanPropertySqlParameterSource'从 Java 声明的属性类型派生 SQL 类型,即使对于 null 值也是如此。
3.5.3. 具有多个批次的批次操作
前面的批处理更新示例处理的批处理太大,以至于您想将它们分成几个较小的批处理。您可以通过多次调用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,
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
。
3.6. 使用 SimpleJdbc 类简化 JDBC 操作
SimpleJdbcInsert
和SimpleJdbcCall
类通过利用可通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。这意味着您可以更少地进行前期配置,但是如果您愿意在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。
3.6.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.util.Map
作为其唯一参数。这里要注意的重要一点是,用于Map
的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的 insert 语句。
3.6.2. 通过使用 SimpleJdbcInsert 检索自动生成的密钥
下一个示例使用与前面的示例相同的插入内容,但是它没有传递id
,而是检索自动生成的密钥并将其设置在新的Actor
对象上。当创建SimpleJdbcInsert
时,除了指定表名之外,还使用usingGeneratedKeyColumns
方法指定生成的键列的名称。以下 Lists 显示了它的工作方式:
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
。
3.6.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
}
插入的执行与依靠元数据确定要使用的列的执行相同。
3.6.4. 使用 SqlParameterSource 提供参数值
使用Map
提供参数值可以很好地工作,但这并不是最方便使用的类。 Spring 提供了SqlParameterSource
接口的几种实现,您可以代替使用它们。第一个是BeanPropertySqlParameterSource
,如果您有一个包含值的 JavaBean 兼容类,这是一个非常方便的类。它使用相应的 getter 方法提取参数值。以下示例显示了如何使用BeanPropertySqlParameterSource
:
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
}
另一个选项是MapSqlParameterSource
,它类似于Map
,但提供了更方便的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 类。
3.6.5. 用 SimpleJdbcCall 调用存储过程
SimpleJdbcCall
类使用数据库中的元数据来查找in
和out
参数的名称,因此您不必显式声明它们。如果愿意,可以声明参数,也可以声明没有自动 Map 到 Java 类的参数(例如ARRAY
或STRUCT
)。第一个示例显示了一个简单过程,该过程仅从 MySQL 数据库返回VARCHAR
和DATE
格式的标量值。示例过程读取指定的 actor 条目,并以out
参数的形式返回first_name
,last_name
和birth_date
列。以下 Lists 显示了第一个示例:
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
参数返回从表读取的数据。
您可以采用类似于声明SimpleJdbcInsert
的方式声明SimpleJdbcCall
。您应该在数据访问层的初始化方法中实例化并配置该类。与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
参数名称的情况下发生冲突。
3.6.6. 明确声明要用于 SimpleJdbcCall 的参数
在本章的前面,我们描述了如何从元数据推导出参数,但是如果需要,可以显式声明它们。您可以通过使用declareParameters
方法创建和配置SimpleJdbcCall
来实现,该方法采用可变数量的SqlParameter
对象作为 Importing。
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 }
3.6.7. 如何定义 SqlParameters
要为SimpleJdbc
类以及 RDBMS 操作类(在将 JDBC 操作建模为 Java 对象中介绍)定义参数,可以使用SqlParameter
或其子类之一。为此,通常在构造函数中指定参数名称和 SQL 类型。通过使用java.sql.Types
常量指定 SQL 类型。在本章的前面,我们看到了类似于以下内容的声明:
new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR),
带有SqlParameter
的第一行声明一个 IN 参数。通过使用SqlQuery
及其子类(在Understanding SqlQuery中介绍),可以将 IN 参数用于存储过程调用和查询。
第二行(带有SqlOutParameter
)声明要在存储过程调用中使用的out
参数。还有SqlInOutParameter
表示InOut
参数(为过程提供 IN 值并返回值的参数)。
Note
仅声明为SqlParameter
和SqlInOutParameter
的参数用于提供 Importing 值。这与StoredProcedure
类不同,后者(出于向后兼容的原因)允许为声明为SqlOutParameter
的参数提供 Importing 值。
对于 IN 参数,除了名称和 SQL 类型之外,还可以为数字数据指定比例,或者为自定义数据库类型指定类型名称。对于out
参数,您可以提供RowMapper
来处理从REF
游标返回的行的 Map。另一个选择是指定一个SqlReturnType
,它提供了一个机会来定义返回值的自定义处理。
3.6.8. 通过使用 SimpleJdbcCall 调用存储的函数
可以使用与调用存储过程几乎相同的方式来调用存储函数,除了提供函数名而不是过程名。您将withFunctionName
方法用作配置的一部分,以指示您要对函数进行调用,并生成函数调用的相应字符串。专门的执行调用(executeFunction
)用于执行函数,它以指定类型的对象的形式返回函数返回值,这意味着您不必从结果 Map 中检索返回值。对于只有一个out
参数的存储过程,也可以使用类似的便捷方法(名为executeObject
)。以下示例(对于 MySQL)基于名为get_actor_name
的存储函数,该函数返回参与者的全名:
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
,其中包含该函数调用的返回值。
3.6.9. 从 SimpleJdbcCall 返回 ResultSet 或 REF 游标
调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则需要显式注册的特定类型的out
参数。两种方法都需要进行额外的处理才能遍历结果集并处理返回的行。使用SimpleJdbcCall
,您可以使用returningResultSet
方法并声明RowMapper
实现用于特定参数。如果在结果处理期间返回了结果集,则没有定义任何名称,因此返回的结果必须与声明RowMapper
实现的 Sequences 匹配。指定的名称仍用于将处理后的结果列表存储在从execute
语句返回的结果 Map 中。
下一个示例(对于 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
。因为您要 Map 到的类遵循 JavaBean 规则,所以可以使用通过在newInstance
方法中传入要 Map 的必需类而创建的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
,因为该调用没有任何参数。然后从结果图中检索参与者列表,并将其返回给调用者。
3.7. 将 JDBC 操作建模为 Java 对象
org.springframework.jdbc.object
软件包包含一些类,这些类使您可以以更加面向对象的方式访问数据库。例如,您可以执行查询并将结果作为包含业务对象的列表返回,该业务对象的关系列数据 Map 到业务对象的属性。您还可以运行存储过程并运行 update,delete 和 insert 语句。
Note
许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类(但StoredProcedure类除外)通常可以被直接JdbcTemplate
调用替换。通常,编写直接在JdbcTemplate
上调用方法的 DAO 方法(与将查询封装为完整的类相反)更容易。
但是,如果通过使用 RDBMS 操作类获得可测量的价值,则应 continue 使用这些类。
3.7.1. 了解 SqlQuery
SqlQuery
是可重用的,线程安全的类,它封装了 SQL 查询。子类必须实现newRowMapper(..)
方法来提供RowMapper
实例,该实例可以为通过在查询执行期间创建的ResultSet
进行迭代而获得的每一行创建一个对象。 SqlQuery
类很少直接使用,因为MappingSqlQuery
子类为将行 Map 到 Java 类提供了更为方便的实现。扩展SqlQuery
的其他实现是MappingSqlQueryWithParameters
和UpdatableSqlQuery
。
3.7.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
,因此它可以包含在执行期间要传递的任何参数的占位符。您必须使用SqlParameter
传递的declareParameter
方法声明每个参数。 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
便捷方法。相反,如果有一个查询返回一个对象列表并采用其他参数,则将使用execute
方法之一,该方法采用以 varargs 形式传入的参数值数组。以下示例显示了这种方法:
public List<Actor> searchForActors(int age, String namePattern) { List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern); return actors; }
3.7.3. 使用 SqlUpdate
SqlUpdate
类封装了 SQL 更新。与查询一样,更新对象是可重用的,并且与所有RdbmsOperation
类一样,更新可以具有参数并在 SQL 中定义。此类提供了许多update(..)
方法,类似于查询对象的execute(..)
方法。 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); } }
3.7.4. 使用 StoredProcedure
StoredProcedure
类是 RDBMS 存储过程的对象抽象的超类。此类是abstract
,并且其各种execute(..)
方法具有protected
访问权限,除了通过提供更严格的键入的子类之外,其他都禁止使用。
继承的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
及其子类(在Understanding SqlQuery中覆盖)的查询。
第二行(带有SqlOutParameter
)声明了要在存储过程调用中使用的out
参数。还有SqlInOutParameter
表示InOut
参数(为过程提供in
值并返回值的参数)。
对于in
参数,除了名称和 SQL 类型外,还可以为数字数据指定比例,或者为自定义数据库类型指定类型名称。对于out
参数,您可以提供RowMapper
来处理从REF
游标返回的行的 Map。另一个选择是指定一个SqlReturnType
,它允许您定义返回值的自定义处理。
下一个简单 DAO 的示例使用StoredProcedure
调用任何 Oracle 数据库随附的函数(sysdate()
)。要使用存储过程功能,您必须创建一个扩展StoredProcedure
的类。在此的示例StoredProcedure
类是一个内部类。但是,如果需要重用StoredProcedure
,则可以将其声明为顶级类。本示例没有 Importing 参数,但是使用SqlOutParameter
类将输出参数声明为日期类型。 execute()
方法运行该过程,并从结果Map
中提取返回的日期。通过使用参数名称作为键,结果Map
为每个声明的输出参数(在本例中为一个)都有一个条目。以下 Lists 显示了我们的自定义 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>()); } }
请注意,如何在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)
方法,如以下示例所示:
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); } }
3.8. 参数和数据值处理的常见问题
Spring Framework 的 JDBC 支持提供的不同方法中存在参数和数据值的常见问题。本节介绍如何解决它们。
3.8.1. 提供参数的 SQL 类型信息
通常,Spring 根据传入的参数类型确定参数的 SQL 类型。可以在设置参数值时显式提供要使用的 SQL 类型。有时需要正确设置NULL
值。
您可以通过几种方式提供 SQL 类型信息:
-
JdbcTemplate
的许多更新和查询方法都采用int
数组形式的附加参数。该数组用于通过使用java.sql.Types
类中的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。 -
您可以使用
SqlParameterValue
类包装需要此附加信息的参数值。为此,请为每个值创建一个新实例,然后在构造函数中传入 SQL 类型和参数值。您还可以为数字值提供可选的比例参数。 -
对于使用命名参数的方法,可以使用
SqlParameterSource
类BeanPropertySqlParameterSource
或MapSqlParameterSource
。它们都具有用于为任何命名参数值注册 SQL 类型的方法。
3.8.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
的实例。通常,您可以通过依赖注入来设置此值。
以下示例显示如何创建和插入 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) { (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) 传递
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 的内容。
3.8.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'\))
。当然,这要求您的数据库支持此语法。
3.8.4. 处理存储过程调用的复杂类型
调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,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", 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
实例或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
调用的 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; } };
3.9. 嵌入式数据库支持
org.springframework.jdbc.datasource.embedded
软件包提供对嵌入式 Java 数据库引擎的支持。本地提供对HSQL,H2和Derby的支持。您还可以使用可扩展的 API 来插入新的嵌入式数据库类型和DataSource
实现。
3.9.1. 为什么要使用嵌入式数据库?
嵌入式数据库由于其轻量级的特性,因此在项目的开发阶段可能会很有用。好处包括易于配置,启动时间短,可测试性以及在开发过程中快速演化 SQL 的能力。
3.9.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>
前面的配置创建了一个嵌入式 HSQL 数据库,该数据库使用来自 Classpath 根目录中schema.sql
和test-data.sql
资源的 SQL 进行填充。另外,作为最佳实践,将为嵌入式数据库分配一个唯一生成的名称。嵌入式数据库作为javax.sql.DataSource
类型的 bean 对于 Spring 容器可用,然后可以根据需要将其注入到数据访问对象中。
3.9.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()
您还可以使用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(); } }
3.9.4. 选择嵌入式数据库类型
本节介绍如何选择 Spring 支持的三个嵌入式数据库之一。
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)
方法。
3.9.5. 使用嵌入式数据库测试数据访问逻辑
嵌入式数据库提供了一种轻量级的方法来测试数据访问代码。下一个示例是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在测试类之间重用时,使用这种模板可以一次性使用。但是,如果要创建在测试套件中共享的嵌入式数据库,请考虑使用Spring TestContext 框架并将嵌入式数据库配置为 Spring ApplicationContext
中的 Bean,如使用 Spring XML 创建嵌入式数据库和以编程方式创建嵌入式数据库中所述。以下 Lists 显示了测试模板:
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(); } }
3.9.6. 为嵌入式数据库生成唯一名称
如果开发团队的测试套件无意中尝试重新创建同一数据库的其他实例,则开发团队经常会遇到错误。如果 XML 配置文件或@Configuration
类负责创建嵌入式数据库,然后在同一测试套件(即,同一 JVM 进程)中的多个测试场景中重复使用相应的配置,则这很容易发生。例如,集成测试针对ApplicationContext
配置仅在哪个 bean 定义配置文件处于活动状态方面有所不同的嵌入式数据库。
此类错误的根本原因是,如果没有另外指定,Spring 的EmbeddedDatabaseFactory
(由<jdbc:embedded-database>
XML 名称空间元素和EmbeddedDatabaseBuilder
用于 Java 配置内部使用)会将嵌入式数据库的名称设置为testdb
。对于<jdbc:embedded-database>
,通常为嵌入式数据库分配一个与 Bean 的id
相同的名称(通常为dataSource
之类的名称)。因此,随后创建嵌入式数据库的尝试不会产生新的数据库。取而代之的是,相同的 JDBC 连接 URL 被重用,并且尝试创建新的嵌入式数据库实际上指向的是从相同配置创建的现有嵌入式数据库。
为了解决这个常见问题,Spring Framework 4.2 提供了对生成嵌入式数据库的唯一名称的支持。要启用生成名称的使用,请使用以下选项之一。
-
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
-
EmbeddedDatabaseBuilder.generateUniqueName()
-
<jdbc:embedded-database generate-name="true" … >
3.9.7. 扩展嵌入式数据库支持
您可以通过两种方式扩展 Spring JDBC 嵌入式数据库的支持:
-
实现
EmbeddedDatabaseConfigurer
以支持新的嵌入式数据库类型。 -
实施
DataSourceFactory
以支持新的DataSource
实施,例如用于管理嵌入式数据库连接的连接池。
3.10. 初始化数据源
org.springframework.jdbc.datasource.init
软件包为初始化现有的DataSource
提供支持。嵌入式数据库支持提供了一个为应用程序创建和初始化DataSource
的选项。但是,有时您可能需要初始化在某处的服务器上运行的实例。
3.10.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}"> (1)
<jdbc:script location="..."/>
</jdbc:initialize-database>
- (1) 从名为
INITIALIZE_DATABASE
的系统属性获取enabled
的值。
控制现有数据会发生什么的第二种选择是更加容忍失败。为此,您可以控制初始化程序忽略脚本执行的 SQL 中某些错误的能力,如以下示例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的示例中,我们说我们期望有时脚本是针对空数据库运行的,并且脚本中有一些DROP
语句将因此失败。因此,失败的 SQL DROP
语句将被忽略,但其他失败将导致异常。如果您的 SQL 方言不支持DROP … IF EXISTS
(或类似值),但是您希望在重新创建它之前无条件地删除所有测试数据,这将很有用。在这种情况下,第一个脚本通常是一组DROP
语句,然后是一组CREATE
语句。
ignore-failures
选项可以设置为NONE
(默认设置),DROPS
(忽略失败的放置)或ALL
(忽略所有失败)。
如果脚本中根本没有;
字符,则每个语句应用;
或换行符分隔。您可以全局控制该脚本,也可以逐个脚本控制脚本,如以下示例所示:
<jdbc:initialize-database data-source="dataSource" separator="@@"> (1)
<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> (2)
<jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
- (1) 将分隔符脚本设置为
@@
。 - (2) 将
db-schema.sql
的分隔符设置为;
。
在此示例中,两个test-data
脚本使用@@
作为语句分隔符,只有db-schema.sql
使用;
。此配置指定默认分隔符为@@
并覆盖db-schema
脚本的默认分隔符。
如果您需要比从 XML 名称空间获得更多控制权,则可以直接使用DataSourceInitializer
并将其定义为应用程序中的组件。
初始化依赖于数据库的其他组件
大量的应用程序(那些在 Spring 上下文启动之后才使用数据库的应用程序)可以使用数据库初始化程序,而不会带来更多麻烦。如果您的应用程序不是其中之一,则可能需要阅读本节的其余部分。
数据库初始化程序取决于DataSource
实例,并运行其初始化回调中提供的脚本(类似于 XML Bean 定义中的init-method
,组件中的@PostConstruct
方法或实现InitializingBean
的组件中的afterPropertiesSet()
方法)。如果其他 bean 依赖于相同的数据源并在初始化回调中使用该数据源,则可能存在问题,因为尚未初始化数据。一个常见的示例是一个高速缓存,它会在应用程序启动时急于初始化并从数据库加载数据。
要解决此问题,您有两个选择:将高速缓存初始化策略更改为以后的阶段,或者确保首先初始化数据库初始化程序。
如果应用程序在您的控制之下,则更改缓存初始化策略可能很容易,否则就不那么容易。有关如何实现此目的的一些建议包括:
-
使缓存在首次使用时延迟初始化,从而缩短了应用程序的启动时间。
-
让您的缓存或单独的组件初始化缓存实现
Lifecycle
或SmartLifecycle
。当应用程序上下文启动时,您可以通过设置autoStartup
标志来自动启动SmartLifecycle
,并且可以通过在封闭上下文中调用ConfigurableApplicationContext.start()
来手动启动Lifecycle
。 -
使用 Spring
ApplicationEvent
或类似的自定义观察器机制来触发缓存初始化。ContextRefreshedEvent
随时可供使用(在初始化所有 bean 之后)由上下文发布,因此通常是一个有用的钩子(默认情况下SmartLifecycle
的工作方式)。
确保首先初始化数据库初始化程序也很容易。关于如何实现这一点的一些建议包括:
-
依靠 Spring
BeanFactory
的默认行为,即按注册 Sequences 初始化 bean。通过采用 XML 配置中一组<import/>
元素(对应用程序模块进行排序)的通用做法并确保首先列出数据库和数据库初始化,您可以轻松地进行安排。 -
将
DataSource
和使用它的业务组件分开,并通过将它们放在单独的ApplicationContext
实例中来控制其启动 Sequences(例如,父上下文包含DataSource
,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更广泛地应用。