©
This document uses PHP Chinese website manual Release
JdbcTemplate
是core包的核心类。它替我们完成了资源的创建以及释放工作,从而简化了我们对JDBC的使用。
它还可以帮助我们避免一些常见的错误,比如忘记关闭数据库连接。
JdbcTemplate将完成JDBC核心处理流程,比如SQL语句的创建、执行,而把SQL语句的生成以及查询结果的提取工作留给我们的应用代码。
它可以完成SQL查询、更新以及调用存储过程,可以对ResultSet
进行遍历并加以提取。
它还可以捕获JDBC异常并将其转换成org.springframework.dao
包中定义的,通用的,信息更丰富的异常。
使用JdbcTemplate进行编码只需要根据明确定义的一组契约来实现回调接口。
PreparedStatementCreator
回调接口通过给定的Connection
创建一个PreparedStatement,包含SQL和任何相关的参数。
CallableStatementCreateor
实现同样的处理,只不过它创建的是CallableStatement。
RowCallbackHandler
接口则从数据集的每一行中提取值。
我们可以在DAO实现类中通过传递一个DataSource
引用来完成JdbcTemplate的实例化,也可以在Spring的IoC容器中配置一个JdbcTemplate的bean并赋予DAO实现类作为一个实例。
需要注意的是DataSource
在Spring的IoC容器中总是配制成一个bean,第一种情况下,DataSource
bean将传递给service,第二种情况下DataSource
bean传递给JdbcTemplate bean。
最后,JdbcTemplate中使用的所有SQL将会以“DEBUG”级别记入日志(一般情况下日志的category是JdbcTemplate
相应的全限定类名,不过如果需要对JdbcTemplate
进行定制的话,可能是它的子类名)。
下面是一些使用JdbcTemplate
类的示例。(这些示例并不是完整展示所有的JdbcTemplate
所暴露出来的功能。请查看与之相关的Javadoc)。
一个简单的例子用于展示如何获取一个表中的所有行数。
int rowCount = this.jdbcTemplate.queryForInt("select count(0) from t_accrual");
一个简单的例子展示如何进行参数绑定。
int countOfActorsNamedJoe = this.jdbcTemplate.queryForInt( "select count(0) from t_actors where first_name = ?", new Object[]{"Joe"});
查询一个String
。
String surname = (String) this.jdbcTemplate.queryForObject( "select surname from t_actor where id = ?", new Object[]{new Long(1212)}, String.class);
查询并将结果记录为一个简单的数据模型。
Actor actor = (Actor) this.jdbcTemplate.queryForObject( "select first_name, surname from t_actor where id = ?", new Object[]{new Long(1212)}, new RowMapper() { public Object mapRow(ResultSet rs, int rowNum) throws SQLException { Actor actor = new Actor(); actor.setFirstName(rs.getString("first_name")); actor.setSurname(rs.getString("surname")); return actor; } });
查询并组装多个数据模型。
Collection actors = this.jdbcTemplate.query( "select first_name, surname from t_actor", new RowMapper() { public Object mapRow(ResultSet rs, int rowNum) throws SQLException { Actor actor = new Actor(); actor.setFirstName(rs.getString("first_name")); actor.setSurname(rs.getString("surname")); return actor; } });
如果最后2个示例中的代码出现在同一段程序中,我们有必要去掉这些重复的RowMapper
匿名类代码,将这些代码抽取到一个单独的类中(通常是一个静态的内部类)。
这样,这个内部类就可以在DAO的方法中被共享。因而,最后2个示例写成如下的形式将更加好:
public Collection findAllActors() { return this.jdbcTemplate.query( "select first_name, surname from t_actor", new ActorMapper()); } private static final class ActorMapper implements RowMapper { public Object mapRow(ResultSet rs, int rowNum) throws SQLException { Actor actor = new Actor(); actor.setFirstName(rs.getString("first_name")); actor.setSurname(rs.getString("surname")); return actor; } }
this.jdbcTemplate.update( "insert into t_actor (first_name, surname) values (?, ?)", new Object[] {"Leonor", "Watling"});
this.jdbcTemplate.update( "update t_actor set weapon = ? where id = ?", new Object[] {"Banjo", new Long(5276)});
this.jdbcTemplate.update( "delete from actor where id = ?", new Object[] {new Long.valueOf(actorId)});
execute(..)
方法可以被用作执行任何类型的SQL,甚至是DDL语句。
这个方法的实现需要传入一个回调接口、需要绑定的参数数组等作为参数。
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
调用一个简单的存储过程(更多复杂的存储过程支持请参见存储过程支持)。
this.jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", new Object[]{Long.valueOf(unionId)});
JdbcTemplate
类的实例是线程安全的实例。这一点非常重要,正因为如此,你可以配置一个简单的JdbcTemplate
实例,并将这个“共享的”、“安全的”实例注入到不同的DAO类中去。
另外, JdbcTemplate
是有状态的,因为他所维护的DataSource
实例是有状态的,但是这种状态是无法变化的。
使用JdbcTemplate
的一个常见的最佳实践(同时也是SimpleJdbcTemplate
和NamedParameterJdbcTemplate
类的最佳实践)就是在Spring配置文件中配置一个DataSource
实例,然后将这个共享的DataSource
实例助于到你的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 version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- the DataSource
(parameterized for configuration via a PropertyPlaceHolderConfigurer
) -->
<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
<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>
</beans>
如果你使用Spring提供的JdbcDaoSupport
类,并且你的那些基于JDBC的DAO都继承自这个类,那么你会自动地从JdbcDaoSupport
类中继承了setDataSource(..)
方法。
是否将你的DAO类继承自这些类完全取决于你自己的决定,事实上这并不是必须的,如果你看一下JdbcDaoSupport
类你会发现,这里只是提供了一个简便的方式而已。
无论你是否使用上述这种初始化方式,都无需在执行某些SQL操作时多次创建一个JdbcTemplate
实例。记住,一旦JdbcTemplate
被创建,他是一个线程安全的对象。
一个你需要创建多次JdbcTemplate
实例的理由可能在于,你的应用需要访问多个不同的数据库,从而需要不同的DataSources
来创建不同的JdbcTemplates
实例。
NamedParameterJdbcTemplate
类为JDBC操作增加了命名参数的特性支持,而不是传统的使用('?'
)作为参数的占位符。NamedParameterJdbcTemplate
类对JdbcTemplate
类进行了封装,
在底层,JdbcTemplate
完成了多数的工作。这一个章节将主要描述NamedParameterJdbcTemplate
类与JdbcTemplate
类的一些区别,也就是使用命名参数进行JDBC操作。
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(0) from T_ACTOR where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}
注意这里在'sql'
中使用了命名参数作为变量,而这个名称所对应的值被定义在传入的'namedParameters'
中作为参数(也可以传入到MapSqlParameterSource
中作为参数)。
你也可以传入许多命名参数以及他们所对应的值,以Map
的方式,作为键值对传入到NamedParameterJdbcTemplate
中。
(其余的被NamedParameterJdbcOperations
所暴露的接口以及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(0) from T_ACTOR where first_name = :first_name";
Map namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}
NamedParameterJdbcTemplate
类所具备的另外一个比较好的特性就是可以接收SqlParameterSource
作为传入参数 (这个类位于相同的包定义中)。
你已经在先前的一个例子中看到了这个接口的一个具体实现类。(
MapSqlParameterSource
类)。而SqlParameterSource
这个接口对于NamedParameterJdbcTemplate
类的操作而言是一个传入的参数。MapSqlParameterSource
只是一个非常简单的实现,使用了java.util.Map
作为转接器,
其中,Map中的Key表示参数名称,而Map中的Value表示参数值。
另外一个SqlParameterSource
的实现类是BeanPropertySqlParameterSource
。这个类对传统的Java进行了封装(也就是那些符合JavaBean标准的类),
并且使用了JavaBean的属性作为参数的名称和值。
public class Actor {
private Long id;
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public Long getId() {
return this.id;
}
// setters omitted...
}
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActors(Actor exampleActor) {
// notice how the named parameters match the properties of the above 'Actor
' class
String sql = "select count(0) from T_ACTOR where first_name = :firstName and last_name = :lastName";
SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
return this.namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}
注意,NamedParameterJdbcTemplate
类只是封装了JdbcTemplate
模板;
因而如果你需要访问相应被封装的JdbcTemplate
类,并访问一些只有在JdbcTemplate
中拥有的功能,你需要使用getJdbcOperations()
方法来进行访问。
请参照第 11.2.1.2 节 “JdbcTemplate
的最佳实践”来获取一些使用NamedParameterJdbcTemplate
的最佳实践。
SimpleJdbcTemplate
所提供的一些特性必须工作在Java 5及以上版本。
SimpleJdbcTemplate
类是对JdbcTemplate
类进行的封装,从而可以充分利用Java 5所带来的varargs和autoboxing等特性。
SimpleJdbcTemplate
类完全利用了Java 5语法所带来的蜜糖效应。凡是使用过Java 5的程序员们如果要从Java 5迁移回之前的JDK版本,无疑会发现这些特性所带来的蜜糖效应。
“before and after”示例可以成为SimpleJdbcTemplate
类所带来的蜜糖效应的最佳诠释。
下面的代码示例首先展示了使用传统的JdbcTemplate
进行JDBC访问的代码,接着是使用SimpleJdbcTemplate
类做同样的事情。
// classic JdbcTemplate
-style...
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Actor findActor(long id) {
String sql = "select id, first_name, last_name from T_ACTOR where id = ?";
RowMapper mapper = new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) 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;
}
};
// notice the cast, the wrapping up of the 'id' argument
// in an array, and the boxing of the 'id' argument as a reference type
return (Actor) jdbcTemplate.queryForObject(sql, mapper, new Object[] {Long.valueOf(id)});
}
下面是同样的逻辑,使用了SimpleJdbcTemplate
;可以看到代码“干净”多了:
// SimpleJdbcTemplate
-style...
private SimpleJdbcTemplate simpleJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
}
public Actor findActor(long id) {
String sql = "select id, first_name, last_name from T_ACTOR where id = ?";
ParameterizedRowMapper<Actor> mapper = new ParameterizedRowMapper<Actor>() {
// notice the return type with respect to Java 5 covariant return types
public Actor mapRow(ResultSet rs, int rowNum) 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;
}
};
return this.simpleJdbcTemplate.queryForObject(sql, mapper, id);
}
请同样参照第 11.2.1.2 节 “JdbcTemplate
的最佳实践”来获取一些SimpleJdbcTemplate
的最佳实践
SimpleJdbcTemplate
只是提供了JdbcTemplate
所提供的功能的子类。
如果你需要使用JdbcTemplate
的方法,而这些方法又没有在SimpleJdbcTemplate
中定义,你需要调用getJdbcOperations()
方法
获取相应的方法调用。JdbcOperations
接口中定义的方法需要在这边做强制转化才能使用。
为了从数据库中取得数据,我们首先需要获取一个数据库连接。Spring通过DataSource
对象来完成这个工作。
DataSource
是JDBC规范的一部分,它被视为一个通用的数据库连接工厂。通过使用DataSource,
Container或Framework可以将连接池以及事务管理的细节从应用代码中分离出来。
作为一个开发人员,在开发和测试产品的过程中,你可能需要知道连接数据库的细节。但在产品实施时,你不需要知道这些细节。通常数据库管理员会帮你设置好数据源。
在使用Spring JDBC时,你既可以通过JNDI获得数据源,也可以自行配置数据源(使用Spring提供的DataSource实现类)。使用后者可以更方便的脱离Web容器来进行单元测试。
这里我们将使用DriverManagerDataSource
,不过DataSource有多种实现,
后面我们会讲到。使用DriverManagerDataSource
和你以前获取一个JDBC连接
的做法没什么两样。你首先必须指定JDBC驱动程序的全限定名,这样DriverManager
才能加载JDBC驱动类,接着你必须提供一个url(因JDBC驱动而异,为了保证设置正确请参考相关JDBC驱动的文档),
最后你必须提供一个用户连接数据库的用户名和密码。下面我们将通过一个例子来说明如何配置一个DriverManagerDataSource
:
DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword("");
SQLExceptionTranslator
是一个接口,如果你需要在
SQLException
和org.springframework.dao.DataAccessException
之间作转换,那么必须实现该接口。
转换器类的实现可以采用一般通用的做法(比如使用JDBC的SQLState code),如果为了使转换更准确,也可以进行定制(比如使用Oracle的error code)。
SQLErrorCodeSQLExceptionTranslator
是SQLExceptionTranslator的默认实现。
该实现使用指定数据库厂商的error code,比采用SQLState
更精确。转换过程基于一个JavaBean(类型为SQLErrorCodes
)中的error code。
这个JavaBean由SQLErrorCodesFactory
工厂类创建,其中的内容来自于
“sql-error-codes.xml”配置文件。该文件中的数据库厂商代码基于 Database MetaData
信息中的DatabaseProductName,从而配合当前数据库的使用。
SQLErrorCodeSQLExceptionTranslator
使用以下的匹配规则:
首先检查是否存在完成定制转换的子类实现。通常SQLErrorCodeSQLExceptionTranslator
这个类可以作为一个具体类使用,不需要进行定制,那么这个规则将不适用。
接着将SQLException的error code与错误代码集中的error code进行匹配。
默认情况下错误代码集将从SQLErrorCodesFactory
取得。
错误代码集来自classpath下的sql-error-codes.xml文件,它们将与数据库metadata信息中的database name进行映射。
使用fallback翻译器。SQLStateSQLExceptionTranslator
类是缺省的fallback翻译器。
SQLErrorCodeSQLExceptionTranslator
可以采用下面的方式进行扩展:
public class MySQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator { protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) { if (sqlex.getErrorCode() == -12345) { return new DeadlockLoserDataAccessException(task, sqlex); } return null; } }
在上面的这个例子中,error code为'-12345'
的SQLException将采用该转换器进行转换,而其他的error code将由默认的转换器进行转换。
为了使用该转换器,必须将其作为参数传递给JdbcTemplate
类的setExceptionTranslator
方法,并在需要使用这个转换器器的数据
存取操作中使用该JdbcTemplate
。下面的例子演示了如何使用该定制转换器:
// create aJdbcTemplate
and set data source JdbcTemplate jt = new JdbcTemplate(); jt.setDataSource(dataSource); // create a custom translator and set theDataSource
for the default translation lookup MySQLErrorCodesTransalator tr = new MySQLErrorCodesTransalator(); tr.setDataSource(dataSource); jt.setExceptionTranslator(tr); // use theJdbcTemplate
for thisSqlUpdate
SqlUpdate su = new SqlUpdate(); su.setJdbcTemplate(jt); su.setSql("update orders set shipping_charge = shipping_charge * 1.05"); su.compile(); su.update();
在上面的定制转换器中,我们给它注入了一个数据源,因为我们仍然需要
使用默认的转换器从sql-error-codes.xml
中获取错误代码集。
我们仅需要非常少的代码就可以达到执行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))"); } }
除了execute方法之外,JdbcTemplate
还提供了大量的查询方法。
在这些查询方法中,有很大一部分是用来查询单值的。比如返回一个汇总(count)结果
或者从返回行结果中取得指定列的值。这时我们可以使用queryForInt(..)
、
queryForLong(..)
或者queryForObject(..)
方法。
queryForObject方法用来将返回的JDBC类型对象转换成指定的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.queryForInt("select count(*) from mytable"); } public String getName() { return (String) this.jdbcTemplate.queryForObject("select name from mytable", String.class); } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
除了返回单值的查询方法,JdbcTemplate
还提供了一组返回List结果
的方法。List中的每一项对应查询返回结果中的一行。其中最简单的是queryForList
方法,
该方法将返回一个List
,该List
中的每一条
记录是一个Map
对象,对应应数据库中某一行;而该Map
中的每一项对应该数据库行中的某一列值。下面的代码片断接着上面的例子演示了如何用该方法返回表中所有记录:
private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public List getList() { return this.jdbcTemplate.queryForList("select * from mytable"); }
返回的结果集类似下面这种形式:
[{name=Bob, id=1}, {name=Mary, id=2}]
JdbcTemplate
还提供了一些更新数据库的方法。
在下面的例子中,我们根据给定的主键值对指定的列进行更新。
例子中的SQL语句中使用了“?”占位符来接受参数(这种做法在更新和查询SQL语句中很常见)。
传递的参数值位于一个对象数组中(基本类型需要被包装成其对应的对象类型)。
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 = ?", new Object[] {name, new Integer(id)}); } }
JdbcTemplate
中有一个update
方法,可以方便地从数据库中获取数据库自动创建的主键。(这是JDBC 3.0的标准 - 可以参见13.6节获取详细信息)。
这个方法使用了PreparedStatementCreator
接口作为第一个参数, 可以通过这个接口的实现类来定义相应的Insert语句。另外一个参数是KeyHolder
,
一旦update方法成功,这个参数将包含生成的主键。这里对于创建合适的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