Chapter 11. 使用JDBC进行数据访问

11.1. 简介

Spring JDBC抽象框架所带来的价值将在以下几个方面得以体现:(注:使用了Spring JDBC抽象框架之后,应用开发人员只需要完成斜体字部分的编码工作。)

  1. 定义数据库连接参数

  2. 打开数据库连接

  3. 声明SQL语句

  4. 预编译并执行SQL语句

  5. 遍历查询结果(如果需要的话)

  6. 处理每一次遍历操作

  7. 处理抛出的任何异常

  8. 处理事务

  9. 关闭数据库连接

Spring将替我们完成所有使用JDBC API进行开发的单调乏味的、底层细节处理工作。

11.1.1. 选择一种工作模式

使用Spring进行基本的JDBC访问数据库有多种选择。Spring至少提供了三种不同的工作模式:JdbcTemplate, 一个在Spring2.5中新提供的SimpleJdbc类能够更好的处理数据库元数据; 还有一种称之为RDBMS Object的风格的面向对象封装方式, 有点类似于JDO的查询设计。 我们在这里简要列举你采取某一种工作方式的主要理由. 不过请注意, 即使你选择了其中的一种工作模式, 你依然可以在你的代码中混用其他任何一种模式以获取其带来的好处和优势。 所有的工作模式都必须要求JDBC 2.0以上的数据库驱动的支持, 其中一些高级的功能可能需要JDBC 3.0以上的数据库驱动支持。

  • JdbcTemplate - 这是经典的也是最常用的Spring对于JDBC访问的方案。这也是最低级别的封装, 其他的工作模式事实上在底层使用了JdbcTemplate作为其底层的实现基础。JdbcTemplate在JDK 1.4以上的环境上工作得很好。

  • NamedParameterJdbcTemplate - 对JdbcTemplate做了封装,提供了更加便捷的基于命名参数的使用方式而不是传统的JDBC所使用的“?”作为参数的占位符。这种方式在你需要为某个SQL指定许多个参数时,显得更加直观而易用。该特性必须工作在JDK 1.4以上。

  • SimpleJdbcTemplate - 这个类结合了JdbcTemplate和NamedParameterJdbcTemplate的最常用的功能,同时它也利用了一些Java 5的特性所带来的优势,例如泛型、varargs和autoboxing等,从而提供了更加简便的API访问方式。需要工作在Java 5以上的环境中。

  • SimpleJdbcInsert 和 SimpleJdbcCall - 这两个类可以充分利用数据库元数据的特性来简化配置。通过使用这两个类进行编程,你可以仅仅提供数据库表名或者存储过程的名称以及一个Map作为参数。其中Map的key需要与数据库表中的字段保持一致。这两个类通常和SimpleJdbcTemplate配合使用。这两个类需要工作在JDK 5以上,同时数据库需要提供足够的元数据信息。

  • RDBMS 对象包括MappingSqlQuery, SqlUpdate and StoredProcedure - 这种方式允许你在初始化你的数据访问层时创建可重用并且线程安全的对象。该对象在你定义了你的查询语句,声明查询参数并编译相应的Query之后被模型化。一旦模型化完成,任何执行函数就可以传入不同的参数对之进行多次调用。这种方式需要工作在JDK 1.4以上。

11.1.2. Spring JDBC包结构

Spring Framework的JDBC抽象框架由四个包构成:coredataSourceobject以及support

org.springframework.jdbc.core包由JdbcTemplate类以及相关的回调接口(callback interface)和类组成。 org.springframework.jdbc.core.simple 子包则包含了 SimpleJdbcTemplate 类以及相关的SimpleJdbcInsert 类和SimpleJdbcCall 类。 org.springframework.jdbc.core.namedparam 子包则包含了NamedParameterJdbcTemplate 类以及其相关的支持类。

org.springframework.jdbc.datasource包提供了一些工具类来简化对DataSource的访问。同时提供了多种简单的DataSource实现。这些实现可以脱离J2EE容器进行独立测试和运行。 这些工具类提供了一些静态方法,允许你通过JNDI来获取数据库连接和关闭连接。同时支持绑定到当前线程的数据库连接。例如使用DataSourceTransactionManager

接着org.springframework.jdbc.object包包含了一些类,用于将RDBMS查询、更新以及存储过程表述为一些可重用的、线程安全的对象。这种方式通过JDO进行模型化,不过这些通过查询返回的对象是与数据库脱离的对象。 这种对于JDBC的高层次的封装是基于org.springframework.jdbc.core包对JDBC的低层次封装之上的。

最后,org.springframework.jdbc.support包定义了SQLException转化类以及一些其他的工具类。

在JDBC调用过程中所抛出的异常都会被转化为在org.springframework.dao包中定义的异常。也就是说,凡是使用Spring的JDBC封装层的代码无需实现任何JDBC或者RDBMS相关的异常处理。所有的这些被转化的异常都是unchecked异常,因而也给了你一种额外的选择,你可以抓住这些异常,从而转化成其他类型的异常被允许调用者传播。

11.2. 利用JDBC核心类控制JDBC的基本操作和错误处理

11.2.1. JdbcTemplate

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进行定制的话,可能是它的子类名)。

11.2.1.1. 一些示例

