12.2.利用JDBC核心类实现JDBC的基本操作和错误处理
12.2. 利用JDBC核心类实现JDBC的基本操作和错误处理
12.2.1. JdbcTemplate
类
JdbcTemplate
是core包的核心类。它替我们完成了资源的创建以及释放工作,从而简化了我们对JDBC的使用。它还可以帮助我们避免一些常见的错误,比如忘记关闭数据库连接。JdbcTemplate将完成JDBC核心处理流程,比如SQL语句的创建、执行,而把SQL语句的生成以及查询结果的提取工作留给我们的应用代码。它可以完成SQL查询、更新以及调用存储过程,可以对ResultSet
进行遍历并加以提取。它还可以捕获JDBC异常并将其转换成org.springframework.dao
包中定义的,通用的,信息更丰富的异常。
使用JdbcTemplate进行编码只需要根据明确定义的一组契约来实现回调接口。PreparedStatementCreator
回调接口通过给定的Connection
创建一个PreparedStatement,包含SQL和任何相关的参数。CallableStatementCreateor
实现同样的处理,只不过它创建的是CallableStatement。RowCallbackHandler
接口则从数据集的每一行中提取值。
我们可以在一个service实现类中通过传递一个DataSource
引用来完成JdbcTemplate的实例化,也可以在application context中配置一个JdbcTemplate bean,来供service使用。需要注意的是DataSource
在application context总是配制成一个bean,第一种情况下,DataSource
bean将传递给service,第二种情况下DataSource
bean传递给JdbcTemplate bean。因为JdbcTemplate使用回调接口和SQLExceptionTranslator
接口作为参数,所以一般情况下没有必要通过继承JdbcTemplate来定义其子类。
JdbcTemplate中使用的所有SQL将会以“DEBUG”级别记入日志(一般情况下日志的category是JdbcTemplate
相应的全限定类名,不过如果需要对JdbcTemplate
进行定制的话,可能是它的子类名)。
12.2.2. NamedParameterJdbcTemplate
类
NamedParameterJdbcTemplate
类增加了在SQL语句中使用命名参数的支持。在此之前,在传统的SQL语句中,参数都是用'?'
占位符来表示的。
NamedParameterJdbcTemplate
类内部封装了一个普通的JdbcTemplate
,并作为其代理来完成大部分工作。下面的内容主要针对NamedParameterJdbcTemplate
与JdbcTemplate
的不同之处来加以说明,即如何在SQL语句中使用命名参数。
The expect usage pattern for the NamedParameterJdbcTemplate
class is perhaps best illustrated by looking at an example (the finer points will be addressed immediately thereafter).
通过下面的例子我们可以更好地了解NamedParameterJdbcTemplate
的使用模式(在后面我们还有更好的使用方式)。
// some JDBC-backed DAO class... public int countOfActorsByFirstName(String firstName) { String sql = "select count(0) from T_ACTOR where first_name = :first_name"; NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(this.getDataSource()); SqlParameterSource namedParameters = new SimpleSqlParameterSource("first_name", firstName); return template.queryForInt(sql, namedParameters); }
在上面例子中,sql
变量使用了命名参数占位符“first_name”,与其对应的值存在namedParameters
变量中(类型为SimpleSqlParameterSource
)。
如果你喜欢的话,也可以使用基于Map风格的名值对将命名参数传递给NamedParameterJdbcTemplate
(NamedParameterJdbcTemplate
实现了NamedParameterJdbcOperations
接口,剩下的工作将由调用该接口的相应方法来完成,这里我们就不再赘述):
// some JDBC-backed DAO class... public int countOfActorsByFirstName(String firstName) { String sql = "select count(0) from T_ACTOR where first_name = :first_name"; NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(this.getDataSource()); Map namedParameters = new HashMap(); namedParameters.put("first_name", firstName); return template.queryForInt(sql, namedParameters); }
另外一个值得一提的特性是与NamedParameterJdbcTemplate
位于同一个包中的SqlParameterSource
接口。在前面的代码片断中我们已经看到了该接口的实现(即SimpleSqlParameterSource
类),SqlParameterSource
可以用来作为NamedParameterJdbcTemplate
命名参数的来源。SimpleSqlParameterSource
类是一个非常简单的实现,它仅仅是一个java.util.Map
适配器,当然其用法也就不言自明了(如果还有不明了的,可以在Spring的JIRA系统中要求提供更多的相关资料)。
SqlParameterSource
接口的另一个实现--BeanPropertySqlParameterSource
为我们提供了更有趣的功能。该类包装一个类似JavaBean的对象,所需要的命名参数值将由包装对象提供,下面我们使用一个例子来更清楚地说明它的用法。
// some JavaBean-like class... 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... 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"; NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(this.getDataSource()); SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor); return template.queryForInt(sql, namedParameters); }
大家必须牢记一点:NamedParameterJdbcTemplate
类内部包装了一个标准的JdbcTemplate
类。如果你需要访问其内部的JdbcTemplate
实例(比如访问JdbcTemplate
的一些方法)那么你需要使用getJdbcOperations()
方法返回的JdbcOperations
接口。(JdbcTemplate
实现了JdbcOperations
接口)。
NamedParameterJdbcTemplate
类是线程安全的,该类的最佳使用方式不是每次操作的时候实例化一个新的NamedParameterJdbcTemplate
,而是针对每个DataSource
只配置一个NamedParameterJdbcTemplate
实例(比如在Spring IoC容器中使用Spring IoC来进行配置),然后在那些使用该类的DAO中共享该实例。
12.2.3. SimpleJdbcTemplate
类
注意
请注意该类所提供的功能仅适用于Java 5 (Tiger)。
SimpleJdbcTemplate
类是JdbcTemplate
类的一个包装器(wrapper),它利用了Java 5的一些语言特性,比如Varargs和Autoboxing。对那些用惯了Java 5的程序员,这些新的语言特性还是很好用的。
SimpleJdbcTemplate
类利用Java 5的语法特性带来的好处可以通过一个例子来说明。在下面的代码片断中我们首先使用标准的JdbcTemplate
进行数据访问,接下来使用SimpleJdbcTemplate
做同样的事情。
// classic JdbcTemplate
-style...
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(Long.valueOf(rs.getLong("id"))));
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
};
// normally this would be dependency injected of course...
JdbcTemplate jdbcTemplate = new JdbcTemplate(this.getDataSource());
// notice the cast, and 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...
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 (c.f. 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;
}
};
// again, normally this would be dependency injected of course...
SimpleJdbcTemplate simpleJdbcTemplate = new SimpleJdbcTemplate(this.getDataSource());
return simpleJdbcTemplate.queryForObject(sql, mapper, id);
}
12.2.4. DataSource
接口
为了从数据库中取得数据,我们首先需要获取一个数据库连接。 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("");
12.2.5. SQLExceptionTranslator
接口
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进行映射。如果仍然无法匹配,最后将调用fallbackTranslator属性的translate方法,
SQLStateSQLExceptionTranslator
类实例是默认的fallbackTranslator。
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 a JdbcTemplate and set data source JdbcTemplate jt = new JdbcTemplate(); jt.setDataSource(dataSource); // create a custom translator and set the DataSource for the default translation lookup MySQLErrorCodesTransalator tr = new MySQLErrorCodesTransalator(); tr.setDataSource(dataSource); jt.setExceptionTranslator(tr); // use the JdbcTemplate for this SqlUpdate 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
中获取错误代码集。
12.2.6. 执行SQL语句
我们仅需要非常少的代码就可以达到执行SQL语句的目的,一旦获得一个 DataSource
和一个JdbcTemplate
, 我们就可以使用JdbcTemplate
提供的丰富功能实现我们的操作。 下面的例子使用了极少的代码完成创建一张表的工作。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAStatement { private JdbcTemplate jt; private DataSource dataSource; public void doExecute() { jt = new JdbcTemplate(dataSource); jt.execute("create table mytable (id integer, name varchar(100))"); } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
12.2.7. 执行查询
除了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 jt; private DataSource dataSource; public int getCount() { jt = new JdbcTemplate(dataSource); int count = jt.queryForInt("select count(*) from mytable"); return count; } public String getName() { jt = new JdbcTemplate(dataSource); String name = (String) jt.queryForObject("select name from mytable", String.class); return name; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
除了返回单值的查询方法,JdbcTemplate
还提供了一组返回List结果 的方法。List中的每一项对应查询返回结果中的一行。其中最简单的是queryForList
方法, 该方法将返回一个List
,该List
中的每一条 记录是一个Map
对象,对应应数据库中某一行;而该Map
中的每一项对应该数据库行中的某一列值。下面的代码片断接着上面的例子演示了如何用该方法返回表中 所有记录:
public List getList() { jt = new JdbcTemplate(dataSource); List rows = jt.queryForList("select * from mytable"); return rows; }
返回的结果集类似下面这种形式:
[{name=Bob, id=1}, {name=Mary, id=2}]
12.2.8. 更新数据库
JdbcTemplate
还提供了一些更新数据库的方法。 在下面的例子中,我们根据给定的主键值对指定的列进行更新。 例子中的SQL语句中使用了“?”占位符来接受参数(这种做法在更新和查询SQL语句中很常见)。 传递的参数值位于一个对象数组中(基本类型需要被包装成其对应的对象类型)。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAnUpdate { private JdbcTemplate jt; private DataSource dataSource; public void setName(int id, String name) { jt = new JdbcTemplate(dataSource); jt.update("update mytable set name = ? where id = ?", new Object[] {name, new Integer(id)}); } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }