19. 使用 JDBC 进行数据访问

19.1 Spring Framework JDBC 简介

下表概述的动作序列可能最好地显示了 Spring Framework JDBC 抽象提供的增值。该表显示了 Spring 将采取哪些操作,以及哪些操作是您(应用程序开发人员)的责任。

表 19.1. Spring JDBC-谁做什么?

ActionSpringYou
定义连接参数。 X
打开连接。X
指定 SQL 语句。 X
声明参数并提供参数值 X
准备并执行该语句。X
设置循环以遍历结果(如果有)。X
进行每次迭代的工作。 X
处理任何异常。X
Handle transactions.X
关闭连接,语句和结果集。X

Spring 框架负责所有可能使 JDBC 成为乏味的 API 的底层细节。

19.1.1 选择用于 JDBC 数据库访问的方法

您可以选择几种方法来构成 JDBC 数据库访问的基础。除了三种形式的 JdbcTemplate 外,新的 SimpleJdbcInsert 和 SimplejdbcCall 方法还优化了数据库元数据,并且 RDBMS Object 样式采用了一种与 JDO Query 设计类似的面向对象的方法。一旦开始使用这些方法之一,您仍然可以混合搭配以包含来自其他方法的功能。所有方法都需要兼容 JDBC 2.0 的驱动程序,某些高级功能需要 JDBC 3.0 驱动程序。

    • JdbcTemplate *是经典的 Spring JDBC 方法,也是最受欢迎的方法。这种“最低级别”的方法以及其他所有方法都在幕后使用了 JdbcTemplate。
    • NamedParameterJdbcTemplate *包装JdbcTemplate来提供命名参数,而不是传统的 JDBC“?”占位符。当您有多个 SQL 语句参数时,此方法可提供更好的文档编制和易用性。
    • SimpleJdbcInsert 和 SimpleJdbcCall *优化数据库元数据以限制必要的配置量。这种方法简化了编码,因此您只需要提供表或过程的名称,并提供与列名称匹配的参数 Map 即可。仅当数据库提供足够的元数据时,此方法才有效。如果数据库不提供此元数据,则必须提供参数的显式配置。
    • RDBMS 对象,包括 MappingSqlQuery,SqlUpdate 和 StoredProcedure *,要求您在数据访问层初始化期间创建可重用且线程安全的对象。此方法以 JDO Query 为模型,其中您定义查询字符串,声明参数并编译查询。完成此操作后,可以传入各种参数值来多次调用 execute 方法。

19.1.2 程序包层次结构

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

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

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

org.springframework.jdbc.object软件包包含将 RDBMS 查询,更新和存储过程表示为线程安全的可重用对象的类。参见第 19.6 节“将 JDBC 操作建模为 Java 对象”。这种方法是由 JDO 建模的,尽管查询返回的对象自然会与数据库断开连接。较高级别的 JDBC 抽象取决于org.springframework.jdbc.core包中的较低级别的抽象。

org.springframework.jdbc.support软件包提供SQLException转换功能和一些 Util 类。 JDBC 处理期间引发的异常将转换为org.springframework.dao包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换的异常均未选中,这使您可以选择捕获可从中恢复的异常,同时允许将其他异常传播到调用方。参见第 19.2.3 节“ SQLExceptionTranslator”

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

19.2.1 JdbcTemplate

JdbcTemplate类是 JDBC 核心软件包中的中心类。它处理资源的创建和释放,这有助于您避免常见的错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务,例如语句创建和执行,而使应用程序代码提供 SQL 并提取结果。 JdbcTemplate类执行 SQL 查询,更新语句和存储过程调用,在ResultSet上执行迭代并提取返回的参数值。它还捕获 JDBC 异常,并将其转换为org.springframework.dao包中定义的通用,信息量更大的异常层次结构。

在代码中使用JdbcTemplate时,只需实现回调接口,即可为它们明确定义 Contract。 PreparedStatementCreator回调接口根据此类提供的Connection创建准备好的语句,并提供 SQL 和任何必要的参数。 CallableStatementCreator接口(创建可调用语句)也是如此。 RowCallbackHandler接口从ResultSet的每一行提取值。

JdbcTemplate可以通过直接使用DataSource引用实例化在 DAO 实现中使用,也可以在 Spring IoC 容器中进行配置并作为 Bean 引用提供给 DAO。

Note

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

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

JdbcTemplate 类用法示例

本节提供了JdbcTemplate类用法的一些示例。这些示例不是JdbcTemplate公开的所有功能的详尽列表;请参阅相关的 javadocs。

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

查询并填充单个域对象:

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

查询和填充多个域对象:

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

如果最后两个代码段确实存在于同一应用程序中,则有必要删除两个RowMapper匿名内部类中存在的重复项,并将它们提取到单个类(通常是static嵌套类)中,然后可以根据需要由 DAO 方法引用。例如,最好编写如下的最后一个代码片段:

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 更新(插入/更新/删除)

您使用update(..)方法执行插入,更新和删除操作。参数值通常以 var args 或对象数组的形式提供。

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 语句。带有回调接口,绑定变量数组等的变体严重地超载了它。

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

下面的示例调用一个简单的存储过程。 covered later是更复杂的存储过程支持。

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

JdbcTemplate 最佳做法

JdbcTemplate类的实例一旦配置便是* threadsafe 。这很重要,因为这意味着您可以配置JdbcTemplate的单个实例,然后安全地将此“共享”引用注入多个 DAO(或存储库)中。 JdbcTemplate是有状态的,因为它维护对DataSource的引用,但是此状态为非*会话状态。