下面是一些使用JdbcTemplate类的示例。(这些示例并不是完整展示所有的JdbcTemplate所暴露出来的功能。请查看与之相关的Javadoc)。

11.2.1.1.1. 查询(SELECT)

一个简单的例子用于展示如何获取一个表中的所有行数。

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;
    }
}
11.2.1.1.2. 更新(INSERT/UPDATE/DELETE)
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)});
11.2.1.1.3. 其他操作

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)});

11.2.1.2. JdbcTemplate 的最佳实践

JdbcTemplate类的实例是线程安全的实例。这一点非常重要,正因为如此,你可以配置一个简单的JdbcTemplate实例,并将这个“共享的”、“安全的”实例注入到不同的DAO类中去。 另外, JdbcTemplate 是有状态的,因为他所维护的DataSource 实例是有状态的,但是这种状态是无法变化的。

使用JdbcTemplate的一个常见的最佳实践(同时也是SimpleJdbcTemplateNamedParameterJdbcTemplate 类的最佳实践)就是在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实例。

11.2.2. NamedParameterJdbcTemplate

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()方法来进行访问。

请参照Section 11.2.1.2, “JdbcTemplate 的最佳实践”来获取一些使用NamedParameterJdbcTemplate的最佳实践。

11.2.3. SimpleJdbcTemplate

[Note]Note

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);
}

请同样参照Section 11.2.1.2, “JdbcTemplate 的最佳实践”来获取一些SimpleJdbcTemplate的最佳实践

[Note]Note

SimpleJdbcTemplate只是提供了JdbcTemplate所提供的功能的子类。 如果你需要使用JdbcTemplate的方法,而这些方法又没有在SimpleJdbcTemplate中定义,你需要调用getJdbcOperations()方法 获取相应的方法调用。JdbcOperations接口中定义的方法需要在这边做强制转化才能使用。

11.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("");

11.2.5. SQLExceptionTranslator接口

SQLExceptionTranslator是一个接口,如果你需要在 SQLExceptionorg.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 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中获取错误代码集。

11.2.6. 执行SQL语句

我们仅需要非常少的代码就可以达到执行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))");
    }
}

11.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 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}]

11.2.8. 更新数据库

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)});
    }
}

11.2.9. 获取自动生成的主键

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

11.3. 控制数据库连接

11.3.1. DataSourceUtils

DataSourceUtils作为一个帮助类提供易用且强大的数据库访问能力, 我们可以使用该类提供的静态方法从JNDI获取数据库连接以及在必要的时候关闭之。 它提供支持线程绑定的数据库连接(比如使用DataSourceTransactionManager的时候,将把数据库连接绑定到当前的线程上)。

11.3.2. SmartDataSource接口

SmartDataSourceDataSource 接口的一个扩展,用来提供数据库连接。使用该接口的类在指定的操作之后可以检查是否需要关闭连接。该接口在某些情况下非常有用,比如有些情况需要重用数据库连接。

11.3.3. AbstractDataSource

AbstractDataSource是一个实现了DataSource 接口的abstract基类。它实现了DataSource接口的 一些无关痛痒的方法,如果你需要实现自己的DataSource,那么可以继承该类。

11.3.4. SingleConnectionDataSource

SingleConnectionDataSourceSmartDataSource接口 的一个实现,其内部包装了一个单连接。该连接在使用之后将不会关闭,很显然它不能在多线程的环境下使用。

当客户端代码调用close方法的时候,如果它总是假设数据库连接来自连接池(就像使用持久化工具时一样), 你应该将suppressClose设置为true。这样,通过该类获取的将是代理连接(禁止关闭)而不是原有的物理连接。 需要注意的是,我们不能把使用该类获取的数据库连接造型(cast)为Oracle Connection之类的本地数据库连接。

SingleConnectionDataSource主要在测试的时候使用。它使得测试代码很容易脱离应用服务器而在一个简单的JNDI环境下运行。 与DriverManagerDataSource不同的是,它始终只会使用同一个数据库连接,从而避免每次建立物理连接的开销。

11.3.5. DriverManagerDataSource

DriverManagerDataSource类实现了 SmartDataSource接口。可以使用bean properties来设置JDBC Driver属性,该类每次返回的都是一个新的连接。

该类主要在测试以及脱离J2EE容器的独立环境中使用。它既可以用来在application context中作为一个DataSource bean,也可以在简单的JNDI环境下使用。 由于Connection.close()仅仅只是简单的关闭数据库连接,因此任何能够获取DataSource的持久化代码都能很好的工作。不过使用JavaBean风格的连接池 (比如commons-dbcp)也并非难事。即使是在测试环境下,使用连接池也是一种比使用DriverManagerDataSource更好的做法。

11.3.6. TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy作为目标DataSource的一个代理, 在对目标DataSource包装的同时,还增加了Spring的事务管理能力, 在这一点上,这个类的功能非常像J2EE服务器所提供的事务化的JNDI DataSource

[Note]Note

该类几乎很少被用到,除非现有代码在被调用的时候需要一个标准的 JDBC DataSource接口实现作为参数。 这种情况下,这个类可以使现有代码参与Spring的事务管理。通常最好的做法是使用更高层的抽象 来对数据源进行管理,比如JdbcTemplateDataSourceUtils等等。

如果需要更详细的资料,请参考 TransactionAwareDataSourceProxy JavaDocs。

