Spring Framework 中文文档

4.3.21.RELEASE

19. 使用 JDBC 进行数据访问

19.1 Spring Framework JDBC 简介

Spring Framework JDBC 抽象提供的 value-add 可能最好通过下面 table 中概述的操作序列来显示。 table 显示了 Spring 将要处理的操作以及哪些操作是您(application 开发人员)的责任。

表格 1_.Spring JDBC - 谁做了什么?

行为spring用户
定义连接参数。 X
打开连接。X
指定 SQL 语句。 X
声明参数并提供参数值 X
准备并执行声明。X
设置循环以迭代结果(如果有)。X
为每次迭代做好工作。 X
Process 任何 exception。X
处理 transactions。X
关闭连接,语句和结果集。X

Spring Framework 负责处理所有 low-level 细节,这些细节可以使 JDBC 成为一个繁琐的 API。

19.1.1 选择 JDBC 数据库访问方法

您可以选择多种方法来构成 JDBC 数据库访问的基础。除了三种类型的 JdbcTemplate 之外,新的 SimpleJdbcInsert 和 SimplejdbcCall 方法优化了数据库元数据,而 RDBMS Object 样式采用了类似于 JDO Query 设计的更多 object-oriented 方法。一旦开始使用这些方法之一,您仍然可以混合和 match 以包含来自不同方法的 feature。所有方法都需要 JDBC 2.0-compliant 驱动程序,而某些高级 features 需要 JDBC 3.0 驱动程序。

  • JdbcTemplate 是经典的 Spring JDBC 方法,也是最受欢迎的方法。这种“最低级”方法和所有其他方法都使用了 JdbcTemplate。

  • NamedParameterJdbcTemplate 包装JdbcTemplate以提供命名参数而不是传统的 JDBC“?”占位符。当您有多个 SQL 语句参数时,此方法可提供更好的文档和易用性。

  • SimpleJdbcInsert 和 SimpleJdbcCall 优化数据库元数据以限制必要的 configuration 数量。此方法简化了编码,因此您只需提供 table 或过程的 name,并提供与列名匹配的参数的 map。这仅在数据库提供足够的元数据时有效。如果数据库未提供此元数据,则必须提供参数的显式 configuration。

  • 包含 MappingSqlQuery,SqlUpdate 和 StoredProcedure 的 RDBMS Objects 要求您在数据访问层初始化期间创建可重用的和 thread-safe objects。此方法在 JDO Query 之后建模,其中您定义查询 string,声明参数和编译查询。执行此操作后,可以多次调用 execute 方法,并传入各种参数值。

19.1.2 包层次结构

Spring Framework 的 JDBC 抽象 framework 由四个不同的包组成,即coredatasourceobjectsupport

org.springframework.jdbc.core包包含JdbcTemplate class 及其各种回调接口,以及各种相关的 classes。名为org.springframework.jdbc.core.simple的子包包含SimpleJdbcInsertSimpleJdbcCall classes。另一个名为org.springframework.jdbc.core.namedparam的子包包含NamedParameterJdbcTemplate class 和相关的支持 classes。请参见第 19.2 节,“使用 JDBC 核心 classes 来控制基本的 JDBC 处理和错误处理”第 19.4 节,“JDBC 批处理操作”第 19.5 节,“使用 SimpleJdbc classes 简化 JDBC 操作”

org.springframework.jdbc.datasource包包含一个用于轻松DataSource访问的实用程序 class,以及各种简单的DataSource __mplementations,可用于在 Java EE 容器外测试和运行未修改的 JDBC code。名为org.springfamework.jdbc.datasource.embedded的子包支持使用 Java 数据库引擎(如 HSQL,H2 和 Derby)创建嵌入式数据库。见第 19.3 节,“控制数据库连接”第 19.8 节,“嵌入式数据库支持”

org.springframework.jdbc.object包中包含 classes,它们将 RDBMS 查询,更新和存储过程表示为 thread-safe,可重用的 objects。见第 19.6 节,“将 JDBC 操作建模为 Java objects”。这种方法由 JDO 建模,尽管查询返回的 objects 自然地与数据库断开连接。这种更高级别的 JDBC 抽象取决于org.springframework.jdbc.core包中的 lower-level 抽象。

org.springframework.jdbc.support包提供SQLException转换功能和一些实用程序 classes。 _ JDBC 处理期间抛出的异常被转换为org.springframework.dao包中定义的 exceptions。这意味着使用 Spring JDBC 抽象层的 code 不需要实现 JDBC 或 RDBMS-specific 错误处理。所有已翻译的 exceptions 都是未选中的,这使您可以选择捕获可以从中恢复的 exceptions,同时允许其他 exceptions 传播给调用者。见第 19.2.3 节,“SQLExceptionTranslator”

19.2 使用 JDBC 核心 classes 来控制基本的 JDBC 处理和错误处理

19.2.1 JdbcTemplate

JdbcTemplate class 是 JDBC 核心包中的中心 class。它处理资源的创建和释放,帮助您避免 common 错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务,例如语句创建和执行,留下 application code 以提供 SQL 并提取结果。 JdbcTemplate class 执行 SQL 查询,更新 statements 和存储过程 calls,在ResultSet上执行迭代并提取返回的参数值。它还捕获 JDBC exceptions 并将它们转换为org.springframework.dao包中定义的通用的,更具信息性的 exception 层次结构。

当您为 code 使用JdbcTemplate时,您只需要实现回调接口,为它们提供明确定义的 contract。给定由 class 提供的ConnectionPreparedStatementCreator回调接口创建一个预准备语句,提供 SQL 和任何必要的参数。 CallableStatementCreator接口的 true 也是如此,它创建了可调用的 statements。 RowCallbackHandler接口从ResultSet的每一行中提取值。

JdbcTemplate可以通过直接实例化DataSource reference 在 DAO implementation 中使用,或者在 Spring IoC 容器中配置,并作为 bean reference 提供给 DAO。

始终应在 Spring IoC 容器中将DataSource配置为 bean。在第一种情况下,bean 直接提供给服务;在第二种情况下,它被给予准备好的模板。

此 class 发出的所有 SQL 都记录在与模板实例的完全限定 class name 对应的类别下的DEBUG level(通常为JdbcTemplate,但如果使用JdbcTemplate class 的自定义子类,则可能会有所不同)。

JdbcTemplate class 用法示例

本节提供了JdbcTemplate class 用法的一些示例。这些示例并不是JdbcTemplate所公开的所有功能的详尽列表。看到服务员 javadocs。

查询(SELECT)

这是一个简单的查询,用于获取关系中的行数:

int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

使用绑定变量的简单查询:

int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

查询String

String lastName = this.jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        new Object[]{1212L}, String.class);

查询和填充单个域 object:

Actor actor = this.jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        new Object[]{1212L},
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

查询和填充许多域 objects:

List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

如果 code 的最后两个片段实际存在于同一个 application 中,那么删除两个RowMapper匿名内部 classes 中存在的重复并将它们提取到单个 class(通常是static嵌套的 class)中是有意义的,然后可以根据需要由 DAO 方法引用。对于 example,最好编写最后一个 code 片段,如下所示:

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}

private static final class ActorMapper implements RowMapper<Actor> {

    public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
        Actor actor = new Actor();
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }
}
使用 JdbcTemplate 更新(INSERT/UPDATE/DELETE)