使用JdbcTemplate类(和相关的NamedParameterJdbcTemplate类)的一种常见做法是在 Spring 配置文件中配置DataSource,然后将共享的DataSource bean 依赖注入到 DAO 类中。 JdbcTemplateDataSource的设置器中创建。这导致 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"
    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>

显式配置的替代方法是使用组件扫描和 Comments 支持进行依赖项注入。在这种情况下,您可以使用@RepositoryComments 类(这使其成为组件扫描的候选对象),并使用@AutowiredCommentsDataSource setter 方法。

@Repository
public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

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

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}

相应的 XML 配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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类,并且各种 JDBC 支持的 DAO 类都从该类继承而来,则您的子类将从JdbcDaoSupport类继承一个setDataSource(..)方法。您可以选择是否从此类继承。提供JdbcDaoSupport类只是为了方便。

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

19.2.2 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate类增加了对使用命名参数编程 JDBC 语句的支持,这与仅使用经典占位符('?')参数进行编程的 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(*) from T_ACTOR where first_name = :first_name";

    SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

请注意,在分配给sql变量的值和插入到namedParameters变量(类型MapSqlParameterSource)中的相应值中使用了命名参数符号。

或者,您可以使用基于Map的样式将命名参数及其对应的值传递给NamedParameterJdbcTemplate实例。NamedParameterJdbcOperations公开并由NamedParameterJdbcTemplate类实现的其余方法遵循类似的模式,此处不再赘述。

下面的示例说明基于Map的样式的用法。

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
}

NamedParameterJdbcTemplate相关联(并且存在于同一 Java 包中)的一项不错的功能是SqlParameterSource接口。您已经在前面的代码片段之一(MapSqlParameterSource类)中看到了此接口的实现示例。 SqlParameterSourceNamedParameterJdbcTemplate的命名参数值的来源。 MapSqlParameterSource类是一个非常简单的实现,它只是java.util.Map周围的适配器,其中键是参数名称,值是参数值。

另一个SqlParameterSource实现是BeanPropertySqlParameterSource类。此类包装任意 JavaBean(即,遵循JavaBean 约定的类的实例),并将包装的 JavaBean 的属性用作命名参数值的源。

public class Actor {

    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public Long getId() {
        return this.id;
    }

    // setters omitted...

}
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActors(Actor exampleActor) {

    // notice how the named parameters match the properties of the above 'Actor' class
    String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

    SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

请记住,NamedParameterJdbcTemplate类“包装”了经典的JdbcTemplate模板;如果您需要访问包装的JdbcTemplate实例以访问仅在JdbcTemplate类中提供的功能,则可以使用getJdbcOperations()方法通过JdbcOperations接口访问包装的JdbcTemplate

另请参阅名为“ JdbcTemplate 最佳做法”的部分,以获取有关在应用程序上下文中使用NamedParameterJdbcTemplate类的准则。

19.2.3 SQLExceptionTranslator

SQLExceptionTranslator是要由可以在SQLExceptions和 Spring 自己的org.springframework.dao.DataAccessException之间进行转换的类实现的接口,而在数据访问策略方面则不可知。为了提高精度,实现可以是通用的(例如,使用 SQLState 代码用于 JDBC)或专有的(例如,使用 Oracle 错误代码)。

SQLErrorCodeSQLExceptionTranslatorSQLExceptionTranslator的实现,默认情况下使用。此实现使用特定的供应商代码。它比SQLState实现更为精确。错误代码转换基于 JavaBean 类型类SQLErrorCodes中保存的代码。此类由SQLErrorCodesFactory创建和填充,顾名思义,该类是用于基于名为sql-error-codes.xml的配置文件的内容创建SQLErrorCodes的工厂。该文件使用供应商代码填充,并且基于DatabaseMetaData中的DatabaseProductName填充。使用您正在使用的实际数据库的代码。

SQLErrorCodeSQLExceptionTranslator按以下 Sequences 应用匹配规则:

Note

SQLErrorCodesFactory默认用于定义错误代码和自定义异常转换。在 Classpath 的名为sql-error-codes.xml的文件中查找它们,并根据使用中数据库的数据库元数据中的数据库名称找到匹配的SQLErrorCodes实例。

  • 子类实现的任何自定义转换。通常情况下,将使用提供的具体SQLErrorCodeSQLExceptionTranslator,因此该规则不适用。仅当您确实提供了子类实现时才适用。

  • 作为SQLErrorCodes类的customSqlExceptionTranslator属性提供的SQLExceptionTranslator接口的任何自定义实现。

  • 搜索为SQLErrorCodes类的customTranslations属性提供的CustomSQLErrorCodesTranslation类的实例列表。

  • 错误代码匹配被应用。

  • 使用后备翻译器。 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;
    }
}

在此示例中,特定的错误代码-12345被转换,而其他错误则由默认转换器实现转换。若要使用此自定义转换器,必须通过方法setExceptionTranslator将其传递给JdbcTemplate,并将此JdbcTemplate用于需要该转换器的所有数据访问处理。这是如何使用此自定义转换器的示例:

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

    // create a JdbcTemplate and set data source
    this.jdbcTemplate = new JdbcTemplate();
    this.jdbcTemplate.setDataSource(dataSource);

    // create a custom translator and set the DataSource for the default translation lookup
    CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
    tr.setDataSource(dataSource);
    this.jdbcTemplate.setExceptionTranslator(tr);

}