11.3.7. DataSourceTransactionManager

DataSourceTransactionManager类是 PlatformTransactionManager接口的一个实现,用于处理单JDBC数据源。 它将从指定DataSource取得的JDBC连接绑定到当前线程,因此它也支持了每个数据源对应到一个线程。

我们推荐在应用代码中使用DataSourceUtils.getConnection(DataSource)来获取 JDBC连接,而不是使用J2EE标准的DataSource.getConnection。因为前者将抛出 unchecked的org.springframework.dao异常,而不是checked的 SQLException异常。Spring Framework中所有的类(比如 JdbcTemplate)都采用这种做法。如果不需要和这个 DataSourceTransactionManager类一起使用,DataSourceUtils 提供的功能跟一般的数据库连接策略没有什么两样,因此它可以在任何场景下使用。

DataSourceTransactionManager类支持定制隔离级别,以及对SQL语句查询超时的设定。 为了支持后者,应用代码必须使用JdbcTemplate或者在每次创建SQL语句时调用DataSourceUtils.applyTransactionTimeout(..)方法。

在使用单个数据源的情形下,你可以用DataSourceTransactionManager来替代JtaTransactionManager, 因为DataSourceTransactionManager不需要容器支持JTA。如果你使用DataSourceUtils.getConnection(DataSource)来获取 JDBC连接,二者之间的切换只需要更改一些配置。最后需要注意的一点就是JtaTransactionManager不支持隔离级别的定制!

11.3.8. NativeJdbcExtractor

有时我们需要执行特殊的,由特定厂商提供的与标准JDBC的API不同的JDBC方法。此时,当我们在某个应用服务器上运行包装了这些厂商各自实现的Connection, StatementResultSet对象的DataSource 时,可能会遇到一些问题。如果你要访问这些对象,你可以配置一个包含NativeJdbcExtractorJdbcTemplate或者OracleLobHandler

NativeJdbcExtractor根据执行环境的不同,会有不同的风格的实现:

  • SimpleNativeJdbcExtractor

  • C3P0NativeJdbcExtractor

  • CommonsDbcpNativeJdbcExtractor

  • JBossNativeJdbcExtractor

  • WebLogicNativeJdbcExtractor

  • WebSphereNativeJdbcExtractor

  • XAPoolNativeJdbcExtractor

通常来说SimpleNativeJdbcExtractor类对于绝大多数环境,已经足以屏蔽 Connection 对象。可以参见Java Docs获取详细信息。

11.4. JDBC批量操作

绝大多数JDBC驱动针对批量调用相同的prepared statement对象提供了性能提升。通过将这些更新操作封装到一个批量操作中,可以大量减少与数据库的操作频繁度。 本章节将详细描述使用JdbcTemplate或者SimpleJdbcTemplate进行批量操作的流程。

11.4.1. 使用JdbcTemplate进行批量操作

JdbcTemplate的批量操作特性需要实现特定的接口BatchPreparedStatementSetter来进行的, 通过实现这个接口,并将其传入batchUpdate方法进行调用。 这个接口有两个方法需要实现。一个叫做getBatchSize来提供当前需要批量操作的数量。另外一个方法是setValues 允许你为prepared statement设置参数。这个方法将在整个过程中被调用的次数,则取决于你在getBatchSize中所指定的大小。 下面的示例展示了根据传入的list参数更新actor表,而传入的list同时作为批量操作的参数。

public class JdbcActorDao implements ActorDao {
    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int[] batchUpdate(final List actors) {
        int[] updateCounts = 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, ((Actor)actors.get(i)).getFirstName());
                        ps.setString(2, ((Actor)actors.get(i)).getLastName());
                        ps.setLong(3, ((Actor)actors.get(i)).getId().longValue());
                    }

                    public int getBatchSize() {
                        return actors.size();
                    }
                } );
        return updateCounts;
    }

    //  ... additional methods
}

如果你是通过读取文件进行批量操作,那么你可能需要一个特定的批量操作的数量,不过最后一次的批量操作,你可能没有那么多数量的记录。 在这种情况下,你可以实现InterruptibleBatchPreparedStatementSetter接口,从而允许你在某些情况中断批量操作,isBatchExhausted 方法允许你指定一个终止批量操作的信号量。

11.4.2. 使用SimpleJdbcTemplate进行批量操作

SimpleJdbcTemplate类提供了另外一种批量操作的方式。无需实现一个特定的接口,你只需要提供所有在调用过程中要用到的参数,框架会遍历这些参数值,并使用内置的prepared statement类进行批量操作。API将根据你是否使用命名参数而有所不同。对于使用命名参数的情况,你需要提供一个SqlParameterSource的数组, 其中的每个元素将将作为批量操作的参数。 你可以使用SqlParameterSource.createBatch方法,通过传入一个JavaBean的数组或者一个包含了参数键值对的Map数组来创建这个数组。

下面的示例展示了使用命名参数进行批量更新的方法:

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }

    public int[] batchUpdate(final List<Actor> actors) {
        SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray());
        int[] updateCounts = simpleJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                batch);
        return updateCounts;
    }

    //  ... additional methods
}