您使用update(..)方法执行 insert,更新和删除操作。参数值通常以 var args 形式提供,或者作为 object array 提供。

this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");
this.jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L);
this.jdbcTemplate.update(
        "delete from actor where id = ?",
        Long.valueOf(actorId));
其他 JdbcTemplate 操作

您可以使用execute(..)方法执行任意 SQL,因此该方法通常用于 DDL statements。它采用了回调接口,binding 变量数组等变体而严重超载。

this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

以下 example 调用一个简单的存储过程。更复杂的存储过程支持是稍后报道

this.jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        Long.valueOf(unionId));

JdbcTemplate 最佳实践

一旦配置,JdbcTemplate class 的实例就是线程安全的。这很重要,因为这意味着您可以配置JdbcTemplate的单个实例,然后安全地将此共享 reference 注入多个 DAO(或 repositories)。 JdbcTemplate是有状态的,因为它维护的 reference,但是 state 不是会话 state。

使用JdbcTemplate class(以及关联的是 NamedParameterJdbcTemplate classes)时的 common 习惯是在 Spring configuration 文件中配置DataSource,然后在bean 中分配DataSource bean。 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...
}

相应的 configuration 可能如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

显式 configuration 的替代方法是使用 component-scanning 和 annotation 支持依赖注入。在这种情况下,您使用@Repository(使其成为 component-scanning 的候选者)注释 class 并使用@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 configuration 文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- Scans within the base package of the application for @Component classes to configure as beans -->
    <context:component-scan base-package="org.springframework.docs.test" />

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

如果您正在使用 Spring 的JdbcDaoSupport class,并且各种 JDBC-backed DAO classes 从中扩展,那么 sub-class 将从JdbcDaoSupport class 继承setDataSource(..)方法。您可以选择是否继承此 class。 JdbcDaoSupport class 仅为方便起见而提供。

无论您选择使用(或不使用)上述哪种模板初始化样式,都很少需要在每次 time _您要执行 SQL 时创建JdbcTemplate class 的新实例。配置完成后,JdbcTemplate实例就是线程安全的。如果 application 访问多个数据库,这可能需要多个JdbcTemplate实例,这需要多个DataSources,随后需要多个不同配置的JdbcTemplates

19.2.2 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate class 添加了对使用命名参数编写 JDBC statements 的支持,而不是仅使用经典占位符('?')arguments 编写 JDBC statements。 NamedParameterJdbcTemplate class 包装JdbcTemplate,并委托包装JdbcTemplate来完成大部分工作。本节仅描述NamedParameterJdbcTemplate class 中与JdbcTemplate本身不同的那些区域;即,使用命名参数编写 JDBC statements。

// 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变量的 value 中使用命名参数表示法,以及插入namedParameters变量(类型MapSqlParameterSource)的相应 value。

或者,您可以使用由NamedParameterJdbcOperations公开的Map -based style.The 剩余方法将命名参数及其相应值传递给NamedParameterJdbcTemplate实例,并由NamedParameterJdbcTemplate class 实现,遵循类似的 pattern,此处不予介绍。

以下 example 显示了Map -based 样式的用法。

// 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相关的一个很好的 feature(并且存在于同一个 Java 包中)是SqlParameterSource接口。您已经在之前的 code 片段(MapSqlParameterSource class)中看到了此接口的实例实例。 SqlParameterSourceNamedParameterJdbcTemplate的命名参数值的来源。 MapSqlParameterSource class 是一个非常简单的 implementation,只是一个围绕java.util.Map的适配器,其中键是参数名称,值是参数值。

另一个SqlParameterSource implementation 是BeanPropertySqlParameterSource class。此 class 包装一个任意 JavaBean(即 class 的一个实例,它遵循JavaBean 约定),并使用包装的 JavaBean 的 properties 作为命名参数值的来源。

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(*) 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 class 包装了一个经典的JdbcTemplate模板;如果您需要访问包装的JdbcTemplate实例以访问仅存在于JdbcTemplate class 中的功能,则可以使用getJdbcOperations()方法通过JdbcOperations接口访问包装的JdbcTemplate

有关在 application 的 context 中使用NamedParameterJdbcTemplate class 的指导,另请参阅这节名为“JdbcTemplate 最佳实践”

19.2.3 SQLExceptionTranslator

SQLExceptionTranslator是由 classes 实现的接口,可以在SQLExceptions和 Spring 自己的org.springframework.dao.DataAccessException之间进行转换,这与数据访问策略无关。 Implementations 可以是通用的(例如,使用 JDBC 的 SQLState 代码)或专有的(例如,使用 Oracle 错误代码)以获得更高的精度。

SQLErrorCodeSQLExceptionTranslator是默认使用的SQLExceptionTranslator的 implementation。此 implementation 使用特定的供应商代码。它比SQLState implementation 更精确。错误 code 转换基于 JavaBean 类型 class 中保存的代码SQLErrorCodes。这个 class 是由SQLErrorCodesFactory创建和填充的,因为 name 建议它是 creating SQLErrorCodes的工厂,它基于名为sql-error-codes.xml的 configuration 文件的内容。此文件使用供应商代码填充,并基于DatabaseMetaData中的DatabaseProductName。使用您正在使用的实际数据库的代码。

SQLErrorCodeSQLExceptionTranslator按以下顺序应用匹配规则:

默认情况下,SQLErrorCodesFactory用于定义错误代码和自定义 exception 转换。它们在 classpath 中名为sql-error-codes.xml的文件中查找,匹配的SQLErrorCodes实例基于正在使用的数据库的数据库元数据中的数据库 name。

  • 由子类实现的任何自定义转换。通常使用提供的具体SQLErrorCodeSQLExceptionTranslator,因此该规则不适用。它仅适用于您实际提供了子类 implementation 的情况。

  • SQLExceptionTranslator接口的任何自定义 implementation,作为SQLErrorCodes class 的customSqlExceptionTranslator property 提供。

  • SQLErrorCodes class 的customTranslations property 提供的CustomSQLErrorCodesTranslation class 实例列表将搜索 match。

  • 应用错误 code 匹配。

  • 使用后备翻译器。 SQLExceptionSubclassTranslator是默认的后备翻译器。如果此转换不可用,则下一个后备转换器为SQLStateSQLExceptionTranslator

你可以扩展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;
    }
}

在此 example 中,特定错误 code -12345被翻译,其他错误将由默认翻译器 implementation 翻译。要使用此自定义转换程序,必须通过方法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);
}

自定义转换器在 order 中传递数据源以查找sql-error-codes.xml中的错误代码。

19.2.4 执行 statements

执行 SQL 语句只需要很少的 code。您需要DataSourceJdbcTemplate,包括随JdbcTemplate提供的便捷方法。以下 example 显示了为创建新 table 的最小但功能齐全的 class 所需要包含的内容:

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

19.2.5 运行查询

一些查询方法 return 一个 value。要从一行检索计数或特定 value,请使用queryForObject(..)。后者将返回的 JDBC Type转换为作为参数传入的 Java class。如果类型转换无效,则抛出InvalidDataAccessApiUsageException。这是一个包含两个查询方法的 example,一个用于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,map 中的每个条目代表该行的列 value。如果您向上面的 example 添加一个方法来检索所有行的列表,它将如下所示:

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

19.2.6 更新数据库

以下 example 显示了针对某个主 key 更新的列。在此 example 中,SQL 语句具有行参数的占位符。参数值可以作为 varargs 传递,也可以作为 objects 的 array 传递。因此 primitives 应该显式地包装在原始 wrapper classes 中或使用 auto-boxing。

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

19.2.7 检索 auto-generated 键

update()便捷方法支持检索数据库生成的主键。这种支持是 JDBC 3.0 标准的一部分;有关详细信息,请参阅规范的第 13.6 章。该方法将PreparedStatementCreator作为其第一个参数,这是指定所需的 insert 语句的方式。另一个参数是KeyHolder,它包含从更新成功 return 时生成的 key。没有标准的单一方法来创建适当的PreparedStatement(这解释了为什么方法签名就是这样)。以下 example 适用于 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

19.3 控制数据库连接

19.3.1 DataSource

Spring 通过DataSource获取与数据库的连接。 DataSource是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或 framework 隐藏 application code 中的连接池和 transaction management 问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息;这是设置数据源的管理员的责任。您最有可能在开发和测试 code 时填充这两个角色,但您不一定要知道如何配置 production 数据源。

使用 Spring 的 JDBC 层时,您可以从 JNDI 获取数据源,或者使用第三方提供的连接池 implementation 配置您自己的数据源。流行的 implementations 是 Apache Jakarta Commons _DBCP 和 C3P0。 Spring 发行版中的实现仅用于测试目的,不提供合并。

本节使用 Spring 的DriverManagerDataSource implementation,稍后将介绍几个额外的 implementation。

仅使用DriverManagerDataSource class 只应用于测试目的,因为它不提供池,并且在进行多个连接请求时性能很差。

您通常获得 JDBC 连接时获得与DriverManagerDataSource的连接。指定 JDBC 驱动程序的完全限定类名,以便DriverManager可以加载驱动程序 class。接下来,提供不同 JDBC 驱动程序之间的 URL。 (请参阅驱动程序的文档以获取正确的 value.)然后提供用户名和密码以连接到数据库。以下是如何在 Java code 中配置DriverManagerDataSource的示例:

DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

这是相应的 XML configuration:

<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 的基本连接和 configuration。要了解有助于控制池 features 的更多选项,请参阅相应连接池 implementations 的产品文档。

DBCP configuration:

<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 configuration:

<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"/>

19.3.2 DataSourceUtils

DataSourceUtils class 是一个方便且强大的帮助器 class,它提供static方法以在必要时从 JNDI 获取连接并关闭连接。它支持 thread-bound 连接,例如,DataSourceTransactionManager

19.3.3 SmartDataSource

SmartDataSource接口应该由可以提供与关系数据库的连接的 classes 实现。它扩展了DataSource接口以允许 classes 使用它来查询在给定操作之后是否应该关闭连接。当您知道将重用连接时,此用法很有效。

19.3.4 AbstractDataSource

AbstractDataSource是 Spring DataSource __mplement 的一个abstract base class,它实现所有DataSource __mplementations common 的 code。如果您正在编写自己的DataSource implementation,则扩展AbstractDataSource class。

19.3.5 SingleConnectionDataSource

SingleConnectionDataSource class 是SmartDataSource接口的 implementation,它包装了每次使用后未关闭的单个Connection。显然,这不是 multi-threading 能力。

如果任何 client code calls close假设池化连接,就像使用持久性工具时一样,将suppressClose property 设置为true。此设置返回包装物理连接的 close-suppressing 代理。请注意,您将无法再将其强制转换为原生 Oracle Connection之类。

这主要是测试 class。例如,它可以在一个简单的 JNDI 环境中轻松测试 application 服务器外部的 code。与DriverManagerDataSource相反,它在所有 time 中重用相同的连接,避免过度创建物理连接。

19.3.6 DriverManagerDataSource

DriverManagerDataSource class 是标准DataSource接口的 implementation,它通过 bean properties 配置普通 JDBC 驱动程序,并且每 time 返回一个新的Connection

此 implementation 对于 Java EE 容器外的测试和 stand-alone 环境非常有用,可以作为 Spring IoC 容器中的DataSource bean,也可以与简单的 JNDI 环境结合使用。 Pool-assuming Connection.close() calls 将简单地关闭连接,因此任何DataSource -aware 持久性 code 都应该有效。但是,使用 JavaBean-style 连接池(如commons-dbcp)非常简单,即使在测试环境中也是如此,因此在DriverManagerDataSource上使用这样的连接池几乎总是可取的。

19.3.7 TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy是目标DataSource的代理,它包装目标DataSource以添加 Spring-managed transactions 的意识。在这方面,它类似于 Java EE 服务器提供的 transactional JNDI DataSource

除非已经存在必须调用并传递标准 JDBC DataSource接口 implementation 的 code,否则很少使用此 class。在这种情况下,仍然可以使这个 code 可用,并且在同一 time 时,此 code 参与 Spring managed transactions。通常最好使用资源 management 的更高 level 抽象编写自己的新 code,例如JdbcTemplateDataSourceUtils

(更多 details.)请参阅TransactionAwareDataSourceProxy javadocs

19.3.8 DataSourceTransactionManager

DataSourceTransactionManager class 是单个 JDBC 数据源的PlatformTransactionManager implementation。它将 JDBC 连接从指定的数据源绑定到当前正在执行的线程,可能允许每个数据源一个线程连接。

Application code 需要通过DataSourceUtils.getConnection(DataSource)而不是 Java EE 的标准DataSource.getConnection来检索 JDBC 连接。它会抛出未经检查的org.springframework.dao exceptions 而不是选中SQLExceptions。所有 framework classes(如JdbcTemplate)都隐式使用此策略。如果不与 transaction manager 一起使用,查找策略的行为与 common 一样 - 它可以在任何情况下使用。

DataSourceTransactionManager class 支持自定义隔离级别,以及作为适当的 JDBC 语句查询超时应用的超时。要支持后者,application code 必须使用JdbcTemplate或为每个创建的语句调用DataSourceUtils.applyTransactionTimeout(..)方法。

在单个资源的情况下,可以使用 implementation 而不是JtaTransactionManager,因为它不需要容器支持 JTA。如果您坚持所需的连接查找 pattern,则在两者之间切换只是 configuration 的问题。 JTA 不支持自定义隔离级别!

19.3.9 NativeJdbcExtractor

有时,您需要访问与标准 JDBC API 不同的供应商特定 JDBC 方法。如果您在 application 服务器中运行或使用DataSource包装ConnectionStatementResultSet objects 并使用自己的 wrapper objects,则可能会出现问题。要访问本机 objects,您可以使用NativeJdbcExtractor配置JdbcTemplateOracleLobHandler

NativeJdbcExtractor有各种各样的风格来匹配你的执行环境:

  • SimpleNativeJdbcExtractor

  • C3P0NativeJdbcExtractor

  • CommonsDbcpNativeJdbcExtractor

  • JBossNativeJdbcExtractor

  • WebLogicNativeJdbcExtractor

  • WebSphereNativeJdbcExtractor

  • XAPoolNativeJdbcExtractor

通常SimpleNativeJdbcExtractor足以在大多数环境中展开Connection object。有关更多详细信息,请参阅 javadocs。

19.4 JDBC 批处理操作

如果将多个 calls 批处理到同一个预准备语句,则大多数 JDBC 驱动程序都会提供改进的 performance。通过将更新分组到批次中,可以限制到数据库的往返次数。

19.4.1 使用 JdbcTemplate 进行基本批处理操作

您可以通过实现特殊接口BatchPreparedStatementSetter的两个方法来完成JdbcTemplate批处理,并将其作为batchUpdate方法调用中的第二个参数传递。使用getBatchSize方法提供当前批次的大小。使用setValues方法设置预准备语句的参数值。此方法将被称为您在getBatchSize调用中指定的次数。以下 example 根据列表中的条目更新 actor table。整个列表用作此 example 中的批处理:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

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

    public int[] batchUpdate(final List<Actor> actors) {
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                new BatchPreparedStatementSetter() {
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, actors.get(i).getFirstName());
                        ps.setString(2, actors.get(i).getLastName());
                        ps.setLong(3, actors.get(i).getId().longValue());
                    }
                    public int getBatchSize() {
                        return actors.size();
                    }
                });
    }

    // ... additional methods
}

如果您正在处理更新流或从文件读取,那么您可能具有首选批量大小,但最后一批可能没有该数量的条目。在这种情况下,您可以使用InterruptibleBatchPreparedStatementSetter接口,它允许您在输入源耗尽后中断批处理。 isBatchExhausted方法允许您发出批次结束的信号。

19.4.2 使用 objects 列表进行批处理操作

JdbcTemplateNamedParameterJdbcTemplate都提供了另一种提供批量更新的方法。您可以将调用中的所有参数值作为列表提供,而不是实现特殊的批处理接口。 framework 循环遍历这些值并使用内部预处理语句 setter。 API 会根据您是否使用命名参数而有所不同。对于命名参数,您提供了SqlParameterSource的 array,该批次的每个成员都有一个条目。您可以使用SqlParameterSourceUtils.createBatch便捷方法创建此 array,传入 bean-style objects 的 array(使用与参数对应的 getter 方法)and/or String-keyed Maps(包含相应参数作为值)。

此 example 显示使用命名参数的批量更新:

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.toArray()));
    }

    // ... additional methods
}

对于使用经典“?”的 SQL 语句在占位符中,传入包含带有更新值的 object array 的列表。此 object array 必须在 SQL 语句中为每个占位符分配一个条目,并且它们必须与 SQL 语句中定义的顺序相同。

使用经典 JDBC 的相同 example“?”占位符:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

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

    public int[] batchUpdate(final List<Actor> actors) {
        List<Object[]> batch = new ArrayList<Object[]>();
        for (Actor actor : actors) {
            Object[] values = new Object[] {
                    actor.getFirstName(), actor.getLastName(), actor.getId()};
            batch.add(values);
        }
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                batch);
    }

    // ... additional methods
}

所有上述批量更新方法_return int array,其中包含每个批次条目的受影响行数。 JDBC 驱动程序报告此计数。如果计数不可用,则 JDBC 驱动程序返回-2 value。

19.4.3 多批次的批处理操作

批量更新的最后一个示例处理的批量非常大,您希望将它们分成几个较小的批次。你当然可以通过对batchUpdate方法进行多次 calls 来实现上述方法,但现在有一个更方便的方法。除了 SQL 语句之外,此方法还包含一个包含参数的 objects 集合,每个批处理要进行的更新次数以及ParameterizedPreparedStatementSetter来设置预处理语句的参数值。 framework 循环提供的值并将 update calls 分解为指定大小的批处理。

此 example 显示批量更新为 100 的批量更新:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

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

    public int[][] batchUpdate(final Collection<Actor> actors) {
        int[][] updateCounts = jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                actors,
                100,
                new ParameterizedPreparedStatementSetter<Actor>() {
                    public void setValues(PreparedStatement ps, Actor argument) throws SQLException {
                        ps.setString(1, argument.getFirstName());
                        ps.setString(2, argument.getLastName());
                        ps.setLong(3, argument.getId().longValue());
                    }
                });
        return updateCounts;
    }

    // ... additional methods
}

此调用的批处理更新方法返回 int 数组的 array,其中包含每个批处理的 array 条目,每个更新的受影响行数为 array。 top level array 的长度表示执行的批次数,第二 level array 的长度表示该批次中的更新数。每个批次中的更新数量应该是为所有批次提供的批量大小,但最后一个批次可能更少,具体取决于提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的更新计数。如果计数不可用,则 JDBC 驱动程序返回-2 value。

19.5 使用 SimpleJdbc classes 简化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall classes 通过利用可通过 JDBC 驱动程序检索的数据库元数据来提供简化的 configuration。这意味着预先配置较少,但如果您希望提供 code 中的所有详细信息,则可以覆盖或关闭元数据处理。

19.5.1 使用 SimpleJdbcInsert 插入数据

让我们从使用最少量的 configuration 选项查看SimpleJdbcInsert class 开始。您应该在数据访问层的初始化方法中实例化SimpleJdbcInsert。对于此 example,初始化方法是setDataSource方法。你不需要继承SimpleJdbcInsert class;只需创建一个新实例并使用withTableName方法设置 table name。 __Conlass 的 Configuration 方法遵循返回SimpleJdbcInsert实例的“fluid”样式,允许您链接所有 configuration 方法。这个 example 只使用一个 configuration 方法;稍后您将看到多个示例。

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(3);
        parameters.put("id", actor.getId());
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        insertActor.execute(parameters);
    }

    // ... additional methods
}

这里使用的 execute 方法将 plain java.utils.Map作为唯一参数。这里需要注意的重要一点是,用于 Map 的键必须匹配数据库中定义的 table 的列名。这是因为我们读取了 order 中的元数据来构造实际的 insert 语句。

19.5.2 使用 SimpleJdbcInsert 检索 auto-generated 键

此 example 使用与前面相同的 insert,但不是传入 id,而是检索 auto-generated key 并在新的 Actor object 上设置它。创建SimpleJdbcInsert时,除了指定 table name 之外,还可以使用usingGeneratedKeyColumns方法指定生成的 key 列的 name。

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

通过第二种方法执行 insert 时的主要区别在于您不将 id 添加到 Map 并调用executeAndReturnKey方法。这将返回一个java.lang.Number object,您可以使用该对象创建我们的域 class 中使用的数字类型的实例。您不能依赖所有数据库来返回特定的 Java class; java.lang.Number是您可以信赖的 base class。如果您有多个 auto-generated 列,或者生成的值是 non-numeric,那么您可以使用从executeAndReturnKeyHolder方法返回的KeyHolder

19.5.3 指定 SimpleJdbcInsert 的列

您可以通过使用usingColumns方法指定列名列表来限制 insert 的列:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingColumns("first_name", "last_name")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

insert 的执行与依赖元数据确定要使用的列相同。

19.5.4 使用 SqlParameterSource 提供参数值

使用Map提供参数值工作正常,但它不是最方便使用的 class。 Spring 提供了几个SqlParameterSource接口的 implementation,可以使用 instead.The 第一个是BeanPropertySqlParameterSource,如果你有一个包含你的值的 JavaBean-compliant class,这是一个非常方便的 class。它将使用相应的 getter 方法来提取参数值。这是一个 example:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