public void updateShippingCharge(long orderId, long pct) {
    // use the prepared JdbcTemplate for this update
    this.jdbcTemplate.update("update orders" +
        " set shipping_charge = shipping_charge * ? / 100" +
        " where id = ?", pct, orderId);
}

自定义转换器会传递一个数据源,以便在sql-error-codes.xml中查找错误代码。

19.2.4 执行语句

执行一条 SQL 语句需要很少的代码。您需要DataSourceJdbcTemplate,包括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))");
    }
}

19.2.5 运行查询

一些查询方法返回单个值。要从一行中检索计数或特定值,请使用queryForObject(..)。后者将返回的 JDBC Type转换为作为参数传入的 Java 类。如果类型转换无效,则抛出InvalidDataAccessApiUsageException。这是一个示例,其中包含两种查询方法,一种用于int,另一种用于查询String

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class RunAQuery {

    private JdbcTemplate jdbcTemplate;

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

    public int getCount() {
        return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
    }

    public String getName() {
        return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
    }
}

除了单个结果查询方法外,还有几种方法返回一个列表,其中包含查询返回的每一行的条目。最通用的方法是queryForList(..),它返回List,其中每个条目都是Map,Map 中的每个条目代表该行的列值。如果您在上述示例中添加了一种方法来检索所有行的列表,则它看起来像这样:

private JdbcTemplate jdbcTemplate;

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

public List<Map<String, Object>> getList() {
    return this.jdbcTemplate.queryForList("select * from mytable");
}

返回的列表如下所示:

[{name=Bob, id=1}, {name=Mary, id=2}]

19.2.6 更新数据库

以下示例显示为特定主键更新的列。在此示例中,SQL 语句具有用于行参数的占位符。参数值可以作为 varargs 或作为对象数组传递。因此,应将 Primitives 显式地或使用自动装箱包装在 Primitives 包装类中。

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 检索自动生成的密钥

update()便捷方法支持检索由数据库生成的主键。该支持是 JDBC 3.0 标准的一部分。有关详细信息,请参见规范的第 13.6 章。该方法以PreparedStatementCreator作为其第一个参数,这是指定所需插入语句的方式。另一个参数是KeyHolder,它包含从更新成功返回时生成的密钥。没有创建合适的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

19.3 控制数据库连接

19.3.1 DataSource

Spring 通过DataSource获得与数据库的连接。 DataSource是 JDBC 规范的一部分,是通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务 Management 问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息。这是设置数据源的 Management 员的责任。您很可能在开发和测试代码时同时担当这两个角色,但是不必一定要知道如何配置生产数据源。

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

本节使用 Spring 的DriverManagerDataSource实现,稍后将介绍其他一些实现。

Note

仅使用DriverManagerDataSource类应仅用于测试目的,因为它不提供池化,并且在发出多个连接请求时性能会很差。

您通常会获得 JDBC 连接,因此获得与DriverManagerDataSource的连接。指定 JDBC 驱动程序的标准类名,以便DriverManager可以加载驱动程序类。接下来,提供一个在 JDBC 驱动程序之间变化的 URL。 (请咨询驱动程序的文档以获取正确的值.)然后提供用户名和密码以连接到数据库。这是如何在 Java 代码中配置DriverManagerDataSource的示例:

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

这是相应的 XML 配置:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

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

以下示例显示了 DBCP 和 C3P0 的基本连接和配置。要了解更多有助于控制池功能的选项,请参阅相应连接池实现的产品文档。

DBCP 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类是一种方便且功能强大的帮助器类,它提供static个方法以从 JNDI 获取连接并在必要时关闭连接。它支持例如DataSourceTransactionManager的线程绑定连接。

19.3.3 SmartDataSource

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

19.3.4 AbstractDataSource

AbstractDataSource是 Spring 的DataSource实现的abstractBase Class,该类实现所有DataSource实现的通用代码。如果要编写自己的DataSource实现,则可以扩展AbstractDataSource类。

19.3.5 SingleConnectionDataSource

SingleConnectionDataSource类是SmartDataSource接口的实现,该接口包装了**单个* Connection,每次使用后*都未关闭。显然,这不是多线程功能。

如果使用共享工具,假设任何 Client 端代码在构建池连接的情况下都调用close,请将suppressClose属性设置为true。此设置返回一个封闭物理连接的封闭代理。请注意,您将无法再将其强制转换为本机 Oracle Connection等。

这主要是测试类。例如,它结合简单的 JNDI 环境,可以在应用服务器外部轻松测试代码。与DriverManagerDataSource相比,它始终重用同一连接,从而避免了过多的物理连接创建。

19.3.6 DriverManagerDataSource

DriverManagerDataSource类是标准DataSource接口的实现,该接口通过 bean 属性配置纯 JDBC 驱动程序,并每次返回一个新的Connection

此实现对于 Java EE 容器外部的测试和独立环境非常有用,可以作为 Spring IoC 容器中的DataSource bean 或与简单的 JNDI 环境结合使用。池假设Connection.close()调用将简单地关闭连接,因此任何DataSource感知的持久性代码都应起作用。但是,即使在测试环境中,使用commons-dbcp之类的 JavaBean 风格的连接池也是如此容易,以至于总是总是比DriverManagerDataSource使用这种连接池更可取。