对于使用传统的“?”作为参数占位符的情况,你可以传入一个List,包含了所有需要进行批量更新的对象。这样的对象数组必须与每个SQL Statement的占位符以及他们在SQL Statement中出现的位置一一对应。

下面是同样的例子,使用的传统的“?”作为参数占位符:

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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);
        }
        int[] updateCounts = simpleJdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                batch);
        return updateCounts;
    }

    //  ... additional methods
}

所有的批量更新的方法都会返回一组int的数组,表示在整个操作过程中有多少记录被批量更新。 这个数量是由JDBC驱动所返回的,有时这个返回并不可靠,尤其对于某些JDBC驱动只是简单的返回-2作为返回值。

11.5. 通过使用SimpleJdbc类简化JDBC操作

SimpleJdbcInsert类和SimpleJdbcCall类主要利用了JDBC驱动所提供的数据库元数据的一些特性来简化数据库操作配置。 这意味着你可以尽可能的简化你的数据库操作配置。当然,你可以可以将元数据处理的特性关闭,从而在你的代码中详细指定这些特性。

11.5.1. 使用SimpleJdbcInsert插入数据

让我们从SimpleJdbcInsert类开始。我们将尽可能使用最少量的配置。SimpleJdbcInsert类必须在数据访问层的初始化方法中被初始化。 在这个例子中,初始化方法为setDataSource方法。你无需继承自SimpleJdbcInsert 类,只需要创建一个新的实例,并通过withTableName方法设置table名称。 这个类使用了“fluid”模式返回SimpleJdbcInsert,从而你可以访问到所有的可以进行配置的方法。在这个例子中,我们只使用了一个方法,稍后我们会看到更多的配置方法。

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}

这个方法通过接收 java.utils.Map 作为它唯一的参数。 在这里需要重点注意的是,在Map中的所有的key,必须和数据库中定义的列名完全匹配。这是因为我们是通过读取元数据来构造实际的Insert语句的。

11.5.2. 使用SimpleJdbcInsert来获取自动生成的主键

接下来,我们对于同样的插入语句,我们并不传入id,而是通过数据库自动获取主键的方式来创建新的Actor对象并插入数据库。 当我们创建SimpleJdbcInsert实例时, 我们不仅需要指定表名,同时我们通过usingGeneratedKeyColumns方法指定需要数据库自动生成主键的列名。

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}

这样我们可以看到与之前执行的insert操作的不同之处在于,我们无需添加id到参数的Map中去,只需要调用executeReturningKey方法。 这个方法将返回一个java.lang.Number对象,我们可以使用这个对象来创建一个字符类型的实例作为我们的域模型的属性。 有一点很重要的地方需要注意,我们无法依赖所有的数据库来返回我们指定的Java类型,因为我们只知道这些对象的基类是java.lang.Number。 如果你有联合主键或者那些非数字类型的主键需要生成,那么你可以使用executeReturningKeyHolder方法返回的KeyHolder对象。

11.5.3. 指定SimpleJdbcInsert所使用的字段

通过指定所使用的字段名称,可以使SimpleJdbcInsert仅使用这些字段作为insert语句所使用的字段。这可以通过usingColumns方法进行配置。

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}

执行这样的insert语句所使用的字段,与之前我们所依赖的数据库元数据是一致的。

11.5.4. 使用SqlParameterSource提供参数值

使用Map来指定参数值有时候工作得非常好,但是这并不是最简单的使用方式。Spring提供了一些其他的SqlParameterSource实现类来指定参数值。 我们首先可以看看BeanPropertySqlParameterSource类,这是一个非常简便的指定参数的实现类,只要你有一个符合JavaBean规范的类就行了。它将使用其中的getter方法来获取参数值。 下面是一个例子:

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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 SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}

正如你看到的,配置是一样的,区别只是切换了不同的提供参数的实现方式来执行调用。

11.5.5. 使用SimpleJdbcCall调用存储过程

接下来我们把我们的关注点放在使用 SimpleJdbcCall 来进行存储过程的调用上。 设计这个类的目的在于使得调用存储过程尽可能简单。它同样利用的数据库元数据的特性来查找传入的参数和返回值。 这意味着你无需明确声明那些参数。当然,如果你喜欢,可以依然声明这些参数,尤其对于某些参数,你无法直接将他们映射到Java类上,例如ARRAY类型和STRUCT类型的参数。 在我们的第一个示例中,我们可以看到一个简单的存储过程调用,它仅仅返回VARCHAR和DATE类型。 这里,我特地为Actor类增加了一个birthDate的属性,从而可以使得返回值拥有不同的数据类型。 这个存储过程读取actor的主键,并返回first_name,last_name,和birth_date字段作为返回值。 下面是这个存储过程的源码,它可以工作在MySQL数据库上:

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”,表示了Actor的主键,剩下的参数是作为读取数据库表中的数据所返回的返回值。

SimpleJdbcCall的声明与SimpleJdbcInsert类似,你无需继承这个类,而只需要在初始化方法中进行初始化。 在这里例子中,我们只需要指定存储过程的名称。

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}

通过SimpleJdbcCall执行存储过程需要创建一个SqlParameterSource的实现类来指定传入的参数。 需要注意的是,传入参数的名称与存储过程中定义的名称必须保持一致。这里,我们无需保持一致,因为我们使用数据库的元数据信息来决定我们需要什么样的数据库对象。 当然,你在源代码中所指定的名称可能和数据库中完全不同,有的数据库会把这些名称全部转化成大写,而有些数据库会把这些名称转化为小写。