另一种选择是类似于 Map 的MapSqlParameterSource,但提供了一种可以链接的更方便的addValue方法。

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        SqlParameterSource parameters = new MapSqlParameterSource()
                .addValue("first_name", actor.getFirstName())
                .addValue("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

如您所见,configuration 是相同的;只有执行 code 必须更改为使用这些替代输入 classes。

19.5.5 使用 SimpleJdbcCall 调用存储过程

SimpleJdbcCall class 利用数据库中的元数据来查找inout参数的名称,这样您就不必显式声明它们。如果您愿意,可以声明参数,或者如果您有ARRAYSTRUCT之类的参数没有自动映射到 Java class。第一个 example 显示了一个简单的过程,该过程仅从 MySQL 数据库返回VARCHARDATE格式的标量值。 example 过程读取指定的 actor 条目,并以out参数的形式返回first_namelast_namebirth_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参数包含您正在查找的 actor 的idout参数 return 从 table 读取的数据。

SimpleJdbcCall的声明方式与SimpleJdbcInsert类似。您应该在数据访问层的初始化方法中实例化和配置 class。与 StoredProcedure class 相比,您不必创建子类,也不必声明可在数据库元数据中查找的参数。以下是使用上述存储过程的 SimpleJdbcCall configuration 的示例。除了DataSource之外,唯一的 configuration 选项是存储过程的 name。

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.procReadActor = new SimpleJdbcCall(dataSource)
                .withProcedureName("read_actor");
    }

    public Actor readActor(Long id) {
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("in_id", id);
        Map out = procReadActor.execute(in);
        Actor actor = new Actor();
        actor.setId(id);
        actor.setFirstName((String) out.get("out_first_name"));
        actor.setLastName((String) out.get("out_last_name"));
        actor.setBirthDate((Date) out.get("out_birth_date"));
        return actor;
    }

    // ... additional methods
}

为执行调用而编写的 code 涉及 creating 包含 IN 参数的SqlParameterSource。将输入 value 提供的 name 与存储过程中声明的参数 name 的 name 匹配很重要。该案例不必 match,因为您使用元数据来确定应如何在存储过程中引用数据库对象。存储过程的源中指定的内容不一定是存储在数据库中的方式。某些数据库将名称转换为全部大写,而其他数据库使用小写或使用指定的大小写。

execute方法接受 IN 参数并返回一个 Map,其中包含由存储过程中指定的 name 键入的任何out参数。在这种情况下,它们是out_first_name, out_last_nameout_birth_date

execute方法的最后一部分创建一个 Actor 实例,用于 return 检索的数据。同样,使用out参数的名称非常重要,因为它们在存储过程中声明。此外,结果 map 中存储的out参数名称中的情况与数据库中out参数名称的情况相匹配,这可能因数据库而异。为了使 code 更具可移植性,您应该进行 case-insensitive 查找或指示 Spring 使用LinkedCaseInsensitiveMap。要执行后者,您可以创建自己的JdbcTemplate并将setResultsMapCaseInsensitive property 设置为true。然后将这个自定义的JdbcTemplate实例传递给SimpleJdbcCall的构造函数。以下是此 configuration 的示例:

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参数名称的情况下发生冲突。

19.5.6 显式声明用于 SimpleJdbcCall 的参数

您已经了解了如何根据元数据推导出参数,但如果您愿意,可以明确声明。你可以通过使用declareParameters方法创建和配置SimpleJdbcCall来实现这一点,该方法采用可变数量的SqlParameter objects 作为输入。有关如何定义SqlParameter的详细信息,请参阅下一节。

如果您使用的数据库不是 Spring-supported 数据库,则必须使用显式声明。目前,Spring 支持以下数据库的存储过程 calls 的元数据查找:Apache Derby,DB2,MySQL,Microsoft SQL Server,Oracle 和 Sybase。我们还支持 MySQL,Microsoft SQL Server 和 Oracle 的存储函数的元数据查找。

您可以选择明确声明一个,一些或所有参数。在未明确声明参数的情况下,仍会使用参数元数据。要绕过对潜在参数的元数据查找的所有处理并仅使用声明的参数,请将方法withoutProcedureColumnMetaDataAccess作为声明的一部分进行调用。假设您为数据库 function 声明了两个或更多不同的调用签名。在这种情况下,您调用useInParameterNames指定要包含给定签名的 IN 参数名称列表。

以下 example 使用前面的 example 中的信息显示完全声明的过程调用。

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
}

两个例子的执行和结束结果是相同的;这个明确指定所有细节而不是依赖元数据。

19.5.7 如何定义 SqlParameters

要定义 SimpleJdbc classes 以及第 19.6 节,“将 JDBC 操作建模为 Java objects”中涵盖的 RDBMS 操作 classes 的参数,可以使用SqlParameter或其子类之一。您通常在构造函数中指定参数 name 和 SQL 类型。使用java.sql.Types常量指定 SQL 类型。我们已经看过如下声明:

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

SqlParameter的第一个 line 声明一个 IN 参数。 IN 参数既可用于存储过程 calls,也可用于使用SqlQuery及其子类在下一节中介绍的查询。

带有SqlOutParameter的第二个 line 声明了一个out参数,用于存储过程调用。还有一个SqlInOutParameter for InOut参数,这些参数为过程提供IN value,并且 return value。

只有声明为SqlParameterSqlInOutParameter的参数才会用于提供输入值。这与StoredProcedure class 不同,后者由于向后兼容性原因允许为声明为SqlOutParameter的参数提供输入值。

对于 IN 参数,除了 name 和 SQL 类型之外,还可以为数字数据指定比例,或为自定义数据库类型指定类型 name。对于out参数,您可以提供RowMapper来处理从REF游标返回的行的映射。另一个选项是指定SqlReturnType,它提供了定义 return 值的自定义处理的机会。

19.5.8 使用 SimpleJdbcCall 调用存储的 function

以与调用存储过程几乎相同的方式调用存储的 function,除了提供 function name 而不是 procedure name。您使用withFunctionName方法作为 configuration 的一部分来指示我们要调用 function,并生成 function 调用的相应 string。一个专门的执行调用,executeFunction,用于执行 function,它返回 function return value 作为指定类型的 object,这意味着您不必从结果 map 中检索 return value。名为executeObject的类似便捷方法也可用于只有一个out参数的存储过程。以下 example 基于一个名为get_actor_name的存储 function,它返回一个 actor 的完整 name。以下是此 function 的 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;

要调用此 function,我们再次在初始化方法中创建SimpleJdbcCall

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcCall funcGetActorName;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
                .withFunctionName("get_actor_name");
    }

    public String getActorName(Long id) {
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("in_id", id);
        String name = funcGetActorName.executeFunction(String.class, in);
        return name;
    }

    // ... additional methods
}

使用的 execute 方法从 function 调用返回包含 return value 的String

19.5.9 从 SimpleJdbcCall 返回 ResultSet/REF 光标

调用存储过程或返回结果集的 function 有点棘手。某些数据库在 JDBC 结果处理期间返回结果_set,而其他数据库则需要显式注册特定类型的out参数。这两种方法都需要额外的处理来循环结果集并处理返回的行。使用SimpleJdbcCall,您可以使用returningResultSet方法并声明RowMapper implementation 用于特定参数。如果在结果处理期间返回结果集,则不会定义任何名称,因此返回的结果必须__chch 声明RowMapper __mplementations 的 order。指定的 name 仍用于在 execute 语句返回的结果 map 中存储已处理的结果列表。

下一个 example 使用一个不带 IN 参数的存储过程,并返回 t_actor table 中的所有行。以下是此过程的 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。因为要 map 的 class 遵循 JavaBean 规则,所以可以使用通过在newInstance方法中将所需的 class 传递给 map 而创建的BeanPropertyRowMapper

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadAllActors;

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

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

    // ... additional methods
}