19.3.7 TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy是目标DataSource的代理,该代理包装该目标DataSource以增加对 SpringManagement 的事务的了解。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource

Note

很少需要使用此类,除非必须调用并传递标准 JDBC DataSource接口实现的现有代码。在这种情况下,可能仍然可以使用此代码,同时使该代码参与 Spring 托管的事务。通常最好使用更高级别的资源 Management 抽象来编写自己的新代码,例如JdbcTemplateDataSourceUtils

(有关更多详细信息,请参见TransactionAwareDataSourceProxy javadocs.)

19.3.8 DataSourceTransactionManager

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

需要应用程序代码才能通过DataSourceUtils.getConnection(DataSource)而不是 Java EE 的标准DataSource.getConnection检索 JDBC 连接。它引发未检查的org.springframework.dao异常,而不是已检查的SQLExceptionsJdbcTemplate之类的所有框架类均暗含使用此策略。如果不与该事务 Management 器一起使用,则查找策略的行为与普通策略完全相同-因此可以在任何情况下使用。

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

在单个资源的情况下,可以使用此实现代替JtaTransactionManager,因为它不需要容器支持 JTA。如果您坚持要求的连接查找模式,则仅在配置之间进行切换。 JTA 不支持自定义隔离级别!

19.3.9 NativeJdbcExtractor

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

NativeJdbcExtractor具有多种风格以匹配您的执行环境:

  • SimpleNativeJdbcExtractor

  • C3P0NativeJdbcExtractor

  • CommonsDbcpNativeJdbcExtractor

  • JBossNativeJdbcExtractor

  • WebLogicNativeJdbcExtractor

  • WebSphereNativeJdbcExtractor

  • XAPoolNativeJdbcExtractor

通常,在大多数环境中,SimpleNativeJdbcExtractor足以解开Connection对象。有关更多详细信息,请参见 javadocs。

19.4 JDBC 批处理操作

如果将多个调用批处理到同一条准备好的语句,则大多数 JDBC 驱动程序都会提高性能。通过将更新分组,可以限制到数据库的往返次数。

19.4.1 使用 JdbcTemplate 的基本批处理操作

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

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

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

    public int[] batchUpdate(final List<Actor> actors) {
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                new BatchPreparedStatementSetter() {
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        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界面,一旦 Importing 源用尽,您就可以中断批处理。 isBatchExhausted方法使您可以发出批处理结束的 signal。

19.4.2 具有对象列表的批处理操作

JdbcTemplateNamedParameterJdbcTemplate都提供了另一种提供批处理更新的方式。无需实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值,并使用内部准备好的语句设置器。 API 取决于您是否使用命名参数。对于命名参数,您提供一个SqlParameterSource数组,每个批次的成员一个条目。您可以使用SqlParameterSourceUtils.createBatch便捷方法来创建此数组,并传入一个由 bean 样式的对象(具有与参数对应的 getter 方法)和/或由 String-keyed Maps(包含相应的参数作为值)组成的数组。

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

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 语句占位符,则传入包含包含更新值的对象数组的列表。该对象数组在 SQL 语句中的每个占位符必须具有一个条目,并且它们的 Sequences 必须与 SQL 语句中定义的 Sequences 相同。

使用经典 JDBC 的同一示例“?”占位符:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

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

    public int[] batchUpdate(final List<Actor> actors) {
        List<Object[]> batch = new ArrayList<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
}

上面所有的批处理更新方法都返回一个 int 数组,其中包含每个批处理条目的受影响的行数。此计数由 JDBC 驱动程序报告。如果该计数不可用,则 JDBC 驱动程序将返回-2 值。

19.4.3 具有多个批次的批次操作

批处理更新的最后一个示例处理的批处理太大,以至于您想将它们分成几个较小的批处理。当然,您可以通过多次调用batchUpdate方法来使用上述方法来执行此操作,但是现在有了更方便的方法。除 SQL 语句外,此方法还包含一个对象集合,其中包含参数,每个批处理要进行的更新次数以及ParameterizedPreparedStatementSetter来设置已准备好的语句的参数值。框架遍历提供的值,并将更新调用分成指定大小的批处理。

本示例显示使用 100 的批量大小的批量更新:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

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

    public int[][] batchUpdate(final Collection<Actor> actors) {
        int[][] updateCounts = jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                actors,
                100,
                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 数组数组,该数组包含每个批处理的数组条目以及每个更新受影响的行数的数组。顶级数组的长度指示已执行的批处理数,第二级数组的长度指示该批处理中的更新数。每个批次中的更新数量应该是为所有批次提供的批次大小,但最后一个数量可能会少一些,这取决于所提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的计数。如果该计数不可用,则 JDBC 驱动程序将返回-2 值。

19.5 使用 SimpleJdbc 类简化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall类通过利用可通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。这意味着无需进行过多的配置,尽管如果您愿意在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。

19.5.1 使用 SimpleJdbcInsert 插入数据

让我们从使用最少配置选项的SimpleJdbcInsert类开始。您应该在数据访问层的初始化方法中实例化SimpleJdbcInsert。对于此示例,初始化方法是setDataSource方法。您不需要将SimpleJdbcInsert类子类化;只需创建一个新实例并使用withTableName方法设置表名。此类的配置方法遵循“ fluid”样式,该样式返回SimpleJdbcInsert的实例,该实例允许您链接所有配置方法。本示例仅使用一种配置方法。稍后您将看到多个示例。

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 方法将普通java.utils.Map作为其唯一参数。这里要注意的重要一点是,用于 Map 的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据是为了构造实际的插入语句。

19.5.2 使用 SimpleJdbcInsert 检索自动生成的密钥

本示例使用与前面相同的插入方式,但是它没有传递 id,而是检索自动生成的键并将其设置在新的 Actor 对象上。创建SimpleJdbcInsert时,除了指定表名之外,还可以使用usingGeneratedKeyColumns方法指定生成的键列的名称。

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
}

通过第二种方法执行插入操作的主要区别在于,您没有将 ID 添加到 Map 中,而是调用了executeAndReturnKey方法。这将返回一个java.lang.Number对象,您可以使用该对象创建在我们的域类中使用的数字类型的实例。您不能在这里依靠所有数据库来返回特定的 Java 类。 java.lang.Number是您可以依赖的 Base Class。如果您有多个自动生成的列,或者生成的值是非数字的,则可以使用从executeAndReturnKeyHolder方法返回的KeyHolder

19.5.3 为 SimpleJdbcInsert 指定列

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

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
}

插入的执行与依靠元数据确定要使用的列的执行相同。

19.5.4 使用 SqlParameterSource 提供参数值

使用Map提供参数值可以很好地工作,但这不是最方便使用的类。 Spring 提供了SqlParameterSource接口的几种实现方式,可以替代使用。第一个是BeanPropertySqlParameterSource,如果您有一个包含值的 JavaBean 兼容类,这是一个非常方便的类。它将使用相应的 getter 方法提取参数值。这是一个例子:

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
}