execute方法接收传入的参数,并返回一个Map作为返回值,这个Map包含所有在存储过程中指定的参数名称作为key。 在这个例子中,他们分别是out_first_name,out_last_nameout_birth_date

execute方法的最后部分是使用存储过程所返回的值创建一个新的Actor实例。 同样的,这里我们将名称与存储过程中定义的名称保持一致非常重要。在这个例子中,在返回的Map中所定义的key值和数据库的存储过程中定义的值一致。 你可能需要在这里指定Spring使用Jakarta Commons提供的CaseInsensitiveMap。这样做,你需要在创建你自己的JdbcTemplate类时,设置setResultsMapCaseInsensitive属性为True。 然后,你将这个自定义的JdbcTemplate传入SimpleJdbcCall的构造函数。当然,你需要把commons-collections.jar加入到classpath中去。 下面是配置示例:

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
}

通过这样的配置,你就可以无需担心返回参数值的大小写问题。

11.5.6. 声明SimpleJdbcCall使用的参数

你已经看到如何通过元数据来简化参数配置,但是你也可以明确地指定这些参数。可以在创建SimpleJdbcCall时,通过使用declareParameters方法来声明参数。 这个方法接收一组SqlParameter对象作为参数。我们可以参照下一个章节,如何创建SqlParameter

我们可以有选择性的显示声明一个、多个、甚至所有的参数。参数元数据在这里会被同时使用。 通过调用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
}

执行和最终的返回处理是相同的,我们在这里只是明确声明了参数类型,而不是依赖数据库元数据特性。 这一点很重要,尤其对于那些数据库并不支持元数据的情况。当前,我们支持元数据的特性的数据包含:Apache Derby、DB2、MySQL、 Microsoft SQL Server、Oracle和Sybase。我们同时对某些数据库内置函数支持元数据特性:MySQL、Microsoft SQL Server和Oracle。

11.5.7. 如何定义SqlParameters

为SimpleJdbc类或者后续章节提到的RDBMS操作指定参数,你需要使用SqlParameter或者他的子类。 你可以通过指定参数名称以及对应的SQL类型并传入构造函数作为参数来指定SqlParameter,其中,SQL类型是java.sql.Types中所定义的常量。 我们已经看到过类似的声明:

   new SqlParameter("in_id", Types.NUMERIC),
   new SqlOutParameter("out_first_name", Types.VARCHAR),

第一行的SqlParameter定义了一个传入参数。传入参数可以在所有的存储过程中使用,也可以在稍后章节中提到的SqlQuery类及其子类中使用。

第二行SqlOutParameter定义了一个返回值。它可以被存储过程调用所使用。当然,还有一个SqlInOutParameter类,可以用于输入输出参数。 也就是说,它既是一个传入参数,也是一个返回值。

除了参数名称和SQL类型,你还可以声明一些其他额外的选项。对于传入参数,你可以为numeric数据类型指定精度,或者对于特定的数据库指定特殊类型。 对于返回值,你可以提供一个RowMapper接口来处理所有从REF cursor返回的列。另外一个选项是指定一个SqlReturnType类,从而可以定制返回值的处理方式。

11.5.8. 使用SimpleJdbcCall调用内置函数

内置函数的调用几乎和存储过程的调用是一样的。唯一的不同在于,你需要声明的是一个函数的名称而不是存储过程的名称。 这可以通过withFunctionName方法来完成。使用这个方法,表明你的调用是一个函数。你所指定的这个函数名称将被作为调用对象。 同时有一个叫做executeFunction的方法,将获得特定的Java类型的函数调用的返回值。 此时,你无需通过返回的Map来获取返回值。另外有一个类似的便捷方法executeObject用于存储过程,但是他只能处理单个返回值的情况。 下面的示例展示了一个叫做get_actor_name 的函数调用,返回actor的完整的名称。 这个函数将工作在MySQL数据库上。

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA 
BEGIN
  DECLARE out_name VARCHAR(200);
  SELECT concat(first_name, ' ', last_name)
    INTO out_name
    FROM t_actor where id = in_id;
  RETURN out_name;
END;

调用这个函数,我们依然在初始化方法中创建SimpleJdbcCall

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcCall funcGetActorName;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}

被调用的函数返回一个String类型。

11.5.9. 使用SimpleJdbcCall返回的ResultSet/REF Cursor

期望通过调用存储过程或者函数来返回ResultSet一直是一个问题。一些数据库在JDBC结果处理中返回结果集,而另外一些数据库则需要明确指定返回值的类型。 无论哪种方法,都需要在循环遍历结果集时,做出一些额外的工作,从而处理每一条记录。 通过SimpleJdbcCall,你可以使用returningResultSet方法,并定义一个RowMapper的实现类来处理特定的返回值。 当结果集在返回结果处理过程中没有被定义名称时,返回的结果集必须与定义的RowMapper的实现类指定的顺序保持一致。 而指定的名字也会被用作返回结果集中的名称。

在这个例子中,我们将使用一个存储过程,它并不接收任何参数,返回t_actor表中的所有的行,下面是MySQL数据库中的存储过程源码:

CREATE PROCEDURE read_all_actors()
BEGIN
 SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;

要调用这个存储过程,我们需要定义一个RowMapper的实现类。我们所使用的类遵循JavaBean的规范,所以我们可以使用ParameterizedBeanPropertyRowMapper作为实现类。 通过将相应的class类作为参数传入到newInstance方法中,我们可以创建这个实现类。

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcCall procReadAllActors;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadAllActors =
                new SimpleJdbcCall(jdbcTemplate)
                        .withProcedureName("read_all_actors")
                        .returningResultSet("actors",
                                ParameterizedBeanPropertyRowMapper.newInstance(Actor.class));
    }

    public List getActorsList() {
        Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
        return (List) m.get("actors");
    }

    //  ... additional methods
}

这个函数调用传入一个空的Map进入,因为这里不需要任何的参数传入。而函数调用所返回的结果集将返回的是Actors列表。

11.6. 用Java对象来表达JDBC操作

org.springframework.jdbc.object包下的类允许用户以更加 面向对象的方式去访问数据库。比如说,用户可以执行查询并返回一个list, 该list作为一个结果集将把从数据库中取出的列数据映射到业务对象的属性上。 用户也可以执行存储过程,以及运行更新、删除以及插入SQL语句。

[Note]Note

在许多Spring开发人员中间存在有一种观点,那就是下面将要提到的各种RDBMS操作类 (StoredProcedure类除外) 通常也可以直接使用JdbcTemplate相关的方法来替换。 相对于把一个查询操作封装成一个类而言,直接调用JdbcTemplate方法将更简单而且更容易理解。

必须强调的一点是,这仅仅只是一种观点而已, 如果你认为你可以从直接使用RDBMS操作类中获取一些额外的好处,你不妨根据自己的需要和喜好进行不同的选择。

11.6.1. SqlQuery

SqlQuery是一个可重用、线程安全的类,它封装了一个SQL查询。 其子类必须实现newResultReader()方法,该方法用来在遍历 ResultSet的时候能使用一个类来保存结果。 我们很少需要直接使用SqlQuery,因为其子类 MappingSqlQuery作为一个更加易用的实现能够将结果集中的行映射为Java对象。 SqlQuery还有另外两个扩展分别是 MappingSqlQueryWithParametersUpdatableSqlQuery

11.6.2. MappingSqlQuery

MappingSqlQuery是一个可重用的查询抽象类,其具体类必须实现 mapRow(ResultSet, int)抽象方法来将结果集中的每一行转换成Java对象。 下面这个例子演示了一个定制查询,它将从客户表中取得的数据映射到一个Customer类实例。

private class CustomerMappingQuery extends MappingSqlQuery {

    public CustomerMappingQuery(DataSource ds) {
        super(ds, "SELECT id, name FROM customer WHERE id = ?");
        super.declareParameter(new SqlParameter("id", Types.INTEGER));
        compile();
    }

    public Object mapRow(ResultSet rs, int rowNumber) throws SQLException {
        Customer cust = new Customer();
        cust.setId((Integer) rs.getObject("id"));
        cust.setName(rs.getString("name"));
        return cust;
    } 
}

在上面的例子中,我们为用户查询提供了一个构造函数并为构造函数传递了一个 DataSource参数。在构造函数里面我们把 DataSource和一个用来返回查询结果的SQL语句作为参数 调用父类的构造函数。SQL语句将被用于生成一个PreparedStatement对象, 因此它可以包含占位符来传递参数。而每一个SQL语句的参数必须通过调用 declareParameter方法来进行声明,该方法需要一个 SqlParameter(封装了一个字段名字和一个 java.sql.Types中定义的JDBC类型)对象作为参数。 所有参数定义完之后,我们调用compile()方法来对SQL语句进行预编译。

public Customer getCustomer(Integer id) {
    CustomerMappingQuery custQry = new CustomerMappingQuery(dataSource); 
    Object[] parms = new Object[1];
    parms[0] = id;
    List customers = custQry.execute(parms);
    if (customers.size() > 0) {
        return (Customer) customers.get(0);
    }
    else {
        return null;
    }
}

在上面的例子中,getCustomer方法通过传递惟一参数id来返回一个客户对象。 该方法内部在创建CustomerMappingQuery实例之后, 我们创建了一个对象数组用来包含要传递的查询参数。这里我们只有唯一的一个 Integer参数。执行CustomerMappingQuery的 execute方法之后,我们得到了一个List,该List中包含一个 Customer对象,如果有对象满足查询条件的话。

11.6.3. SqlUpdate

SqlUpdate类封装了一个可重复使用的SQL更新操作。 跟所有RdbmsOperation类一样,SqlUpdate可以在SQL中定义参数。 该类提供了一系列update()方法,就像SqlQuery提供的一系列execute()方法一样。 SqlUpdate是一个具体的类。通过在SQL语句中定义参数,这个类可以支持不同的更新方法,我们一般不需要通过继承来实现定制。