执行调用传递一个空的 Map,因为此调用不接受任何参数。然后从结果 map 中检索 Actors 列表并返回给调用者。

19.6 将 JDBC 操作建模为 Java objects

org.springframework.jdbc.object包中包含 classes,允许您以更 object-oriented 的方式访问数据库。作为示例,您可以执行查询并将结果作为包含 business objects 的列表返回,其中关系列数据映射到 business object 的 properties。您还可以执行存储过程和 run update,delete 和 insert statements。

许多 Spring 开发人员认为下面描述的各种 RDBMS 操作 classes(使用StoredProcedure class 的 exception)通常可以用直JdbcTemplate calls 替换。编写 DAO 方法通常更简单,只需直接在JdbcTemplate上调用方法(而不是将查询封装为 full-blown class)。

但是,如果从使用 RDBMS 操作 classes 获得可测量的 value,继续使用这些 classes。

19.6.1 SqlQuery

SqlQuery是一个可重用的线程安全 class,它封装了一个 SQL 查询。子类必须实现newRowMapper(..)方法以提供RowMapper实例,该实例可以通过迭代在执行查询期间创建的ResultSet而获得的每行创建一个 object。 SqlQuery class 很少直接使用,因为MappingSqlQuery子类为将行映射到 Java classes 提供了更方便的 implementation。延伸SqlQuery的其他 implementation 是MappingSqlQueryWithParametersUpdatableSqlQuery

19.6.2 MappingSqlQuery

MappingSqlQuery是一个可重用的查询,其中具体的子类必须实现 abstract mapRow(..)方法,以将提供的ResultSet的每一行转换为指定类型的 object。以下 example 显示了一个自定义查询,该查询将t_actor关系中的数据映射到Actor class 的实例。

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

}

class 使用Actor类型扩展MappingSqlQuery参数化。此客户查询的构造函数将DataSource作为唯一参数。在此构造函数中,使用DataSource和应执行的 SQL 调用超类上的构造函数以检索此查询的行。此 SQL 将用于创建PreparedStatement,因此它可能包含在执行期间传递的任何参数的占位符。您必须使用传入SqlParameterdeclareParameter方法声明每个参数。 SqlParameter采用 name 和java.sql.Types中定义的 JDBC 类型。定义所有参数后,调用compile()方法,以便可以准备语句并稍后执行。在编译之后,这个 class 是 thread-safe,因为在初始化 DAO 时创建这些实例的 long,它们可以作为实例变量保存并重用。

private ActorMappingQuery actorMappingQuery;

@Autowired
public void setDataSource(DataSource dataSource) {
    this.actorMappingQuery = new ActorMappingQuery(dataSource);
}

public Customer getCustomer(Long id) {
    return actorMappingQuery.findObject(id);
}

此 example 中的方法检索具有作为唯一参数传入的 id 的 customer。由于我们只想要返回一个 object,我们只需调用带有 id 作为参数的方便方法findObject。如果我们有一个返回 objects 列表并获取其他参数的查询,那么我们将使用一个执行方法,该方法将 array 参数值作为 varargs 传入。

public List<Actor> searchForActors(int age, String namePattern) {
    List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
    return actors;
}

19.6.3 SqlUpdate

SqlUpdate class 封装了 SQL 更新。与查询一样,更新 object 是可重用的,并且与所有RdbmsOperation classes 一样,更新可以具有参数并在 SQL 中定义。这个 class 提供了许多update(..)方法,类似于查询 objects 的execute(..)方法。 SQLUpdate class 是具体的。例如,它可以是子类,以添加自定义更新方法,如下面的代码段所示,它只是简单地称为execute。但是,您不必为SqlUpdate class 创建子类,因为可以通过设置 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("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);
    }
}

19.6.4 StoredProcedure

StoredProcedure class 是 RDBMS 存储过程的 object 抽象的超类。这个 class 是abstract,它的各种execute(..)方法都有protected访问权限,阻止了通过提供更严格 typing 的子类以外的用法。

继承的sql property 将是 RDBMS 中存储过程的 name。

要为StoredProcedure class 定义参数,请使用SqlParameter或其子类之一。您必须在构造函数中指定参数 name 和 SQL 类型,如以下 code 片段中所示。使用java.sql.Types常量指定 SQL 类型。

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

SqlParameter的第一个 line 声明一个 IN 参数。 IN 参数既可用于存储过程 calls,也可用于使用SqlQuery及其子类在下一节中介绍的查询。

带有SqlOutParameter的第二个 line 声明要在存储过程调用中使用的out参数。还有一个SqlInOutParameter for I nOut参数,这些参数为过程提供in value,并且 return value。

对于i n参数,除了 name 和 SQL 类型之外,还可以为数字数据指定比例,或为自定义数据库类型指定类型 name。对于out参数,您可以提供RowMapper来处理从 REF 游标返回的行的映射。另一个选项是指定SqlReturnType,使您可以定义 return 值的自定义处理。

这是一个简单 DAO 的示例,它使用StoredProcedure来调用 function,它随任何 Oracle 数据库一起提供。要使用存储过程功能,您必须创建一个扩展StoredProcedure的 class。在这个 example 中,StoredProcedure class 是一个内部 class,但如果需要重用StoredProcedure,则将其声明为 top-level class。此 example 没有输入参数,但使用 class SqlOutParameter将输出参数声明为 date 类型。 execute()方法执行该过程并从结果Map中提取返回的 date。结果Map为每个声明的输出参数都有一个条目,在这种情况下只有一个,使用参数 name 作为 key。

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

}

以下的示例有两个输出参数(在本例中为 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 implementation 实例的;这是重用现有功能的一种非常方便和强大的方法。下面提供了两个RowMapper __mplementations 的 code。

对于提供的ResultSet中的每一行,TitleMapper class maps ResultSetTitle domain object:

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;

public final class TitleMapper implements RowMapper<Title> {

    public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
        Title title = new Title();
        title.setId(rs.getLong("id"));
        title.setName(rs.getString("name"));
        return title;
    }
}

对于提供的ResultSet中的每一行,GenreMapper class maps 到Genre域 object。

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 的定义中具有一个或多个输入参数的存储过程,您可以 code 一个强类型execute(..)方法,该方法将委托给超类'无类型execute(Map parameters)方法(具有protected访问权限);例如:

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

19.7 参数和数据 value 处理的常见问题

Spring Framework JDBC 提供的不同方法中存在参数和数据值的常见问题。

19.7.1 提供参数的 SQL 类型信息

通常 Spring 根据传入的参数类型确定参数的 SQL 类型。可以显式提供设置参数值时要使用的 SQL 类型。有时需要正确设置 NULL 值。

您可以通过多种方式提供 SQL 类型信息:

  • JdbcTemplate的许多更新和查询方法以int array 的形式获取附加参数。此 array 用于使用java.sql.Types class 中的常量值指示相应参数的 SQL 类型。为每个参数提供一个条目。

  • 您可以使用SqlParameterValue class 来包装参数 value,它需要为每个 value 添加一个新的实例,并在构造函数中传入 SQL 类型和参数 value。您还可以为数值提供可选的缩放参数。

  • 对于使用命名参数的方法,请使用SqlParameterSource classes BeanPropertySqlParameterSourceMapSqlParameterSource。它们都具有为任何命名参数值注册 SQL 类型的方法。