如您所见,配置是相同的。只有执行代码才能更改为使用这些替代 Importing 类。

19.5.5 使用 SimpleJdbcCall 调用存储过程

SimpleJdbcCall类利用数据库中的元数据来查找inout参数的名称,因此您不必显式声明它们。如果愿意,可以声明参数,或者声明诸如ARRAYSTRUCT之类的参数没有自动 Map 到 Java 类的参数。第一个示例显示了一个简单过程,该过程仅从 MySQL 数据库返回VARCHARDATE格式的标量值。示例过程读取指定的 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参数包含您要查找的演员的idout参数返回从表读取的数据。

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

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
}

您为执行调用而编写的代码涉及创建一个包含 IN 参数的SqlParameterSource。将 Importing 值提供的名称与存储过程中声明的参数名称的名称进行匹配很重要。大小写不必匹配,因为您使用元数据来确定在存储过程中应如何引用数据库对象。源中为存储过程指定的内容不一定是存储过程在数据库中存储的方式。一些数据库将名称转换为全部大写,而另一些数据库使用小写或指定的大小写。

execute方法采用 IN 参数,并返回一个 Map,该 Map 包含以存储过程中指定的名称为关键字的任何out参数。在这种情况下,它们是out_first_name, out_last_nameout_birth_date

execute方法的最后一部分创建一个 Actor 实例,用于返回检索到的数据。同样,重要的是使用在存储过程中声明的out参数的名称。同样,结果 Map 中存储的out参数名称的大小写与数据库中out参数名称的大小写匹配,这在数据库之间可能会有所不同。为了使代码更具可移植性,您应该执行不区分大小写的查找或指示 Spring 使用LinkedCaseInsensitiveMap。为此,您需要创建自己的JdbcTemplate并将setResultsMapCaseInsensitive属性设置为true。然后,您将此自定义的JdbcTemplate实例传递到SimpleJdbcCall的构造函数中。这是此配置的示例:

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

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

    // ... additional methods
}

通过执行此操作,可以避免在用于返回的out参数名称的情况下发生冲突。

19.5.6 明确声明要用于 SimpleJdbcCall 的参数

您已经了解了如何基于元数据推导参数,但是如果需要,可以显式声明。为此,您可以使用declareParameters方法创建并配置SimpleJdbcCall,该方法将可变数量的SqlParameter对象作为 Importing。有关如何定义SqlParameter的详细信息,请参见下一部分。

Note

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

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

下面的示例使用前一示例中的信息显示一个完全声明的过程调用。

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

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

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

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

带有SqlOutParameter的第二行声明一个out参数,该参数将在存储过程调用中使用。还有一个SqlInOutParameter表示InOut参数,这些参数为过程提供IN值并返回一个值。

Note

仅声明为SqlParameterSqlInOutParameter的参数将用于提供 Importing 值。这与StoredProcedure类不同,后者出于向后兼容性的原因,允许为声明为SqlOutParameter的参数提供 Importing 值。

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

19.5.8 使用 SimpleJdbcCall 调用存储的函数

调用存储函数的方式几乎与调用存储过程的方式相同,只是提供的是函数名而不是过程名。您将withFunctionName方法用作配置的一部分,以表明我们要调用函数,并生成了函数调用的相应字符串。专门的执行调用executeFunction,用于执行函数,它以指定类型的对象的形式返回函数的返回值,这意味着您不必从结果图中检索返回值。对于只有一个out参数的存储过程,也可以使用名为executeObject的类似便利方法。以下示例基于名为get_actor_name的存储函数,该函数返回演员的全名。这是此功能的 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 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 方法返回一个String,其中包含该函数调用的返回值。

19.5.9 从 SimpleJdbcCall 返回 ResultSet/REF 游标