import java.sql.Types;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

    public UpdateCreditRating(DataSource ds) {
        setDataSource(ds);
        setSql("update customer set credit_rating = ? where id = ?");
        declareParameter(new SqlParameter(Types.NUMERIC));
        declareParameter(new SqlParameter(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 run(int id, int rating) {
        Object[] params =
            new Object[] {
                new Integer(rating),
                new Integer(id)};
        return update(params);
    }
}

11.6.4. StoredProcedure

StoredProcedure类是一个抽象基类,它是对RDBMS存储过程的一种抽象。 该类提供了多种execute(..)方法,不过这些方法的访问类型都是protected的。

从父类继承的sql属性用来指定RDBMS存储过程的名字。 尽管该类提供了许多必须在JDBC3.0下使用的功能,但是我们更关注的是JDBC 3.0中引入的命名参数特性。

下面的程序演示了如何调用Oracle中的sysdate()函数。 这里我们创建了一个继承StoredProcedure的子类,虽然它没有输入参数, 但是我必须通过使用SqlOutParameter来声明一个日期类型的输出参数。 execute()方法将返回一个map,map中的每个entry是一个用参数名作key,以输出参数为value的名值对。

import java.sql.Types;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.datasource.*;
import org.springframework.jdbc.object.StoredProcedure;

public class TestStoredProcedure {

    public static void main(String[] args)  {
        TestStoredProcedure t = new TestStoredProcedure();
        t.test();
        System.out.println("Done!");
    }
    
    void test() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("oracle.jdbc.OracleDriver");
        ds.setUrl("jdbc:oracle:thin:@localhost:1521:mydb");
        ds.setUsername("scott");
        ds.setPassword("tiger");

        MyStoredProcedure sproc = new MyStoredProcedure(ds);
        Map results = sproc.execute();
        printMap(results);
    }

    private class MyStoredProcedure extends StoredProcedure {
        
        private static final String SQL = "sysdate";

        public MyStoredProcedure(DataSource ds) {
            setDataSource(ds);
            setFunction(true);
            setSql(SQL);
            declareParameter(new SqlOutParameter("date", Types.DATE));
            compile();
        }

        public Map execute() {
            // the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
            return execute(new HashMap());
        }
    }

    private static void printMap(Map results) {
        for (Iterator it = results.entrySet().iterator(); it.hasNext(); ) {
            System.out.println(it.next());  
        }
    }
}

下面是StoredProcedure的另一个例子,它使用了两个Oracle游标类型的输出参数。

import oracle.jdbc.driver.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

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 execute() {
        // again, this sproc has no input parameters, so an empty Map is supplied...
        return super.execute(new HashMap());
    }
}

值得注意的是TitlesAndGenresStoredProcedure构造函数中 declareParameter(..)SqlOutParameter参数, 该参数使用了RowMapper接口的实现。这是一种非常方便而强大的重用方式。 下面我们来看一下RowMapper的两个具体实现。

首先是TitleMapper类,它简单的把ResultSet中的每一行映射为一个TitleDomain Object。

import com.foo.sprocs.domain.Title;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

public final class TitleMapper implements RowMapper {
    
    public Object 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中的每一行映射为一个GenreDomain Object。

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.foo.domain.Genre;

public final class GenreMapper implements RowMapper {
    
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}

如果你需要给存储过程传输入参数(这些输入参数是在RDBMS存储过程中定义好了的), 则需要提供一个指定类型的execute(..)方法, 该方法将调用基类的protected execute(Map parameters)方法。例如:

import oracle.jdbc.driver.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

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 execute(Date cutoffDate) {
        Map inputs = new HashMap();
        inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
        return super.execute(inputs);
    }
}

11.6.5. SqlFunction

SqlFunction RDBMS操作类封装了一个SQL“函数”包装器(wrapper), 该包装器适用于查询并返回一个单行结果集。默认返回的是一个int值, 不过我们可以采用类似JdbcTemplate中的queryForXxx 做法自己实现来返回其它类型。SqlFunction优势在于我们不必创建 JdbcTemplate,这些它都在内部替我们做了。

该类的主要用途是调用SQL函数来返回一个单值的结果集,比如类似“select user()”、 “select sysdate from dual”的查询。如果需要调用更复杂的存储函数, (可以为这种类型的处理使用StoredProcedureSqlCall)

SqlFunction是一个具体类,通常我们不需要它的子类。 其用法是创建该类的实例,然后声明SQL语句以及参数就可以调用相关的run方法去多次执行函数。 下面的例子用来返回指定表的记录行数:

public int countRows() {
    SqlFunction sf = new SqlFunction(dataSource, "select count(*) from mytable");
    sf.compile();
    return sf.run();
}

11.7. 参数和数据处理的基本原则

在Spring的JDBC框架的所有工作模式中贯彻了一些与参数和数据处理相关的基本原则。

11.7.1. 为参数设置SQL类型信息

多数情况下,Spring会根据传入的参数值来设定相应的SQL类型。有时,我们有必要明确指定传入参数所代表的SQL类型,这一点对于正确设置NULL值的时候可能比较有用。

另外还有一些其他的不同方面的作用:

  • 多数JdbcTemplate的update或者query方法会接收一个额外的int数组构成的参数。 这个数组需要提供的是使用java.sql.Types中所定义的SQL类型。而这个数组中定义的类型需要与每个传入的参数所对应。

  • 你可以使用SqlParameterValue对参数进行额外的封装从而包装更多的参数信息。通过传入参数值和对应的SQL类型作为构造函数的参数,你可以创建这个类的一个实例。 你也可以为numeric的值提供一些额外的精度要求。

  • 对于那些使用命名参数的情况,你可以使用SqlParameterSourceBeanPropertySqlParameterSource或者MapSqlParameterSource类。 他们都具备了为命名参数注册SQL类型的功能。