19.7.2 处理 BLOB 和 CLOB objects

您可以存储数据库中的图像,其他二进制数据和大块文本。这些大 objects 被称为 BLOB(二进制大 OBject)用于二进制数据和 CLOB(字符大 OBject)用于字符数据。在 Spring 中,您可以直接使用JdbcTemplate处理这些大型 object,也可以使用 RDBMS Objects 和SimpleJdbc classes 提供的更高抽象。所有这些方法都使用LobHandler接口的 implementation 来实现 LOB(Large OBject)数据的实际管理。 LobHandler通过getLobCreator方法提供对LobCreator class 的访问,用于创建要插入的新 LOB objects。

LobCreator/LobHandler为 LOB 输入和输出提供以下支持:

  • BLOB

  • byte[] - getBlobAsBytessetBlobAsBytes

  • InputStream - getBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

  • String - getClobAsStringsetClobAsString

  • InputStream - getClobAsAsciiStreamsetClobAsAsciiStream

  • Reader - getClobAsCharacterStreamsetClobAsCharacterStream

下一个 example 显示了如何创建和插入 BLOB。稍后您将看到如何从数据库中读取它。

此 example 使用AbstractLobCreatingPreparedStatementCallbackAbstractLobCreatingPreparedStatementCallback的 implementation。它实现了一个方法setValues。此方法提供了一个LobCreator,用于在 SQL insert 语句中设置 LOB 列的值。

对于这个例子,我们假设有一个变量lobHandler,它已经被设置为DefaultLobHandler的一个实例。您通常通过依赖注入设置此 value。

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

传入lobHandler,在此 example 中是一个普通的DefaultLobHandler

使用方法setClobAsCharacterStream,传入 CLOB 的内容。

使用方法setBlobAsBinaryStream,传入 BLOB 的内容。

如果从DefaultLobHandler.getLobCreator()返回的LobCreator上调用setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream方法,则可以选择为contentLength参数指定负值。如果指定的内容长度为负,则DefaultLobHandler将使用 set-stream 方法的 JDBC 4.0 变体而不使用 length 参数;否则,它会将指定的长度传递给驱动程序。

请参阅正在使用的 JDBC 驱动程序的文档,以验证是否支持流式传输 LOB 而不提供内容长度。

现在是 time 从数据库中读取 LOB 数据。同样,您使用JdbcTemplate与相同的实例变量lobHandler和 reference 到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; } });

使用方法getClobAsString,检索 CLOB 的内容。

使用方法getBlobAsBytes,检索 BLOB 的内容。

19.7.3 传入 IN 子句的 lists 值

SQL 标准允许基于包含变量值列表的表达式来选择行。典型的 example 将是select * from T_ACTOR where id in (1, 2, 3)。 JDBC 标准不直接支持准备的 statements 这个变量列表;您不能声明可变数量的占位符。您需要准备好所需占位符数量的多种变体,或者一旦知道需要多少占位符,就需要动态生成 SQL string。 NamedParameterJdbcTemplateJdbcTemplate中提供的命名参数支持采用后一种方法。将值作为原始 objects 的java.util.List传递。此列表将用于插入所需的占位符,并在语句执行期间传入值。

传递许多值时要小心。 JDBC 标准不保证您可以为in表达式列表使用 100 个以上的值。各种数据库超过此数量,但它们通常对允许的值有多少硬性限制。 Oracle 的限制是 1000。

除了 value 列表中的原始值之外,您还可以创建java.util.List的 object 数组。此列表将支持为in子句定义的多个表达式,例如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'\))。这当然要求您的数据库支持此语法。

19.7.4 处理存储过程 calls 的复杂类型

调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,Spring 提供SqlReturnType用于在从存储过程调用返回它们时处理它们,并在它们作为参数传入存储过程时SqlTypeValue

这是返回用户声明类型ITEM_TYPE的 Oracle STRUCT object 的 value 的示例。 SqlReturnType接口有一个必须实现的名为getTypeValue的方法。此接口用作SqlOutParameter声明的一部分。

public class TestItemStoredProcedure extends StoredProcedure {

    public TestItemStoredProcedure(DataSource dataSource) {
        ...
        declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
            new SqlReturnType() {
                public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String typeName) throws SQLException {
                    STRUCT struct = (STRUCT) cs.getObject(colIndx);
                    Object[] attr = struct.getAttributes();
                    TestItem item = new TestItem();
                    item.setId(((Number) attr[0]).longValue());
                    item.setDescription((String) attr[1]);
                    item.setExpirationDate((java.util.Date) attr[2]);
                    return item;
                }
            }));
        ...
    }

使用SqlTypeValue将 Java object 的 value(如TestItem)传入存储过程。 SqlTypeValue接口有一个名为createTypeValue的方法,您必须实现该方法。传入 active 连接,您可以使用它创建 database-specific objects,例如StructDescriptor s,如下面的 example 或ArrayDescriptors 所示。

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的另一个用途是将 array 值传递给 Oracle 存储过程。 Oracle 有自己的内部ARRAY class,在这种情况下必须使用它,您可以使用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;
    }
};

19.8 嵌入式数据库支持

org.springframework.jdbc.datasource.embedded包提供对嵌入式 Java 数据库引擎的支持。本机提供对HSQLH2Derby的支持。您还可以使用可扩展 API 来插入新的嵌入式数据库类型和DataSource __mplement。

19.8.1 为什么要使用嵌入式数据库?

嵌入式数据库在项目的开发阶段非常有用,因为它具有轻量级特性。优点包括易于配置,快速启动 time,可测试性以及在开发过程中快速发展 SQL 的能力。

19.8.2 使用 Spring XML 创建嵌入式数据库

如果要在 Spring ApplicationContext中将嵌入式数据库实例公开为 bean,请在spring-jdbc命名空间中使用embedded-database标记:

<jdbc:embedded-database id="dataSource" generate-name="true">
    <jdbc:script location="classpath:schema.sql"/>
    <jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>

前面的 configuration 创建一个嵌入式 HSQL 数据库,该数据库填充了来自 classpath 根目录中的schema.sqltest-data.sql资源的 SQL。此外,作为最佳实践,将为嵌入式数据库分配唯一生成的 name。嵌入式数据库作为类型的 bean 可用于 Spring 容器,然后可以根据需要将其注入数据访问 objects。

19.8.3 以编程方式创建嵌入式数据库

EmbeddedDatabaseBuilder class 提供了一个 fluent API,用于以编程方式构建嵌入式数据库。当您需要在独立环境中或在以下 example 中的独立 integration 测试中创建嵌入式数据库时,请使用此选项。

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()

有关所有支持选项的更多详细信息,请参阅 Javadoc 获取EmbeddedDatabaseBuilder

EmbeddedDatabaseBuilder也可用于使用 Java Config 创建嵌入式数据库,如下面的示例所示。

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

19.8.4 选择嵌入式数据库类型

使用 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)方法。

19.8.5 使用嵌入式数据库测试数据访问逻辑

嵌入式数据库提供了一种测试数据访问 code 的轻量级方法。以下是使用嵌入式数据库的数据访问 integration 测试模板。当嵌入式数据库不需要跨测试 classes 重用时,使用这样的模板对于 one-offs 非常有用。但是,如果您希望创建在测试套件中共享的嵌入式数据库,请考虑使用Spring TestContext Framework并将嵌入式数据库配置为 Spring ApplicationContext中的 bean,如部分 19.8.2,“使用 Spring XML 创建嵌入式数据库”第 19.8.3 节,“以编程方式创建嵌入式数据库”中所述。