调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则需要显式注册的特定类型的out参数。两种方法都需要进行额外的处理才能遍历结果集并处理返回的行。对于SimpleJdbcCall,您可以使用returningResultSet方法,并声明RowMapper实现用于特定参数。在结果处理期间返回结果集的情况下,没有定义名称,因此返回的结果必须与声明RowMapper实现的 Sequences 匹配。指定的名称仍用于将处理后的结果列表存储在由 execute 语句返回的结果图中。

下一个示例使用一个不带 IN 参数的存储过程,并从 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。因为要 Map 的类遵循 JavaBean 规则,所以可以使用BeanPropertyRowMapper,该BeanPropertyRowMapper是通过在newInstance方法中传入要 Map 的必需类而创建的。

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadAllActors;

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

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

    // ... additional methods
}

execute 调用传递一个空的 Map,因为此调用不带任何参数。然后从结果图中检索 Actor 列表,并将其返回给调用方。

19.6 将 JDBC 操作建模为 Java 对象

org.springframework.jdbc.object软件包包含允许您以更加面向对象的方式访问数据库的类。例如,您可以执行查询并以包含业务对象的列表的形式返回结果,其中关系列数据 Map 到业务对象的属性。您还可以执行存储过程并运行 update,delete 和 insert 语句。

Note

许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类(但StoredProcedure类除外)通常可以被直接JdbcTemplate调用替换。通常,编写直接在JdbcTemplate上直接调用方法的 DAO 方法(与将查询封装为完整的类相对)更简单。

但是,如果您通过使用 RDBMS 操作类获得可衡量的价值,请 continue 使用这些类。

19.6.1 SqlQuery

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

19.6.2 MappingSqlQuery

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

public class ActorMappingQuery extends MappingSqlQuery<Actor> {

    public ActorMappingQuery(DataSource ds) {
        super(ds, "select id, first_name, last_name from t_actor where id = ?");
        declareParameter(new SqlParameter("id", Types.INTEGER));
        compile();
    }

    @Override
    protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
        Actor actor = new Actor();
        actor.setId(rs.getLong("id"));
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }

}

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

private ActorMappingQuery actorMappingQuery;

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

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

在此示例中,该方法将检索具有作为唯一参数传入的 id 的 Client。由于我们只希望返回一个对象,因此我们只需调用 id 为参数的便捷方法findObject即可。相反,如果我们有一个返回对象列表并采用其他参数的查询,那么我们将使用其中一种执行方法,该方法采用以 varargs 形式传入的参数值数组。

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

19.6.3 SqlUpdate

SqlUpdate类封装了 SQL 更新。像查询一样,更新对象是可重用的,并且像所有RdbmsOperation类一样,更新可以具有参数并在 SQL 中定义。此类提供了许多update(..)方法,类似于查询对象的execute(..)方法。 SQLUpdate类是具体的。例如,可以将其子类化以添加自定义更新方法,如下面的代码片段中简称为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("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类是 RDBMS 存储过程的对象抽象的超类。此类是abstract,并且其各种execute(..)方法具有protected访问权限,除了通过提供更严格的键入的子类之外,其他都禁止使用。

继承的sql属性将是 RDBMS 中存储过程的名称。

要为StoredProcedure类定义参数,请使用SqlParameter或其子类之一。您必须像下面的代码片段中那样在构造函数中指定参数名称和 SQL 类型。使用java.sql.Types常量指定 SQL 类型。

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

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

第二行带有SqlOutParameter的行声明了要在存储过程调用中使用的out参数。还有一个SqlInOutParameter表示I nOut参数,这些参数为过程提供in值并且还返回一个值。

对于i n参数,除了名称和 SQL 类型外,还可以为数字数据指定小数位,或者为自定义数据库类型指定类型名。对于out参数,您可以提供RowMapper来处理从 REF 游标返回的行的 Map。另一个选择是指定SqlReturnType,使您可以定义返回值的自定义处理。

这是一个简单的 DAO 的示例,该 DAO 使用StoredProcedure调用任何 Oracle 数据库附带的函数sysdate()。要使用存储过程功能,您必须创建一个扩展StoredProcedure的类。在此示例中,StoredProcedure类是一个内部类,但是如果您需要重用StoredProcedure,则可以将其声明为顶级类。此示例没有 Importing 参数,但是使用SqlOutParameter类将输出参数声明为日期类型。 execute()方法执行该过程,并从结果Map中提取返回的日期。结果Map使用参数名称作为键,为每个声明的输出参数都有一个条目,在这种情况下,只有一个。

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class StoredProcedureDao {

    private GetSysdateProcedure getSysdate;

    @Autowired
    public void init(DataSource dataSource) {
        this.getSysdate = new GetSysdateProcedure(dataSource);
    }

    public Date getSysdate() {
        return getSysdate.execute();
    }

    private class GetSysdateProcedure extends StoredProcedure {

        private static final String SQL = "sysdate";

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

        public Date execute() {
            // the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
            Map<String, Object> results = execute(new HashMap<String, Object>());
            Date sysdate = (Date) results.get("date");
            return sysdate;
        }
    }

}

以下StoredProcedure的示例具有两个输出参数(在本例中为 Oracle REF 游标)。

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "AllTitlesAndGenres";

    public TitlesAndGenresStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
        compile();
    }

    public Map<String, Object> execute() {
        // again, this sproc has no input parameters, so an empty Map is supplied
        return super.execute(new HashMap<String, Object>());
    }
}