11.7.2. 处理BLOB 和 CLOB对象

你可以在数据库中存储图像、二进制对象或者大文本等对象。这些较大的二进制对象被称之为BLOB类型,而对应的大文本对象被称之为CLOB对象。 Spring允许你使用JdbcTemplate、更高层次封装的RDBMS对象和SimpleJdbc类对这些大对象进行操作。 所有的这些操作方式都实现了LobHandler接口来处理LOB类型的数据。 LobHandler接口提供了访问LobCreator的方法,通过调用getLobCreator,你可以创建一个新的LOB类型的数据。

LobCreator/LobHandler接口针对LOB类型的数据操作提供了下列支持:

  • BLOB

    • byte[] – getBlobAsBytes and setBlobAsBytes

      byte[] – getBlobAsBytes 和 setBlobAsBytes

    • InputStream – getBlobAsBinaryStream and setBlobAsBinaryStream

      InputStream – getBlobAsBinaryStream和setBlobAsBinaryStream

  • CLOB

    • String – getClobAsString and setClobAsString

      String – getClobAsString和setClobAsString

    • InputStream – getClobAsAsciiStream and setClobAsAsciiStream

      InputStream – getClobAsAsciiStream和setClobAsAsciiStream

    • Reader – getClobAsCharacterStream and setClobAsCharacterStream

      Reader – getClobAsCharacterStream和setClobAsCharacterStream

现在我们通过一个示例来展示如何创建一个BLOB数据并插入数据库。稍后的例子,我们将展示如何从数据库中将BLOB数据读取出来。

这个例子使用JdbcTemplate和一个AbstractLobCreatingPreparedStatementCallback的实现类。 这里唯一需要实现的方法就是"setValues"。在这个方法中,将提供一个LobCreator接口,被用作在你的插入语句中设置LOB字段的值。

我们假设有一个变量叫做“lobHandler”已经被设置到DefaultLobHandler的实例中。当然,这是由注入完成的。

final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
  "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
  new AbstractLobCreatingPreparedStatementCallback(lobhandler) {                         (1)
      protected void setValues(PreparedStatement ps, LobCreator lobCreator) 
          throws SQLException {
        ps.setLong(1, 1L);
        lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());    (2)
        lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());           (3)
      }
  }
);
blobIs.close();
clobReader.close();
1

我们在这里使用的lobHandler实现类是一个普通的DefaultLobHandler

2

使用setClobAsCharacterStream,我们传入CLOB的内容

3

使用setBlobAsBinartStream,我们传入BLOB的内容

现在我们来示范从数据库中读取LOB数据。我们这里再次使用JdbcTempate并使用相同的DefaultLobHandler实例。

List l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
    new RowMapper() {
      public Object mapRow(ResultSet rs, int i) throws SQLException {
        Map results = new HashMap();
        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;
      }
    });
2

使用getClobAsString 获取CLOB内容

3

使用getBlobAsBytes获取BLOB内容

11.7.3. 在IN语句中传入一组参数值

SQL标准允许基于一个带参数列表的表达式进行查询。一个典型的例子可能像这样:"select * from T_ACTOR where id in (1, 2, 3)"。 不过这种参数列表的方式并不能直接被JDBC标准所支持 - 因为并不存在这种声明一个列表参数作为占位符的方式。 你不得不为此写多个占位符来表示多个参数,或者当你知道占位符的数量时,你可以动态构建SQL字符串。 NamedParameterJdbcTemplateSimpleJdbcTemplate中所提供的命名参数的特性,采用的是后面一种做法。 当你传入参数时,你需要传入一个java.util.List类型,支持基本类型。而这个list将会在SQL执行时替换占位符并传入参数。

[Note]Note

在使用IN语句时,当你传入大批量的值时要小心,JDBC标准并不确保超过100个元素在IN语句中。 有不少数据库可以超出这个值的限制,但是不同的数据库会有不同的数量限制,比如Oracle的限制数量是1000个。

除了基本类型之外,你还可以创建一个java.util.List的对象数组,这可以让你支持在IN表达式中编写多重表达式,例如"select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))". 当然,这样做的前提是数据库底层的语法支持。

11.7.4. 处理复杂类型的存储过程调用

当调用存储过程时,有时需要使用数据库特定的复杂类型。为了适应这些类型,Spring提供了SqlReturnType类来处理存储过程的返回值,而使用SqlTypeValue来处理传入的参数。

下面这个例子,将Oracle的STRUCT对象作为返回值,这是一个由用户自定义的“ITEM_TYPE”。 SqlReturnType接口有唯一的方法“getTypeValue”需要被实现。而这个接口的实现将被用作SqlOutParameter声明的一部分。

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;
      }
    }));

通过Java代码调用存储过程使用SqlTypeValue来传入一个TestItem作为参数。 SqlTypeValue接口有一个方法"createTypeValue"需要被实现。 一个活动的数据库连接也同时被传入,它将被用作创建数据库特定的对象,类似StructDescriptorArrayDescriptor

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中去,从而可以执行相应的存储过程。