https://docs.spring.io/spring-framework/reference/data-access/jdbc.html
Spring框架JDBC抽象提供的价值或许最好通过下表中概述的操作序列来展示。该表显示了哪些操作由Spring负责,哪些操作是你的职责。
Spring框架负责处理所有低级细节,这些细节可能会使JDBC成为一个繁琐的API。
选择JDBC数据库访问的方法
你可以在几种方法中选择一种作为JDBC数据库访问的基础。除了三种不同风格的JdbcTemplate
,还有SimpleJdbcInsert
和SimpleJdbcCall
方法优化了数据库元数据,而RDBMS对象风格则导致了更面向对象的方法。一旦你开始使用这些方法之一,仍然可以混合搭配,以包含来自不同方法的特性。
JdbcTemplate
是经典且最受欢迎的Spring JDBC方法。这种“最低级别”的方法和其他所有方法都在底层使用了JdbcTemplate
。NamedParameterJdbcTemplate
包装了JdbcTemplate
,提供了命名参数而不是传统的JDBC?
占位符。当SQL语句有多个参数时,这种方法提供了更好的文档和易用性。SimpleJdbcInsert
和SimpleJdbcCall
优化了数据库元数据,以限制所需的配置量。这种方法简化了编码,使你只需提供表或过程的名称以及与列名匹配的参数映射。这只有在数据库提供足够的元数据时才有效。如果数据库不提供这些元数据,你必须提供参数的显式配置。- RDBMS对象——包括
MappingSqlQuery
、SqlUpdate
和StoredProcedure
——要求你在初始化数据访问层期间创建可重用且线程安全的对象。这种方法允许你定义查询字符串,声明参数,并编译查询。一旦完成这些操作,就可以多次调用execute(…)
、update(…)
和findObject(…)
方法,使用不同的参数值。
层次结构
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
包包含了一个便于访问DataSource
的实用工具类,以及各种简单的DataSource
实现,你可以在Jakarta EE容器之外用于测试和运行未经修改的JDBC代码。一个名为org.springframework.jdbc.datasource.embedded
的子包提供了通过使用Java数据库引擎(如HSQL、H2和Derby)创建嵌入式数据库的支持。object
:org.springframework.jdbc.object
包包含了表示RDBMS查询、更新和存储过程的类,这些类是线程安全的、可重用的对象。这种风格导致了更面向对象的方法,尽管查询返回的对象自然与数据库断开连接。这种更高级别的JDBC抽象依赖于org.springframework.jdbc.core
包中的低级别抽象。support
:org.springframework.jdbc.support
包提供了SQLException
转换功能和一些实用工具类。在JDBC处理过程中抛出的异常被转换为org.springframework.dao
包中定义的异常。这意味着使用Spring JDBC抽象层的代码不需要实现JDBC或RDBMS特定的错误处理。所有转换后的异常都是未检查的,这为你提供了捕获可以从中恢复的异常的选项,同时让其他异常传播给调用者。
使用JDBC核心类控制基本的JDBC处理和错误处理
使用JdbcTemplate
JdbcTemplate
是JDBC核心包中的中心类。它处理资源的创建和释放,这有助于避免常见错误,例如忘记关闭连接。它执行核心JDBC工作流的基本任务(如语句创建和执行),让应用程序代码提供SQL并提取结果。JdbcTemplate
类:
- 执行SQL查询
- 更新语句和存储过程调用
- 对
ResultSet
实例进行迭代并提取返回的参数值。 - 捕获JDBC异常,并将它们转换为在
org.springframework.dao
包中定义的通用、更具信息性的异常层次结构。
当你使用 JdbcTemplate
编写代码时,你只需要实现回调接口,这些接口给出了一个明确的约定。给定 JdbcTemplate
类提供的 Connection
,PreparedStatementCreator
回调接口会创建一个预编译的语句,提供 SQL 和任何必要的参数。这对于 CallableStatementCreator
接口也是同样的道理,它用于创建可调用语句。RowCallbackHandler
接口从 ResultSet
的每一行中提取值。
可以通过使用DataSource
引用直接实例化,在DAO实现中使用JdbcTemplate
,或者可以在Spring IoC容器中配置它,并将其作为bean引用提供给DAO。
DataSource
应该始终在Spring IoC容器中配置为一个bean。在第一种情况下,bean直接提供给服务;在第二种情况下,它提供给准备好的模板。
该类发出的所有SQL都在与模板实例的完全限定类名相对应的类别下(通常是JdbcTemplate
,但如果使用JdbcTemplate
类的自定义子类,则可能不同)以DEBUG
级别进行记录。
以下部分提供了一些JdbcTemplate
使用示例。这些示例并不是JdbcTemplate
暴露的所有功能的详尽列表。
查询(SELECT)
以下查询获取关系中的行数:
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
以下查询使用绑定变量:
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
以下查询查找String
:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
以下查询查找并填充单个域对象:
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
},
1212L);
以下查询查找并填充域对象列表:
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
如果最后两个代码片段实际上存在于同一个应用程序中,那么消除这两个RowMapper
lambda表达式中的重复并将它们提取到一个单独的字段中是有意义的,然后根据需要由DAO方法引用。例如,最好将前面的代码片段编写如下:
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}
使用JdbcTemplate进行更新(INSERT、UPDATE和DELETE)
可以使用update(..)
方法执行插入、更新和删除操作。参数值通常作为可变参数提供,或者作为对象数组提供。
以下示例插入一个新条目:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
以下示例更新现有条目:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
以下示例删除一个条目:
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
其他JdbcTemplate操作
可以使用execute(..)
方法运行任意SQL。因此,该方法通常用于DDL语句。它有大量重载的变体,接受回调接口、绑定变量数组等。以下示例创建一个表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
以下示例调用一个存储过程:
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
JdbcTemplate 最佳实践
JdbcTemplate
类的实例在配置完成后是线程安全的。这一点很重要,因为这意味着可以配置一个JdbcTemplate
的单个实例,然后将这个共享引用安全地注入到多个DAO(或存储库)中。JdbcTemplate
是有状态的,因为它维护了一个对DataSource
的引用,但这种状态不是会话状态。
使用JdbcTemplate
类(以及相关的NamedParameterJdbcTemplate
类)的一个常见做法是在Spring配置文件中配置一个DataSource
,然后将这个共享的DataSource
bean依赖注入到DAO类中。JdbcTemplate
在DataSource
的setter中创建。这导致类似以下的DAO:
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
以下示例显示了相应的XML配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
显式配置的一种替代方法是使用组件扫描和注解支持进行依赖注入。在这种情况下,可以使用@Repository
(使其成为组件扫描的候选)注解类,并使用@Autowired
注解DataSource
setter方法。以下示例显示了如何执行此操作:
@Repository
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
以下示例显示了相应的XML配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Scans within the base package of the application for @Component classes to configure as beans -->
<context:component-scan base-package="org.springframework.docs.test" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
如果使用Spring的JdbcDaoSupport
类,并且你的各种基于JDBC的DAO类从它继承,那么你的子类将从JdbcDaoSupport
类继承一个setDataSource(..)
方法。你可以选择是否从这个类继承。JdbcDaoSupport
类仅作为方便提供。
无论你选择使用(或不使用)上述模板初始化风格中的哪一种,每次你想要运行 SQL 时都很少需要创建一个新的 JdbcTemplate
类实例。一旦配置完成,JdbcTemplate
实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个 JdbcTemplate
实例,这要求有多个 DataSource
,随后需要多个不同配置的 JdbcTemplate
实例。
使用NamedParameterJdbcTemplate
NamedParameterJdbcTemplate
类增加了使用命名参数编程JDBC语句的支持,而不是仅使用经典的占位符('?'
)参数来编程JDBC语句。NamedParameterJdbcTemplate
类包装了一个JdbcTemplate
,并将其大部分工作委托给被包装的JdbcTemplate
。本节仅描述NamedParameterJdbcTemplate
类与JdbcTemplate
本身不同的领域——即使用命名参数编程JDBC语句。以下示例显示了如何使用NamedParameterJdbcTemplate
:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
注意在sql
变量的值中使用了命名参数表示法,以及插入到namedParameters
变量(类型为MapSqlParameterSource
)中的相应值。
或者,可以使用基于Map
的风格将命名参数及其对应的值传递给NamedParameterJdbcTemplate
实例。NamedParameterJdbcOperations
暴露的其余方法和由NamedParameterJdbcTemplate
类实现的方法遵循类似的模式,此处未涵盖。
以下示例显示了基于Map
的风格的使用:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
与NamedParameterJdbcTemplate
相关的一个很好的特性是SqlParameterSource
接口(存在于相同的Java包中)。已经在之前的代码片段之一中看到了这个接口的一个实现示例(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
。
统一JDBC查询/更新操作:JdbcClient
自6.1版本起,NamedParameterJdbcTemplate
的命名参数语句和常规JdbcTemplate
的位置参数语句可以通过具有流畅交互模型的统一客户端API获得。
例如,使用位置参数:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?")
.param(firstName)
.query(Integer.class).single();
}
例如,使用命名参数:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName")
.param("firstName", firstName)
.query(Integer.class).single();
}
RowMapper
功能也同样可用,具有灵活的结果解析能力:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name")))
.list();
可以指定一个映射到的类,而不是自定义RowMapper
。例如,假设Actor
具有firstName
和lastName
属性作为记录类、自定义构造函数、bean属性或普通字段:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query(Actor.class)
.list();
需要单个对象结果时:
Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.single();
使用java.util.Optional
结果时:
Optional<Actor> actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.optional();
对于更新语句:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)")
.param("Leonor").param("Watling")
.update();
或者使用命名参数的更新语句:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.param("firstName", "Leonor").param("lastName", "Watling")
.update();
可以指定一个参数源对象,而不是单独的命名参数——例如,具有bean属性的记录类或普通字段持有者,它提供firstName
和lastName
属性,就像上面的Actor
类:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.paramSource(new Actor("Leonor", "Watling")
.update();
上面提到的参数和查询结果的自动Actor
类映射是通过隐式的SimplePropertySqlParameterSource
和SimplePropertyRowMapper
策略提供的,这些策略也可以直接使用。它们可以作为BeanPropertySqlParameterSource
和BeanPropertyRowMapper
/DataClassRowMapper
的通用替代品,也适用于JdbcTemplate
和NamedParameterJdbcTemplate
本身。
JdbcClient
是一个灵活但简化的JDBC查询/更新语句的外观。高级功能,如批量插入和存储过程调用,通常需要额外的定制:对于JdbcClient
中不可用的任何此类功能,请考虑使用Spring的SimpleJdbcInsert
和SimpleJdbcCall
类或直接使用JdbcTemplate
。
使用SQLExceptionTranslator
SQLExceptionTranslator
是一个接口,需要由能够在SQLExceptions
和Spring自己的org.springframework.dao.DataAccessException
之间进行转换的类实现,后者对数据访问策略不可知。实现可以是通用的(例如,使用JDBC的SQLState
代码)或专有的(例如,使用Oracle错误代码)以获得更高的精确度。这种异常转换机制被用于常见的JdbcTemplate
和JdbcTransactionManager
入口点之后,这些入口点不传播SQLException
,而是传播DataAccessException
。
自6.0版本起,默认的异常转换器是SQLExceptionSubclassTranslator
,它通过一些额外的检查来检测JDBC 4 SQLException
子类,并在必要时通过SQLStateSQLExceptionTranslator
回退到SQLState
自省。这对于常见的数据库访问通常是足够的,不需要特定于供应商的检测。为了向后兼容,请考虑使用下面描述的SQLErrorCodeSQLExceptionTranslator
,可能还需要自定义错误代码映射。
SQLErrorCodeSQLExceptionTranslator
是SQLExceptionTranslator
的实现,当类路径根目录下存在名为sql-error-codes.xml
的文件时,默认使用这个实现。这个实现使用特定的供应商代码。它比SQLState
或SQLException
子类转换更精确。错误代码转换基于一个名为SQLErrorCodes
的JavaBean类型类中保存的代码。这个类由SQLErrorCodesFactory
创建并填充,顾名思义,这个工厂用于根据名为sql-error-codes.xml
的配置文件内容创建SQLErrorCodes
。这个文件填充了供应商代码,并根据从DatabaseMetaData
获取的DatabaseProductName
进行配置。使用实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator
按以下顺序应用匹配规则:
- 任何由子类实现的自定义翻译。通常,使用提供的具体的
SQLErrorCodeSQLExceptionTranslator
,因此此规则不适用。只有当你实际提供了子类实现时,此规则才适用。 - 任何作为
SQLErrorCodes
类的customSqlExceptionTranslator
属性提供的SQLExceptionTranslator
接口的自定义实现。 - 在
CustomSQLErrorCodesTranslation
类的实例列表(为SQLErrorCodes
类的customTranslations
属性提供的)中搜索匹配项。 - 应用错误代码匹配。
- 使用后备翻译器。
SQLExceptionSubclassTranslator
是默认的后备翻译器。如果此翻译不可用,下一个后备翻译器是SQLStateSQLExceptionTranslator
。
默认情况下,SQLErrorCodesFactory
用于定义错误代码和自定义异常翻译。它们在类路径下的名为sql-error-codes.xml
的文件中查找,并根据正在使用的数据库的数据库元数据中的数据库名称定位匹配的SQLErrorCodes
实例。
可以扩展SQLErrorCodeSQLExceptionTranslator
,如下例所示:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
if (sqlEx.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlEx);
}
return null;
}
}
在上述示例中,特定的错误代码(-12345
)被翻译,而其他错误则留给默认的翻译器实现来处理。要使用这个自定义翻译器,必须通过setExceptionTranslator
方法将其传递给JdbcTemplate
,并且必须在所有需要此翻译器的数据访问处理中使用这个JdbcTemplate
。以下示例展示了如何使用这个自定义翻译器:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
定义翻译器传递了一个数据源,以便在sql-error-codes.xml
中查找错误代码。
执行语句(Running Statements)
运行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))");
}
}
运行查询(Running Queries)
某些查询方法返回单个值。要检索一行的计数或特定值,请使用queryForObject(..)。
后者将返回的JDBC Type
转换为作为参数传递的Java类。如果类型转换无效,则会抛出InvalidDataAccessApiUsageException
。以下示例包含两个查询方法,一个用于int
,另一个查询String
:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class RunAQuery {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int getCount() {
return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
}
public String getName() {
return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
}
}
除了返回单个结果的查询方法外,还有几个方法返回一个列表,其中每个条目对应查询返回的一行。最常用的方法是queryForList(..)
,它返回一个List
,其中每个元素是一个Map
,包含每列的一个条目,使用列名作为键。如果在前面的示例中添加一个方法来检索所有行的列表,可能如下所示:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getList() {
return this.jdbcTemplate.queryForList("select * from mytable");
}
返回的列表将类似于以下内容:
[{name=Bob, id=1}, {name=Mary, id=2}]
更新数据库
以下示例以主键更新了某个列:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAnUpdate {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void setName(int id, String name) {
this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
}
}
在上述示例中,SQL语句有行参数的占位符。可以将参数值作为可变参数传递,或者作为对象数组传递。因此,应该显式地将基本类型包装在基本类型的包装类中,或者使用自动装箱。
检索自动生成的键
update()
便捷方法支持检索由数据库生成的主键。此支持是JDBC 3.0标准的一部分。该方法的第一个参数是一个PreparedStatementCreator
,通过它来指定所需的插入语句。另一个参数是KeyHolder
,在更新成功返回时包含生成的键。没有标准的统一方法创建适当的PreparedStatement
(这就是为什么方法签名是这样的)。以下示例适用于Oracle,但可能不适用于其他平台:
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
ps.setString(1, name);
return ps;
}, keyHolder);
// keyHolder.getKey() now contains the generated key
控制数据库连接
使用DataSource
Spring 通过 DataSource
获取数据库连接。DataSource
是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架将连接池和事务管理问题从应用程序代码中隐藏起来。作为开发者,不需要了解如何连接到数据库的细节。这是设置数据源的管理员的责任。在开发和测试代码时,很可能同时扮演这两个角色,但不一定需要知道生产数据源是如何配置的。
当使用Spring的JDBC层时,可以从JNDI获取数据源,或者可以使用第三方提供的连接池实现来配置自己的数据源。传统的选择是Apache Commons DBCP和C3P0,它们提供了bean风格的DataSource
类;对于现代的JDBC连接池,可以考虑使用HikariCP,它提供了构建器风格的API。
应该仅将DriverManagerDataSource
和SimpleDriverDataSource
类(包含在Spring分发包中)用于测试目的!这些变体不提供连接池,并且在多个请求同时需要连接时性能较差。
以下部分使用了Spring的DriverManagerDataSource
实现。稍后将介绍其他几种DataSource
变体。
要配置DriverManagerDataSource
:
- 使用
DriverManagerDataSource
获取连接,就像通常获取JDBC连接一样。 - 指定JDBC驱动程序的完全限定类名,以便
DriverManager
可以加载驱动程序类。 - 提供一个URL,该URL在不同的JDBC驱动程序之间有所不同。
- 提供用户名和密码以连接到数据库。
以下示例展示了如何在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"/>
使用DataSourceUtils
DataSourceUtils
类是一个方便而强大的辅助类,提供了从JNDI获取连接并在必要时关闭连接的静态方法。它支持使用DataSourceTransactionManager
进行线程绑定的JDBC连接,但也支持JtaTransactionManager
和JpaTransactionManager
。
请注意,JdbcTemplate
的使用意味着通过DataSourceUtils
进行连接访问,它在每个JDBC操作背后隐含地参与了一个正在进行的事务。
实现SmartDataSource
SmartDataSource
接口应由能够提供关系数据库连接的类实现。它扩展了DataSource
接口,允许使用它的类查询在给定操作后是否应关闭连接。当你知道需要重用连接时,这种用法是高效的。
扩展AbstractDataSource
AbstractDataSource
是Spring的DataSource
实现的抽象基类。它实现了所有DataSource
实现共有的代码。如果编写自己的DataSource
实现,应该扩展AbstractDataSource
类。
使用SingleConnectionDataSource
SingleConnectionDataSource
类是SmartDataSource
接口的一个实现,它包装了一个在每次使用后都不关闭的单个连接。这不支持多线程。
如果有客户端代码基于连接池的假设调用close
(例如在使用持久性工具时),你应该将suppressClose
属性设置为true
。此设置返回一个包装物理连接的抑制关闭代理。请注意,你不能再将其转换为本地Oracle Connection
或类似的对象。
SingleConnectionDataSource
主要是一个测试类。它通常使得在应用服务器外部,结合简单的JNDI环境,轻松测试代码。与DriverManagerDataSource
相比,它始终重用相同的连接,避免了过多创建物理连接。
使用DriverManagerDataSource
DriverManagerDataSource
类是标准DataSource
接口的一个实现,它通过bean属性配置普通的JDBC驱动程序,并在每次调用时返回一个新的Connection
。
这个实现对于Jakarta EE容器之外的测试和独立环境很有用,可以作为Spring IoC容器中的DataSource
bean,或者与简单的JNDI环境结合使用。假设连接池的Connection.close()
调用会关闭连接,因此任何感知到DataSource
的持久性代码都应该正常工作。然而,即使在测试环境中,使用JavaBean风格的连接池(如commons-dbcp
)也非常简单,几乎总是比使用DriverManagerDataSource
更可取。
使用TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy
是目标DataSource
的代理。该代理包装目标DataSource
以增加对Spring管理的事务的认知。在这方面,它类似于Jakarta EE服务器提供的事务性JNDI DataSource
。
除非在必须调用已有代码并传递标准JDBC DataSource
接口实现的情况下,否则很少需要使用这个类。在这种情况下,仍然可以正常使用这段代码,同时让这段代码参与Spring管理的事务。通常更推荐编写自己的新代码时使用更高级别的资源管理抽象,如JdbcTemplate
或DataSourceUtils
。
使用DataSourceTransactionManager / JdbcTransactionManager
DataSourceTransactionManager
类是针对单个JDBC DataSource
的PlatformTransactionManager
实现。它将指定DataSource
的JDBC连接绑定到当前执行线程,可能允许每个DataSource
有一个线程绑定的连接。
应用程序代码需要通过DataSourceUtils.getConnection(DataSource)
检索JDBC连接,而不是使用Java EE的标准DataSource.getConnection
。它抛出非检查型的org.springframework.dao
异常,而不是检查型的SQLExceptions
。所有框架类(如JdbcTemplate
)都隐式使用这种策略。如果不与事务管理器一起使用,查找策略的行为与DataSource.getConnection
完全相同,因此可以在任何情况下使用。
DataSourceTransactionManager
类支持保存点(PROPAGATION_NESTED
)、自定义隔离级别,以及适当应用的JDBC语句查询超时时间。为了支持后者,应用程序代码必须使用JdbcTemplate
或者为每个创建的语句调用DataSourceUtils.applyTransactionTimeout(..)
方法。
在单资源情况下,可以使用DataSourceTransactionManager
代替JtaTransactionManager
,因为它不需要容器支持JTA事务协调器。只要你坚持使用所需的连接查找模式,切换这些事务管理器只是配置问题。请注意,JTA不支持保存点或自定义隔离级别,并且具有不同的超时机制,但在JDBC资源和JDBC提交/回滚管理方面表现出类似的行为。
对于JTA风格的实际资源连接的延迟检索,Spring为目标连接池提供了相应的DataSource
代理类:LazyConnectionDataSourceProxy
。这对于没有实际语句执行的潜在空事务特别有用(在这种情况下从不获取实际资源),也适用于路由DataSource
前面,意味着要考虑事务同步的只读标志和/或隔离级别(例如IsolationLevelDataSourceRouter
)。
LazyConnectionDataSourceProxy
还为只读事务期间使用的只读连接池提供特殊支持,避免了在从主连接池获取连接时,在每次事务开始和结束时切换JDBC连接的只读标志的开销(这取决于JDBC驱动程序,可能会很昂贵)。
从5.3版本开始,Spring提供了一个扩展的JdbcTransactionManager
变体,它在提交/回滚时增加了异常转换功能(与JdbcTemplate
保持一致)。DataSourceTransactionManager
只会抛出TransactionSystemException
(类似于JTA),而JdbcTransactionManager
会将数据库锁定失败等转换为相应的DataAccessException
子类。请注意,应用程序代码需要为此类异常做好准备,而不是仅预期TransactionSystemException
。在那种情况下,JdbcTransactionManager
是推荐的选择。
在异常行为方面,JdbcTransactionManager
大致相当于JpaTransactionManager
和R2dbcTransactionManager
,作为彼此的直接配套/替代品。另一方面,DataSourceTransactionManager
相当于JtaTransactionManager
,可以直接替代它。
JDBC批处理操作
大多数JDBC驱动程序在你对同一个预编译语句进行多次调用批处理时,会提供改进的性能。通过将更新分组为批次,你可以减少往返数据库的次数。
使用JdbcTemplate进行基本的批处理操作
你可以通过实现一个特殊接口BatchPreparedStatementSetter
的两个方法来完成JdbcTemplate
的批处理,并将该实现作为第二个参数传递给batchUpdate
方法调用。你可以使用getBatchSize
方法来提供当前批次的大小。你可以使用setValues
方法来设置预编译语句的参数值。这个方法会被调用你在getBatchSize
调用中指定的次数。下面的例子基于列表中的条目更新t_actor
表,整个列表被用作批处理:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
如果你处理一系列更新或从文件中读取,你可能有一个首选的批处理大小,但最后一批可能没有那么多条目。在这种情况下,你可以使用InterruptibleBatchPreparedStatementSetter
接口,它允许你在输入源耗尽时中断批处理。isBatchExhausted
方法让你可以发出批处理结束的信号。
使用对象列表进行批处理操作
JdbcTemplate
和NamedParameterJdbcTemplate
都提供了一种替代的批更新方式。你不需要实现特殊的批处理接口,而是在调用中将所有参数值作为一个列表提供。框架会遍历这些值并使用内部预编译语句设置器。API根据你是否使用命名参数而有所不同。对于命名参数,你需要提供一个SqlParameterSource
数组,每个批次的成员对应一个条目。你可以使用SqlParameterSourceUtils.createBatch
方便方法来创建这个数组,传入一个bean风格对象的数组(带有与参数对应的getter方法)、String键的Map
实例(包含相应参数作为值),或者两者的混合。
以下示例展示了使用命名参数进行批更新:
public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int[] batchUpdate(List<Actor> actors) {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
对于使用经典 ?
占位符的SQL语句,你需要传入一个包含对象数组的列表,该对象数组必须为SQL语句中的每个占位符提供一个条目,并且它们的顺序必须与SQL语句中定义的顺序相同。
以下示例与前一个示例相同,除了它使用传统的JDBC ?
占位符:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
List<Object[]> batch = new ArrayList<>();
for (Actor actor : actors) {
Object[] values = new Object[] {
actor.getFirstName(), actor.getLastName(), actor.getId()};
batch.add(values);
}
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
batch);
}
// ... additional methods
}
我们之前描述的所有批更新方法都返回一个int
数组,包含每个批处理条目影响的行数。这个计数是由JDBC驱动程序报告的。如果计数不可用,JDBC驱动程序将返回-2
。
在这种情况下,自动设置底层PreparedStatement
上的值时,需要从给定的Java类型派生出每个值的相应JDBC类型。虽然这通常运作良好,但可能会出现问题(例如,包含null
值的Map
)。Spring默认情况下会调用ParameterMetaData.getParameterType
,这在你的JDBC驱动程序中可能会很昂贵。你应该使用最新版本的驱动程序,并且如果你遇到性能问题(如在Oracle 12c、JBoss和PostgreSQL上报告的),考虑将spring.jdbc.getParameterType.ignore
属性设置为true
(作为JVM系统属性或通过SpringProperties
机制)。
或者,你可以考虑显式指定相应的JDBC类型,可以通过BatchPreparedStatementSetter
(如前所示),通过基于List<Object[]>
调用的显式类型数组,通过在自定义MapSqlParameterSource
实例上调用registerSqlType
,或者通过BeanPropertySqlParameterSource
,即使对于null
值也能从Java声明的属性类型派生出SQL类型。
使用多个批次进行批处理操作
前面的批更新示例处理的批次太大,以至于你希望将它们拆分为几个较小的批次。你可以使用前面提到的方法通过多次调用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,
(PreparedStatement ps, Actor actor) -> {
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
});
return updateCounts;
}
// ... additional methods
}
这个批量更新方法的调用返回一个int
数组的数组,该数组为每个批处理包含一个数组条目,每个更新操作包含受影响的行数。顶级数组的长度表示运行的批处理数量,而第二级数组的长度表示该批处理中的更新数量。每个批处理中的更新数量应该是为所有批处理提供的批量大小(除了最后一个批处理可能更少),这取决于提供的更新对象的总数。每个更新语句的更新计数是由JDBC驱动程序报告的。如果计数不可用,JDBC驱动程序将返回-2
的值。
使用SimpleJdbc类简化JDBC操作
SimpleJdbcInsert
和SimpleJdbcCall
类通过利用可以通过JDBC驱动程序获取的数据库元数据来提供简化的配置。这意味着你在前期需要配置的内容较少,尽管如果你更喜欢在代码中提供所有详细信息,也可以覆盖或关闭元数据处理。
使用SimpleJdbcInsert插入数据
我们从使用最少配置选项的SimpleJdbcInsert
类开始。你应在数据访问层的初始化方法中实例化SimpleJdbcInsert
。在这个例子中,初始化方法是setDataSource
方法。你不需要继承SimpleJdbcInsert
类。相反,你可以创建一个新实例,并使用withTableName
方法设置表名。这个类的配置方法遵循fluid
的接口风格,即每个方法都会返回SimpleJdbcInsert
的实例,从而允许你链式调用所有配置方法。下面的例子只使用了一个配置方法:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(3);
parameters.put("id", actor.getId());
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
insertActor.execute(parameters);
}
// ... additional methods
}
这里使用的execute
方法只接受一个普通的java.util.Map
作为参数。这里需要注意的重要一点是,用于Map
的键必须与数据库中定义的表列名相匹配。这是因为我们读取元数据来构建实际的插入语句。
使用SimpleJdbcInsert检索自动生成的键
下一个示例使用了与前面示例相同的插入操作,但是,它并没有传递id
,而是检索自动生成的键并将其设置在新的Actor
对象上。在创建SimpleJdbcInsert
时,除了指定表名之外,它还使用usingGeneratedKeyColumns
方法指定了生成的键列的名称。下面的列表展示了它是如何工作的:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("first_name", actor.getFirstName());
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
是你可以依赖的基类。如果你有多个自动生成的列或者生成的值不是数字类型,你可以使用executeAndReturnKeyHolder
方法返回的KeyHolder
。
为SimpleJdbcInsert指定列
你可以通过usingColumns
方法指定一个列名列表来限制插入操作的列,如下例所示:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
插入操作的执行与依赖元数据来确定使用哪些列的方式相同。
使用SqlParameterSource提供参数值
使用Map来提供参数值是可以工作的,但它并不是最方便的类来使用。Spring提供了SqlParameterSource
接口的几种实现,你可以使用它们来代替Map
。第一个实现是BeanPropertySqlParameterSource
,如果你有一个符合JavaBean规范的类,其中包含你的值,那么它是一个非常方便的类。它使用相应的getter方法来提取参数值。以下示例展示了如何使用BeanPropertySqlParameterSource
:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
另一个选项是MapSqlParameterSource
,它类似于一个Map
,但提供了更方便的addValue
方法,可以链式调用。以下示例展示了如何使用它:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", actor.getFirstName())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
正如你所看到的,配置是相同的。只需要改变执行代码来使用这些替代的输入类。
使用SimpleJdbcCall调用存储过程
SimpleJdbcCall
类使用数据库中的元数据来查找输入和输出参数的名称,这样你就不必显式地声明它们了。如果你愿意这样做,或者你的参数(比如 ARRAY
或 STRUCT
类型)没有自动映射到 Java 类,你可以声明参数。第一个示例展示了一个简单的存储过程,它仅从 MySQL 数据库中返回 VARCHAR
和 DATE
格式的标量值。这个示例过程读取一个指定的演员条目,并以输出参数的形式返回 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
参数返回从表中读取的数据。
你可以以类似于声明 SimpleJdbcInsert
的方式来声明 SimpleJdbcCall
。你应该在数据访问层的初始化方法中实例化和配置这个类。与 StoredProcedure
类相比,你不需要创建一个子类,也不需要声明那些可以在数据库元数据中查找到的参数。以下是一个 SimpleJdbcCall
配置的示例,它使用了前面提到的存储过程(除了数据源之外,唯一的配置选项是存储过程的名称):
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
this.procReadActor = new SimpleJdbcCall(dataSource)
.withProcedureName("read_actor");
}
public Actor readActor(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
Map out = procReadActor.execute(in);
Actor actor = new Actor();
actor.setId(id);
actor.setFirstName((String) out.get("out_first_name"));
actor.setLastName((String) out.get("out_last_name"));
actor.setBirthDate((Date) out.get("out_birth_date"));
return actor;
}
// ... additional methods
}
你写的执行调用的代码涉及创建一个包含输入参数的 SqlParameterSource
。你必须确保为输入值提供的名称与存储过程中声明的参数名称相匹配。这里不需要考虑大小写匹配,因为你会使用元数据来确定在存储过程中应如何引用数据库对象。在存储过程源代码中指定的名称并不一定就是它在数据库中存储的方式。一些数据库会将名称转换为全部大写,而另一些数据库则使用小写,或者使用指定的大小写。
execute
方法接收输入参数(IN 参数),并返回一个包含所有输出参数的 Map
,这些输出参数以在存储过程中指定的名称作为键。在这种情况下,输出参数的名称是 out_first_name
、out_last_name
和 out_birth_date
。
execute
方法的最后一部分创建了一个 Actor
实例,用于返回检索到的数据。重要的是要使用在存储过程中声明的输出参数名称。此外,存储在结果映射中的输出参数名称的大小写需要与数据库中输出参数名称的大小写相匹配,这在不同数据库之间可能有所不同。为了使你的代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用 LinkedCaseInsensitiveMap
。要执行后者,你可以创建自己的 JdbcTemplate
实例,并将 setResultsMapCaseInsensitive
属性设置为 true
。然后,你可以将这个自定义的 JdbcTemplate
实例传递给 SimpleJdbcCall
的构造函数。以下示例展示了这种配置:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor");
}
// ... additional methods
}
通过采取这一行动,你可以避免返回的out
参数名称在大小写方面产生的冲突。
明确声明用于 SimpleJdbcCall 的参数
在本章的前面部分,我们描述了如何根据元数据推断参数,但如果你愿意的话,也可以显式地声明它们。你可以通过创建和配置 SimpleJdbcCall
并使用 declareParameters
方法来实现这一点,该方法接受可变数量的 SqlParameter
对象作为输入。
如果你使用的数据库不是 Spring 支持的数据库,那么就需要显式声明参数。目前,Spring 支持以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。此外,我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数的元数据查找。
你可以选择显式声明一个、一些或全部参数。在没有显式声明参数的地方,仍然会使用参数元数据。如果你想绕过所有潜在参数的元数据查找处理,并且只使用声明的参数,你可以在声明时调用 withoutProcedureColumnMetaDataAccess
方法。假设你为数据库函数声明了两个或更多不同的调用签名。在这种情况下,你会调用 useInParameterNames
来指定要包含在给定签名中的输入参数名称列表。
以下示例展示了一个完全声明的过程调用,并使用了前面示例中的信息:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
new SqlOutParameter("out_last_name", Types.VARCHAR),
new SqlOutParameter("out_birth_date", Types.DATE)
);
}
// ... additional methods
}
这两个示例的执行和最终结果是相同的。第二个示例明确指定了所有细节,而不是依赖于元数据。
如何定义 SqlParameters
为了给 SimpleJdbc
类以及 RDBMS 操作类定义参数,你可以使用 SqlParameter
或其子类之一。要做到这一点,你通常需要在构造函数中指定参数名和 SQL 类型。SQL 类型是通过使用 java.sql.Types
中的常量来指定的。在本章的前面部分,我们看到了类似于以下的声明:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
第一行使用 SqlParameter
声明了一个输入参数(IN 参数)。你可以在使用 SqlQuery
及其子类时,为存储过程调用和查询都使用输入参数。
第二行(使用 SqlOutParameter
)声明了一个out
参数,用于存储过程调用。对于InOut
参数(即向过程提供输入值并返回值的参数),还有 SqlInOutParameter
。
只有声明为 SqlParameter
和 SqlInOutParameter
的参数被用来提供输入值。这与 StoredProcedure
类有所不同,StoredProcedure
类(出于向后兼容的原因)允许为声明为 SqlOutParameter
的参数提供输入值。
对于输入参数(IN 参数),除了参数名和 SQL 类型之外,你还可以为数值数据指定一个小数位数,或为自定义数据库类型指定一个类型名。对于输出参数(out
参数),你可以提供一个 RowMapper
来处理从 REF
游标返回的行的映射。另一种选择是指定一个 SqlReturnType
,它提供了一个机会来定义返回值的自定义处理。
使用 SimpleJdbcCall 调用存储函数(Stored Function)
你可以几乎以与调用存储过程相同的方式调用存储函数,只是你需要提供一个函数名而不是过程名。你使用 withFunctionName
方法作为配置的一部分来指示你想要调用一个函数,并生成相应的函数调用字符串。一个专门的调用方法(executeFunction
)用于运行函数,并返回指定类型的对象作为函数的返回值,这意味着你不需要从结果映射中检索返回值。对于只有一个输出参数的存储过程,也有一个类似的便捷方法(名为 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 SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name");
}
public String getActorName(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
String name = funcGetActorName.executeFunction(String.class, in);
return name;
}
// ... additional methods
}
executeFunction
方法用于返回一个字符串,该字符串包含函数调用的返回值。
使用 SimpleJdbcCall 返回 ResultSet 或 REF CURSOR
调用返回结果集的存储过程或函数有点复杂。一些数据库在JDBC结果处理过程中返回结果集,而其他数据库则需要显式注册特定类型的out
参数。这两种方法都需要额外的处理来遍历结果集和处理返回的行。使用SimpleJdbcCall
时,你可以使用returningResultSet
方法,并声明一个RowMapper
实现来用于特定参数。如果结果集是在结果处理过程中返回的,那么就没有定义名称,因此返回的结果必须与你声明RowMapper
实现的顺序相匹配。指定的名称仍然用于将处理过的结果列表存储在从execute
语句返回的结果映射中。
下一个示例(针对MySQL)使用了一个存储过程,该存储过程不接受任何 IN 参数,而是返回t_actor
表中的所有行。
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
要调用这个存储过程,你可以声明一个RowMapper
。由于你想要映射到的类遵循JavaBean规则,你可以使用BeanPropertyRowMapper
,它是通过在newInstance
方法中传入要映射到的目标类来创建的。下面的示例展示了如何做到这一点:
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
,因为这个调用不需要任何参数。然后从结果Map
中获取演员列表,并将其返回给调用者。
将JDBC操作建模为Java对象
org.springframework.jdbc.object
包包含了一些类,这些类允许你以更面向对象的方式访问数据库。例如,你可以运行查询并将结果作为包含业务对象的列表返回,这些业务对象的属性映射了关系列的数据。你还可以运行存储过程,以及执行更新、删除和插入语句。
许多Spring开发者认为,下面描述的各种关系型数据库管理系统(RDBMS)操作类(除了StoredProcedure
类之外)通常可以被直接的JdbcTemplate
调用所替代。通常,编写一个DAO方法直接调用JdbcTemplate
上的方法(而不是将查询封装成一个完整的类)会更为简单。
理解SqlQuery
SqlQuery
是一个可重用且线程安全的类,它封装了一个 SQL 查询。子类必须实现 newRowMapper(..)
方法,以提供一个 RowMapper
实例,该实例能够从执行查询时创建的 ResultSet
中迭代每一行并创建一个对象。SqlQuery
类很少直接使用,因为 MappingSqlQuery
子类为将行映射到 Java 类提供了更为便捷的实现。扩展 SqlQuery
的其他实现包括 MappingSqlQueryWithParameters
和 UpdatableSqlQuery
。
使用MappingSqlQuery
MappingSqlQuery
是一个可重用的查询类,其中具体的子类必须实现抽象的 mapRow(..)
方法,以将提供的 ResultSet
中的每一行转换为指定类型的对象。以下示例展示了一个自定义查询,该查询将 t_actor
关系中的数据映射到 Actor
类的实例:
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
public ActorMappingQuery(DataSource ds) {
super(ds, "select id, first_name, last_name from t_actor where id = ?");
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}
@Override
protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
Actor actor = new Actor();
actor.setId(rs.getLong("id"));
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
该类继承了以Actor
类型参数化的MappingSqlQuery
。这个自定义查询的构造函数只接受一个DataSource
作为参数。在构造函数中,你可以调用父类的构造函数,并传入DataSource
以及用于检索该查询行的SQL语句。这条SQL语句用于创建一个PreparedStatement
,因此它可能包含占位符,用于在执行时传入任何参数。
你必须使用declareParameter
方法声明每个参数,并传入一个SqlParameter
对象。SqlParameter
接受一个名称和JDBC类型,这些类型在java.sql.Types
中定义。定义完所有参数后,你可以调用compile()
方法,以便准备语句并在稍后执行。编译后,该类是线程安全的,因此,只要这些实例在DAO初始化时创建,它们就可以作为实例变量保留并重复使用。
以下示例展示了如何定义这样的类:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
return actorMappingQuery.findObject(id);
}
前面示例中的方法通过传入唯一的参数(即id
)来检索具有该id
的客户。由于我们只想返回一个对象,因此我们调用带有id
作为参数的findObject
便捷方法。如果我们有一个查询返回对象列表并且需要额外的参数,我们会使用那些接受以可变参数(varargs)形式传入的参数值数组的execute
方法之一。以下示例展示了这样的方法:
public List<Actor> searchForActors(int age, String namePattern) {
return actorSearchMappingQuery.execute(age, namePattern);
}
使用SqlUpdate
SqlUpdate
类封装了一个SQL更新操作。与查询一样,更新对象是可重用的,并且与所有RdbmsOperation
类一样,更新操作可以包含参数,并且是在SQL中定义的。这个类提供了许多与查询对象的execute(..)
方法类似的update(..)
方法。SqlUpdate
类是一个具体的类。它可以被子类化,例如,为了添加一个自定义的更新方法。然而,你不需要子类化SqlUpdate
类,因为它可以通过设置SQL和声明参数来轻松地进行参数化。以下示例创建了一个名为execute
的自定义更新方法:
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class UpdateCreditRating extends SqlUpdate {
public UpdateCreditRating(DataSource ds) {
setDataSource(ds);
setSql("update customer set credit_rating = ? where id = ?");
declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
declareParameter(new SqlParameter("id", Types.NUMERIC));
compile();
}
/**
* @param id for the Customer to be updated
* @param rating the new value for credit rating
* @return number of rows updated
*/
public int execute(int id, int rating) {
return update(rating, id);
}
}
使用StoredProcedure
StoredProcedure
类是RDBMS存储过程对象抽象的一个抽象超类。
继承的 sql
属性是RDBMS中存储过程的名称。
要为StoredProcedure
类定义一个参数,你可以使用SqlParameter
或其子类。在构造函数中,你必须指定参数名称和SQL类型,如下面的代码片段所示:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SQL类型是使用java.sql.Types
常量来指定的。
第一行(包含SqlParameter
的)声明了一个输入(IN)参数。你可以在调用存储过程以及使用SqlQuery
及其子类进行查询时都使用输入参数。
第二行(包含SqlOutParameter
的)声明了一个out
参数,用于存储过程调用。此外,还有一个SqlInOutParameter
用于InOut
参数(这些参数既向存储过程提供输入值,也返回一个值)。
对于in
参数,除了名称和SQL类型之外,你还可以为数值数据指定一个小数位数(scale),或者为自定义数据库类型指定一个类型名称。对于out
参数,你可以提供一个RowMapper
来处理从REF
游标返回的行的映射。另一个选项是指定一个SqlReturnType
,它允许你定义对返回值的自定义处理。
下一个简单的数据访问对象(DAO)示例使用StoredProcedure
来调用一个函数(sysdate()
),这个函数是任何Oracle数据库都自带的。要使用存储过程功能,你需要创建一个继承自StoredProcedure
的类。在这个例子中,StoredProcedure
类是一个内部类。但是,如果你需要重用StoredProcedure
,你可以将其声明为顶级类。这个示例没有输入参数,但是使用SqlOutParameter
类声明了一个输出参数,类型为日期。execute()
方法运行存储过程,并从结果映射(Map
)中提取返回的日期。结果映射对于每个声明的输出参数(在这种情况下只有一个)都有一个条目,使用参数名称作为键。以下列表展示了我们的自定义StoredProcedure
类:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class StoredProcedureDao {
private GetSysdateProcedure getSysdate;
@Autowired
public void init(DataSource dataSource) {
this.getSysdate = new GetSysdateProcedure(dataSource);
}
public Date getSysdate() {
return getSysdate.execute();
}
private class GetSysdateProcedure extends StoredProcedure {
private static final String SQL = "sysdate";
public GetSysdateProcedure(DataSource dataSource) {
setDataSource(dataSource);
setFunction(true);
setSql(SQL);
declareParameter(new SqlOutParameter("date", Types.DATE));
compile();
}
public Date execute() {
// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
Map<String, Object> results = execute(new HashMap<String, Object>());
Date sysdate = (Date) results.get("date");
return sysdate;
}
}
}
下面的StoredProcedure
示例中有两个输出参数(在本例中,是Oracle的REF游标):
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAndGenresStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "AllTitlesAndGenres";
public TitlesAndGenresStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
compile();
}
public Map<String, Object> execute() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap<String, Object>());
}
}
请注意,在TitlesAndGenresStoredProcedure
构造函数中使用的declareParameter(..)
方法的重载变体是如何传递RowMapper
实现实例的。这是一种非常方便且强大的方式来重用现有功能。接下来的两个示例提供了这两个RowMapper
实现的代码。
TitleMapper
类将ResultSet
中的每一行映射为一个Title
领域对象,具体实现如下:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;
public final class TitleMapper implements RowMapper<Title> {
public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
Title title = new Title();
title.setId(rs.getLong("id"));
title.setName(rs.getString("name"));
return title;
}
}
GenreMapper
类将ResultSet
中的每一行映射为一个Genre
领域对象,具体实现如下:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;
public final class GenreMapper implements RowMapper<Genre> {
public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Genre(rs.getString("name"));
}
}
要向在RDBMS中定义了一个或多个输入参数的存储过程传递参数,你可以编写一个强类型化的execute(..)
方法,该方法将委托给超类中的无类型execute(Map)
方法,如下例所示:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAfterDateStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "TitlesAfterDate";
private static final String CUTOFF_DATE_PARAM = "cutoffDate";
public TitlesAfterDateStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
compile();
}
public Map<String, Object> execute(Date cutoffDate) {
Map<String, Object> inputs = new HashMap<String, Object>();
inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
return super.execute(inputs);
}
}
参数和数据值处理中常见的问题
为参数提供SQL类型信息
通常,Spring会根据传入参数的类型来确定SQL参数的类型。但在设置参数值时,也可以显式地提供要使用的SQL类型。这在正确设置NULL
值时有时是必需的。
你可以通过以下几种方式提供SQL类型信息:
JdbcTemplate
的许多更新和查询方法都会接受一个额外的参数,这个参数以整型数组的形式存在。该数组通过使用java.sql.Types
类中的常量值来指示对应参数的SQL类型。对于每个参数,都需要提供一个条目。- 你可以使用
SqlParameterValue
类来包装需要这些额外信息的参数值。为此,你需要为每个值创建一个新的实例,并在构造函数中传入SQL类型和参数值。对于数值类型,你还可以提供一个可选的小数位数(scale )参数。 - 对于使用命名参数的方法,你可以使用
SqlParameterSource
类,如BeanPropertySqlParameterSource
或MapSqlParameterSource
。它们都有方法来为任何命名参数值注册SQL类型。
理BLOB和CLOB对象
你可以在数据库中存储图像、其他二进制数据以及大量文本。这些大对象对于二进制数据称为BLOB(Binary Large OBject),对于字符数据称为CLOB(Character Large OBject)。在Spring中,你可以直接使用JdbcTemplate
来处理这些大对象,也可以使用RDBMS Objects和SimpleJdbc
类提供的高级抽象来处理。所有这些方法都使用LobHandler
接口的实现来实际管理LOB(Large OBject)数据。LobHandler
通过getLobCreator
方法提供对LobCreator
类的访问,该类用于创建要插入的新LOB对象。
LobCreator
和LobHandler
为LOB输入和输出提供以下支持:
- BLOB
byte[]
:getBlobAsBytes
和setBlobAsBytes
InputStream
:getBlobAsBinaryStream
和setBlobAsBinaryStream
- CLOB
String
:getClobAsString
和setClobAsString
InputStream
:getClobAsAsciiStream
和setClobAsAsciiStream
Reader
:getClobAsCharacterStream
和setClobAsCharacterStream
接下来的示例展示了如何创建和插入一个BLOB。稍后我们会展示如何从数据库中读取它。
这个示例使用JdbcTemplate
和AbstractLobCreatingPreparedStatementCallback
的实现。它实现了一个方法,即setValues
。这个方法提供了一个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) {
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());
}
}
);
blobIs.close();
clobReader.close();
如果你调用从DefaultLobHandler.getLobCreator()
返回的LobCreator
上的setBlobAsBinaryStream
、setClobAsAsciiStream
或setClobAsCharacterStream
方法,你可以为contentLength
参数选择性地指定一个负值。如果指定的内容长度为负值,DefaultLobHandler
会使用JDBC 4.0版本的设置流方法,该方法不包含长度参数。否则,它会将指定的长度传递给驱动程序。
现在到了从数据库中读取LOB数据的时候了。同样,你需要使用JdbcTemplate
,并且使用相同的实例变量lobHandler
和对DefaultLobHandler
的引用。以下示例展示了如何操作:
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
new RowMapper<Map<String, Object>>() {
public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
Map<String, Object> results = new HashMap<String, Object>();
String clobText = lobHandler.getClobAsString(rs, "a_clob");
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");
results.put("BLOB", blobBytes);
return results;
}
});
为IN子句传递值列表
SQL标准允许基于包含可变值列表的表达式来选择行。一个典型的例子是select * from T_ACTOR where id in (1, 2, 3)
。JDBC标准不直接支持为预编译语句提供这种可变列表。你不能声明可变数量的占位符。你需要为期望数量的占位符准备多个不同的版本,或者当你知道需要多少占位符时,你需要动态生成SQL字符串。NamedParameterJdbcTemplate
提供的命名参数支持采用后一种方法。你可以将值作为java.util.List
(或任何Iterable
)的简单值传递。这个列表用于将所需的占位符插入实际的SQL语句中,并在语句执行期间传递值。
在传递大量值时,请注意。JDBC标准不保证你可以在IN表达式列表中使用超过100个值。各种数据库可能会超过这个数量,但它们通常对允许的最大值有一个硬限制。例如,Oracle的限制是1000。
除了值列表中的基本值,你还可以创建一个对象数组的java.util.List
。这个列表可以支持为in
子句定义多个表达式,例如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))
。当然,这需要你的数据库支持这种语法。
处理存储过程调用的复杂类型
当你调用存储过程时,有时可能会使用特定于数据库的复杂类型。为了处理这些类型,Spring提供了SqlReturnType
来处理从存储过程调用返回的类型,以及SqlTypeValue
来作为参数传递给存储过程。
SqlReturnType
接口有一个必须实现的方法(名为getTypeValue
)。此接口用作SqlOutParameter
声明的一部分。以下示例展示了返回用户声明的类型ITEM_TYPE
的Oracle STRUCT
对象的值:
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
// ...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
(CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
STRUCT struct = (STRUCT) cs.getObject(colIndx);
Object[] attr = struct.getAttributes();
TestItem item = new TestItem();
item.setId(((Number) attr[0]).longValue());
item.setDescription((String) attr[1]);
item.setExpirationDate((java.util.Date) attr[2]);
return item;
}));
// ...
}
你可以使用SqlTypeValue
将Java对象(如TestItem
)的值传递给存储过程。SqlTypeValue
接口有一个必须实现的方法(名为createTypeValue
)。传递了活动的连接,你可以使用它来创建特定于数据库的对象,如StructDescriptor
实例或ArrayDescriptor
实例。以下示例创建了一个StructDescriptor
实例:
final TestItem testItem = new TestItem(123L, "A test item",
new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
Struct item = new STRUCT(itemDescriptor, conn,
new Object[] {
testItem.getId(),
testItem.getDescription(),
new java.sql.Date(testItem.getExpirationDate().getTime())
});
return item;
}
};
现在,你可以将这个SqlTypeValue
添加到存储过程执行调用的输入参数Map
中。
SqlTypeValue
的另一个用途是将值的数组传递给Oracle存储过程。Oracle有它自己的内部ARRAY
类,必须在这种情况下使用,你可以使用SqlTypeValue
来创建Oracle ARRAY
的实例,并用Java ARRAY
中的值填充它,如下例所示:
final Long[] ids = new Long[] {1L, 2L};
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
return idArray;
}
};
嵌入式数据库支持
org.springframework.jdbc.datasource.embedded
包为嵌入式的Java数据库引擎提供支持。它原生地支持HSQL、H2和Derby。此外,你还可以使用可扩展的API来插入新的嵌入式数据库类型和DataSource
实现。
为什么使用嵌入式数据库?
由于嵌入式数据库的轻量级特性,它在项目的开发阶段非常有用。其优势包括配置简单、启动迅速、可测试性强,以及在开发过程中能够迅速调整SQL语句的能力。
使用Spring XML创建嵌入式数据库
如果你想要将一个嵌入式数据库实例作为Spring ApplicationContext
中的一个bean来暴露,你可以使用spring-jdbc
命名空间中的embedded-database
标签:
<jdbc:embedded-database id="dataSource" generate-name="true">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
上述配置创建了一个嵌入式HSQL数据库,该数据库通过classpath根目录下的schema.sql
和test-data.sql
资源中的SQL语句进行填充。此外,作为最佳实践,嵌入式数据库被分配了一个唯一生成的名称。嵌入式数据库作为类型为javax.sql.DataSource
的bean提供给Spring容器,然后根据需要注入到数据访问对象中。
通过编程方式创建嵌入式数据库
EmbeddedDatabaseBuilder
类提供了一个流畅的API,用于通过编程方式构建嵌入式数据库。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用该类,如下例所示:
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
db.shutdown()
你也可以使用EmbeddedDatabaseBuilder
通过Java配置来创建嵌入式数据库,如下例所示:
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
}
}
选择嵌入式数据库类型
本节介绍了如何选择Spring支持的三种嵌入式数据库之一。包括以下主题:
- 使用HSQL
- 使用H2
- 使用Derby
使用HSQL
Spring支持HSQL 1.8.0及以上版本。如果没有明确指定类型,HSQL就是默认的嵌入式数据库。要明确指定HSQL,请将embedded-database
标签的type
属性设置为HSQL
。如果你使用构建器API,请使用EmbeddedDatabaseType.HSQL
调用setType(EmbeddedDatabaseType)
方法。
使用H2
Spring支持H2数据库。要启用H2,请将embedded-database
标签的type
属性设置为H2
。如果你使用构建器API,请使用EmbeddedDatabaseType.H2
调用setType(EmbeddedDatabaseType)
方法。
使用Derby
Spring支持Apache Derby 10.5及以上版本。要启用Derby,请将embedded-database
标签的type
属性设置为DERBY
。如果你使用构建器API,请使用EmbeddedDatabaseType.DERBY
调用setType(EmbeddedDatabaseType)
方法。
使用嵌入式数据库测试数据访问逻辑
嵌入式数据库为测试数据访问代码提供了一种轻量级的方式。接下来的示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在多个测试类之间重用时,使用这种模板会很有用。但是,如果你希望创建一个在测试套件内共享的嵌入式数据库,请考虑使用Spring TestContext Framework,在Spring ApplicationContext
中将嵌入式数据库配置为一个bean。以下列表显示了测试模板:
public class DataAccessIntegrationTestTemplate {
private EmbeddedDatabase db;
@BeforeEach
public void setUp() {
// creates an HSQL in-memory database populated from default scripts
// classpath:schema.sql and classpath:data.sql
db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.addDefaultScripts()
.build();
}
@Test
public void testDataAccess() {
JdbcTemplate template = new JdbcTemplate(db);
template.query( /* ... */ );
}
@AfterEach
public void tearDown() {
db.shutdown();
}
}
为嵌入式数据库生成唯一名称
开发团队在测试套件无意中尝试重新创建同一个数据库的额外实例时,经常会遇到嵌入式数据库的错误。如果XML配置文件或@Configuration
类负责创建嵌入式数据库,并且相应的配置在相同的测试套件(即相同的JVM进程中)内的多个测试场景中重复使用,那么这种情况就很容易发生——例如,针对嵌入式数据库的集成测试,其ApplicationContext
配置仅在活动的bean定义profile方面有所不同。
这些错误的根本原因是,Spring的EmbeddedDatabaseFactory
(在内部同时被<jdbc:embedded-database>
XML命名空间元素和用于Java配置的EmbeddedDatabaseBuilder
使用)如果未指定名称,则将嵌入式数据库的名称设置为testdb
。对于<jdbc:embedded-database>
的情况,嵌入式数据库通常被分配一个与bean的id
相同的名称(通常类似于dataSource
)。因此,随后尝试创建嵌入式数据库并不会生成新的数据库。相反,相同的JDBC连接URL会被重复使用,尝试创建新的嵌入式数据库实际上是指向使用相同配置创建的现有嵌入式数据库。
为了解决这个常见问题,Spring Framework 4.2为嵌入式数据库提供了生成唯一名称的支持。要启用使用生成名称的功能,请使用以下选项之一。
EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
EmbeddedDatabaseBuilder.generateUniqueName()
<jdbc:embedded-database generate-name="true" … >
扩展嵌入式数据库支持
可以通过以下两种方式扩展Spring JDBC嵌入式数据库支持:
- 实现
EmbeddedDatabaseConfigurer
接口以支持新的嵌入式数据库类型。 - 实现
DataSourceFactory
接口以支持新的DataSource
实现,例如连接池,用于管理嵌入式数据库的连接。
初始化数据源(DataSource)
org.springframework.jdbc.datasource.init
包提供了对现有数据源(DataSource
)进行初始化的支持。嵌入式数据库支持为应用程序创建和初始化数据源提供了一种选择。然而,有时你可能需要初始化一个运行在某个服务器上的实例。
使用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>
前面的示例会运行两个指定的脚本来操作数据库。第一个脚本创建了一个模式(schema),第二个脚本则使用测试数据集填充表格。脚本的位置也可以是带有通配符的模式,这些通配符遵循Spring中用于资源的常见Ant风格(例如,classpath*:/com/foo/**/sql/*-data.sql
)。如果你使用模式,脚本将按照其URL或文件名的字典顺序运行。
数据库初始化器的默认行为是无条件地运行提供的脚本。这可能并不总是你想要的——例如,如果你针对已经包含测试数据的数据库运行脚本。通过遵循常见的模式,即首先创建表,然后插入数据,可以减少意外删除数据的可能性。如果表已经存在,第一步将失败。
然而,为了对现有数据的创建和删除获得更多的控制,XML命名空间提供了一些额外的选项。第一个选项是一个标志,用于打开和关闭初始化。你可以根据环境设置这个标志(例如从系统属性或环境bean中获取布尔值)。以下示例从系统属性中获取一个值:
<jdbc:initialize-database data-source="dataSource"
enabled="#{systemProperties.INITIALIZE_DATABASE}">
<jdbc:script location="..."/>
</jdbc:initialize-database>
控制现有数据如何处理的第二个选项是更宽容地对待失败。为此,你可以控制初始化器忽略从脚本中运行的SQL中的某些错误的能力,如下例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的例子中,我们表示我们期望有时脚本会针对一个空数据库运行,并且脚本中有一些DROP
语句可能会失败。因此,失败的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/myapp/sql/db-schema.sql" separator=";"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
在这个例子中,两个测试数据脚本使用@@
作为语句分隔符,而只有db-schema.sql
使用;
作为分隔符。这种配置指定了默认分隔符为@@
,并且为db-schema
脚本覆盖了这一默认设置。
如果你需要从XML命名空间中获得更多的控制,你可以直接使用DataSourceInitializer
,并将其定义为你应用程序中的一个组件。
依赖于数据库的其他组件的初始化
有一大类应用程序(那些在Spring上下文启动之后才使用数据库的应用程序)可以无需额外复杂设置就使用数据库初始化器。
数据库初始化器依赖于一个DataSource
实例,并在其初始化回调中运行提供的脚本(类似于XML bean定义中的init-method
,组件中的@PostConstruct
方法,或实现InitializingBean
接口的组件中的afterPropertiesSet()
方法)。如果其他bean也依赖于相同的数据源,并在初始化回调中使用数据源,可能会出现问题,因为数据尚未初始化。一个常见的例子是一个缓存,它在应用启动时急切地初始化并从数据库中加载数据。
为了解决这个问题,你有两个选择:改变你的缓存初始化策略到稍后的阶段,或者确保数据库初始化器首先被初始化。
如果你的应用程序在你的控制之下,改变缓存初始化策略可能会很简单。以下是一些关于如何实现这一点的建议:
- 使缓存首次使用时进行懒加载,这样可以提高应用程序的启动时间。
- 让你的缓存或初始化缓存的单独组件实现
Lifecycle
或SmartLifecycle
接口。当应用程序上下文启动时,你可以通过设置SmartLifecycle
的autoStartup
标志来自动启动它,你也可以通过调用包含上下文的ConfigurableApplicationContext.start()
方法来手动启动Lifecycle
。 - 使用Spring的
ApplicationEvent
或类似的自定义观察者机制来触发缓存初始化。当上下文准备好使用时(即所有bean都已初始化后),ContextRefreshedEvent
总是由上下文发布,因此这通常是一个有用的钩子(SmartLifecycle
默认就是这样工作的)。
确保数据库初始化器首先被初始化也可以很简单。以下是一些关于如何实现这一点的建议:
- 依赖于Spring
BeanFactory
的默认行为,即bean是按照注册顺序进行初始化的。你可以通过采用XML配置中一组<import/>
元素的通用做法来轻松安排这一点,这些元素将你的应用程序模块排序,并确保数据库和数据库初始化列在首位。 - 将
DataSource
和使用它的业务组件分开,并将它们放在不同的ApplicationContext
实例中来控制它们的启动顺序(例如,父级上下文包含DataSource
,子级上下文包含业务组件)。这种结构在Spring Web应用程序中很常见,但也可以更广泛地应用。