注意如何在RowMapper实现实例中传递在TitlesAndGenresStoredProcedure构造函数中使用的declareParameter(..)方法的重载变体。这是重用现有功能的非常方便且强大的方法。下面提供了两种RowMapper实现的代码。

对于提供的ResultSet中的每一行,TitleMapper类将ResultSetMap 到Title域对象:

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

public final class TitleMapper implements RowMapper<Title> {

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

对于所提供的ResultSet中的每一行,GenreMapper类将ResultSetMap 到Genre域对象。

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

public final class GenreMapper implements RowMapper<Genre> {

    public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}

要将参数传递到 RDBMS 中定义中具有一个或多个 Importing 参数的存储过程,可以编写一个强类型的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 参数和数据值处理的常见问题

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

19.7.1 提供参数的 SQL 类型信息

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

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

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

  • 您可以使用SqlParameterValue类包装需要此附加信息的参数值。为每个值创建一个新实例,然后在构造函数中传入 SQL 类型和参数值。您还可以为数字值提供可选的比例参数。

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

19.7.2 处理 BLOB 和 CLOB 对象

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

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

  • BLOB

  • byte[]getBlobAsBytessetBlobAsBytes

    • InputStreamgetBlobAsBinaryStreamsetBlobAsBinaryStream
  • CLOB

  • StringgetClobAsStringsetClobAsString

    • InputStreamgetClobAsAsciiStreamsetClobAsAsciiStream

    • ReadergetClobAsCharacterStreamsetClobAsCharacterStream

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

本示例使用JdbcTemplateAbstractLobCreatingPreparedStatementCallback的实现。它实现了一种方法setValues。此方法提供LobCreator,可用于设置 SQL 插入语句中的 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) ImportinglobHandler,在此示例中为纯DefaultLobHandler
  • (2) 使用方法setClobAsCharacterStream传入 CLOB 的内容。
  • (3) 使用方法setBlobAsBinaryStream传入 BLOB 的内容。

Note

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

请查阅所用 JDBC 驱动程序的文档,以在不提供内容长度的情况下验证对流式 LOB 的支持。

现在是时候从数据库中读取 LOB 数据了。同样,您使用具有相同实例变量lobHandlerJdbcTemplate和对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"); (1)
results.put("CLOB", clobText); byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); (2)
results.put("BLOB", blobBytes); return results; } });
  • (1) 使用方法getClobAsString检索 CLOB 的内容。
  • (2) 使用方法getBlobAsBytes检索 BLOB 的内容。

19.7.3 传入 IN 子句的值列表

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

Note

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

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

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

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

这是返回声明为类型ITEM_TYPE的用户的 Oracle STRUCT对象的值的示例。 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 对象(例如TestItem)的值传递到存储过程中。 SqlTypeValue接口有一个必须实现的名为createTypeValue的方法。传入活动连接,您可以使用它来创建特定于数据库的对象,如StructDescriptor s(如以下示例所示)或ArrayDescriptor s。

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添加到包含用于存储过程的 execute 调用的 Importing 参数的 Map 中。

SqlTypeValue的另一个用途是将值数组传递给 Oracle 存储过程。在这种情况下,Oracle 具有自己的内部ARRAY类,您可以使用SqlTypeValue创建 Oracle ARRAY的实例,并使用 Java ARRAY的值填充它。

final Long[] ids = new Long[] {1L, 2L};

SqlTypeValue value = new AbstractSqlTypeValue() {
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
        ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
        ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
        return idArray;
    }
};

19.8 嵌入式数据库支持

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

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

嵌入式数据库由于其轻量级的特性,因此在项目的开发阶段非常有用。好处包括易于配置,启动时间短,可测试性以及在开发过程中快速演化 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>

前面的配置从 Classpath 的根中的schema.sqltest-data.sql资源中创建一个用 SQL 填充的嵌入式 HSQL 数据库。另外,作为最佳实践,将为嵌入式数据库分配一个唯一生成的名称。嵌入式数据库作为javax.sql.DataSource类型的 bean 对于 Spring 容器可用,然后可以根据需要将其注入到数据访问对象中。

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

EmbeddedDatabaseBuilder类提供了一种流畅的 API,可用于以编程方式构造嵌入式数据库。需要在独立环境或独立集成测试中创建嵌入式数据库时,请使用此示例,如以下示例所示。

EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
		.generateUniqueName(true)
		.setType(H2)
		.setScriptEncoding("UTF-8")
		.ignoreFailedDrops(true)
		.addScript("schema.sql")
		.addScripts("user_data.sql", "country_data.sql")
		.build();

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()

有关所有受支持选项的更多详细信息,请向 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 选择嵌入式数据库类型

Using HSQL

Spring 支持 HSQL 1.8.0 及更高版本。如果未明确指定类型,则 HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将embedded-database标记的type属性设置为HSQL。如果使用的是构建器 API,请使用EmbeddedDatabaseType.HSQL调用setType(EmbeddedDatabaseType)方法。

Using H2

Spring 也支持 H2 数据库。要启用 H2,请将embedded-database标签的type属性设置为H2。如果使用的是构建器 API,请使用EmbeddedDatabaseType.H2调用setType(EmbeddedDatabaseType)方法。

Using Derby

Spring 还支持 Apache Derby 10.5 及更高版本。要启用 Derby,请将embedded-database标签的type属性设置为DERBY。如果使用的是构建器 API,请使用EmbeddedDatabaseType.DERBY调用setType(EmbeddedDatabaseType)方法。

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