public class DataAccessIntegrationTestTemplate {

    private EmbeddedDatabase db;

    @Before
    public void setUp() {
        // creates an HSQL in-memory database populated from default scripts
        // classpath:schema.sql and classpath:data.sql
        db = new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .addDefaultScripts()
                .build();
    }

    @Test
    public void testDataAccess() {
        JdbcTemplate template = new JdbcTemplate(db);
        template.query( /* ... */ );
    }

    @After
    public void tearDown() {
        db.shutdown();
    }

}

19.8.6 为嵌入式数据库生成唯一名称

如果测试套件无意中尝试重新创建同一数据库的其他实例,则开发团队经常会遇到嵌入式数据库的错误。如果 XML configuration 文件或@Configuration class 负责创建嵌入式数据库,并且相应的 configuration 随后在同一测试套件中的多个测试场景中重复使用(i.e.,在同一个 JVM process 中),这很容易发生 - 对于 example ,integration 测试针对嵌入式数据库,其ApplicationContext configuration 仅在 bean 定义 profiles 为 active 时有所不同。

导致此类错误的根本原因是 Spring 的EmbeddedDatabaseFactory(由<jdbc:embedded-database> XML 名称空间元素和EmbeddedDatabaseBuilder用于 Java Config)在内部使用,如果没有另外指定,则将嵌入数据库的 name 设置为"testdb"。对于<jdbc:embedded-database>的情况,嵌入式数据库通常被赋予 name 等于 bean 的id(i.e.,通常类似于"dataSource")。因此,后续创建嵌入式数据库的尝试不会产生新的数据库。相反,将重用相同的 JDBC 连接 URL,并且尝试创建新的嵌入式数据库实际上将指向从相同的 configuration 创建的现有嵌入式数据库。

为了解决这个 common 问题 Spring Framework 4.2 提供了为嵌入式数据库生成唯一名称的支持。要启用生成的名称,请使用以下选项之一。

  • EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()

  • EmbeddedDatabaseBuilder.generateUniqueName()

  • <jdbc:embedded-database generate-name="true" … >

19.8.7 扩展嵌入式数据库支持

Spring JDBC 嵌入式数据库支持可以通过两种方式扩展:

  • 实现EmbeddedDatabaseConfigurer以支持新的嵌入式数据库类型。

  • 实现DataSourceFactory以支持新的DataSource implementation,例如用于管理嵌入式数据库连接的连接池。

我们鼓励您在jira.spring.io向 Spring 社区贡献 extensions。

19.9 初始化 DataSource

org.springframework.jdbc.datasource.init包提供对初始化现有DataSource的支持。嵌入式数据库支持提供了一个选项,用于为 application 创建和初始化DataSource,但有时您需要在某个服务器上初始化一个实例 running。

19.9.1 使用 Spring XML 初始化数据库

如果要初始化数据库并且可以为DataSource bean 提供 reference,请使用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>

上面的 example 执行针对数据库指定的两个脚本:第一个脚本创建 schema,第二个脚本使用测试数据集填充表。脚本位置也可以是带有通配符样式的通配符的模式,用于 Spring(e.g. classpath*:/com/foo/**/sql/*-data.sql)中的资源。如果使用 pattern,脚本将在其 URL 或文件名的词法 order 中执行。

数据库初始化程序的默认行为是无条件地执行提供的脚本。这并不总是您想要的,例如,如果您正在对已经包含测试数据的数据库执行脚本。通过遵循先创建表的 common pattern(如上所示)然后插入数据来减少意外删除数据的可能性 - 如果表已经存在,则第一个 step 将失败。

但是,为了更好地控制现有数据的创建和删除,XML 命名空间提供了一些其他选项。第一个是 flag 来打开和关闭初始化。这可以根据环境设置(e.g. 从系统 properties 或环境 bean 拉取 boolean value),对于 example:

<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 statements 因此会失败。因此失败的 SQL DROP statements 将被忽略,但其他失败将导致 exception。如果您的 SQL 方言不支持DROP … IF EXISTS(或类似),但您希望在 re-creating 之前无条件地删除所有测试数据,这将非常有用。在这种情况下,第一个脚本通常是一组DROP statements,后跟一组CREATE statements。

ignore-failures选项可以设置为NONE(默认值),DROPS(忽略失败的丢弃)或ALL(忽略所有失败)。

如果脚本中根本不存在;字符,则每个语句应由;或新 line 分隔。您可以通过脚本控制全局或脚本,例如:

<jdbc:initialize-database data-source="dataSource" separator="@@">
    <jdbc:script location="classpath:com/foo/sql/db-schema.sql" separator=";"/>
    <jdbc:script location="classpath:com/foo/sql/db-test-data-1.sql"/>
    <jdbc:script location="classpath:com/foo/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>

在这个 example 中,两个test-data脚本使用@@作为语句分隔符,只有db-schema.sql使用;。此 configuration 指定默认分隔符为@@并覆盖db-schema脚本的默认值。

如果您需要比从 XML 命名空间获得的更多控制,您可以直接使用DataSourceInitializer并将其定义为 application 中的 component。

初始化依赖于数据库的其他组件

一个大的 class 的 applications 只能使用数据库初始化程序而没有进一步的复杂化:那些在 Spring context 启动之后才使用数据库的程序。如果您的 application 不是其中之一,那么您可能需要阅读本节的 rest。

数据库初始化程序依赖于DataSource实例并执行其初始化回调中提供的脚本(类似于 XML bean 定义中的init-method,component 中的@PostConstruct方法或实现InitializingBean的 component 中的afterPropertiesSet()方法)。如果其他 beans 依赖于相同的数据源并且还在初始化回调中使用数据源,则可能存在问题,因为数据尚未初始化。 common example 是一个缓存,它急切地初始化并在 application 启动时从数据库加载数据。

要解决此问题,您有两种选择:将缓存初始化策略更改为稍后阶段,或者确保首先初始化数据库初始化程序。

如果 application 在您的控件中,则第一个选项可能很简单,而不是其他选项。关于如何实现这一点的一些建议包括:

  • 在第一次使用时使缓存初始化,这可以改善 application startup time。

  • 让缓存或初始化缓存的单独 component 实现LifecycleSmartLifecycle。当 application context 启动时,如果设置了autoStartup flag,则可以自动启动SmartLifecycle,并且可以通过在封闭的 context 上调用ConfigurableApplicationContext.start()来手动启动Lifecycle

  • 使用 Spring ApplicationEvent或类似的自定义观察器机制来触发缓存初始化。 总是在 context 准备好使用时发布(在所有 beans 初始化之后),所以这通常是一个有用的 hook(这是SmartLifecycle默认工作的方式)。

第二种选择也很简单。关于如何实现这一点的一些建议包括:

  • 依赖 Spring BeanFactory的默认行为,即 beans 在 registration order 中初始化。您可以通过采用

  • DataSource和使用它的业务组件分开,并通过将它们放在单独的ApplicationContext实例中来控制它们的启动 order(e.g. _ parent context 包含DataSource,这个结构在 Spring web applications 中是 common,但可以更普遍地应用。

校对:
Updated at: 5 months ago
18.3. Annotations 用于配置 DAO 或 Repository classesTable of content20. Object 关系映射(ORM)数据访问