嵌入式数据库提供了一种轻量级的方法来测试数据访问代码。以下是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在测试类之间重用时,使用这样的模板可以一次性完成。但是,如果要创建在测试套件中共享的嵌入式数据库,请考虑使用Spring TestContext 框架并将嵌入式数据库配置为 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类负责创建嵌入式数据库,然后在同一测试套件(即,同一 JVM 进程)内的多个测试场景中重用相应的配置,则这很容易发生-例如,对嵌入式数据库的集成测试,该嵌入式数据库的ApplicationContext配置仅在哪些 bean 定义配置文件处于活动状态方面有所不同。

此类错误的根本原因是,如果没有另外指定,Spring 的EmbeddedDatabaseFactory(由<jdbc:embedded-database> XML 名称空间元素和EmbeddedDatabaseBuilder用于 Java Config 内部使用)会将嵌入式数据库的名称设置为"testdb"。对于<jdbc:embedded-database>,通常为嵌入式数据库分配一个与 Bean 的id相同的名称(即,通常类似于"dataSource")。因此,随后尝试创建嵌入式数据库将不会产生新的数据库。相反,相同的 JDBC 连接 URL 将被重用,并且尝试创建新的嵌入式数据库实际上将指向通过相同配置创建的现有嵌入式数据库。

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

  • EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()

  • EmbeddedDatabaseBuilder.generateUniqueName()

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

19.8.7 扩展嵌入式数据库支持

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

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

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

鼓励您向jira.spring.io的 Spring 社区回馈扩展。

19.9 初始化数据源

org.springframework.jdbc.datasource.init软件包为初始化现有的DataSource提供支持。嵌入式数据库支持提供了一个为应用程序创建和初始化DataSource的选项,但是有时您需要初始化在某处的服务器上运行的实例。

19.9.1 使用 Spring XML 初始化数据库

如果要初始化数据库,并且可以提供对DataSource bean 的引用,请使用spring-jdbc命名空间中的initialize-database标记:

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

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

数据库初始化程序的默认行为是无条件执行所提供的脚本。例如,如果您正在针对已经具有测试数据的数据库执行脚本,那么这将不一定是您想要的。通过遵循首先创建表然后插入数据的通用模式(如上所示),可以减少意外删除数据的可能性。如果表已经存在,则第一步将失败。

但是,为了更好地控制现有数据的创建和删除,XML 名称空间提供了一些其他选项。第一个是用于打开和关闭初始化的标志。可以根据环境进行设置(例如,从系统属性或环境 Bean 中提取布尔值),例如:

<jdbc:initialize-database data-source="dataSource"
    enabled="#{systemProperties.INITIALIZE_DATABASE}">
    <jdbc:script location="..."/>
</jdbc:initialize-database>

控制现有数据会发生什么的第二种选择是更加容忍失败。为此,您可以控制初始化程序忽略脚本执行的 SQL 中某些错误的能力,例如:

<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
    <jdbc:script location="..."/>
</jdbc:initialize-database>

在此示例中,我们说我们期望有时脚本将针对空数据库执行,并且脚本中存在一些DROP语句,因此可能会失败。因此,失败的 SQL DROP语句将被忽略,但其他失败将导致异常。如果您的 SQL 方言不支持DROP … IF EXISTS(或类似值),但是您希望在重新创建它之前无条件地删除所有测试数据,这将很有用。在这种情况下,第一个脚本通常是一组DROP语句,然后是一组CREATE语句。

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

如果脚本中根本没有;字符,则每个语句应用;或换行符分隔。您可以全局控制或逐个脚本控制,例如:

<jdbc:initialize-database data-source="dataSource" separator="@@">
    <jdbc:script location="classpath:com/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>

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

如果您需要比从 XML 名称空间获得更多控制权,则可以直接使用DataSourceInitializer并将其定义为应用程序中的组件。

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

大量的应用程序可以使用数据库初始化程序,而不会带来更多的复杂性:那些在 Spring 上下文启动之后才使用数据库的应用程序。如果您的应用程序不是其中之一,那么您可能需要阅读本节的其余部分。

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

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

如果应用程序在您的控制之下,则第一个选项可能很容易,否则就不容易。有关如何实现此目的的一些建议包括:

  • 使缓存在首次使用时延迟初始化,从而缩短了应用程序的启动时间。

  • 让您的缓存或单独的组件初始化缓存实现LifecycleSmartLifecycle。当应用程序上下文启动时,如果设置了autoStartup标志,则可以自动启动SmartLifecycle,并且可以通过在封闭上下文中调用ConfigurableApplicationContext.start()来手动启动Lifecycle

  • 使用 Spring ApplicationEvent或类似的自定义观察器机制来触发缓存初始化。 ContextRefreshedEvent随时可供使用(在初始化所有 bean 之后)由上下文发布,因此通常是一个有用的钩子(默认情况下SmartLifecycle的工作方式)。

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

  • 依靠 Spring BeanFactory的默认行为,即按注册 Sequences 初始化 bean。通过采用在 XML 配置中对应用程序模块进行排序的一组<import/>元素的常规做法,并确保首先列出数据库和数据库初始化,您可以轻松地进行安排。

  • 分开DataSource和使用它的业务组件,并通过将它们放在单独的ApplicationContext实例中来控制其启动 Sequences(例如,父上下文包含DataSource,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更广泛地应用。