19. 使用JDBC进行数据访问

19.1 Spring Framework JDBC简介

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

Table 19.1. Spring JDBC - who does what?

动作 Spring
定义连接参数。X
打开连接。X
指定SQL语句。X
声明参数并提供参数值X
准备并执行该声明。X
设置循环以迭代结果(如果有)。X
为每次迭代完成工作。X
处理任何异常。X
处理 Transaction 。X
关闭连接,语句和结果集。X

Spring Framework负责处理所有可以使JDBC成为一个繁琐的API的低级细节。

19.1.1 选择JDBC数据库访问方法

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

  • JdbcTemplate是经典的Spring JDBC方法,也是最受欢迎的方法。这个 "lowest level" 方法和所有其他方法都使用了JdbcTemplate。

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

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

  • RDBMS对象(包括MappingSqlQuery,SqlUpdate和StoredProcedure)要求您在数据访问层初始化期间创建可重用且线程安全的对象。此方法在JDO Query之后建模,其中您定义查询字符串,声明参数和编译查询。执行此操作后,可以多次调用execute方法,并传入各种参数值。

19.1.2 包层次结构

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

org.springframework.jdbc.core 包中包含 JdbcTemplate 类及其各种回调接口,以及各种相关类。名为 org.springframework.jdbc.core.simple 的子包包含 SimpleJdbcInsertSimpleJdbcCall 类。另一个名为 org.springframework.jdbc.core.namedparam 的子包包含 NamedParameterJdbcTemplate 类和相关的支持类。请参见 Section 19.2, “Using the JDBC core classes to control basic JDBC processing and error handling”Section 19.4, “JDBC batch operations”Section 19.5, “Simplifying JDBC operations with the SimpleJdbc classes”

org.springframework.jdbc.datasource 包中包含一个易于 DataSource 访问的实用程序类,以及可用于在Java EE容器外测试和运行未修改的JDBC代码的各种简单 DataSource 实现。名为 org.springfamework.jdbc.datasource.embedded 的子包支持使用Java数据库引擎(如HSQL,H2和Derby)创建嵌入式数据库。见 Section 19.3, “Controlling database connections”Section 19.8, “Embedded database support”

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

org.springframework.jdbc.support 包提供 SQLException 转换功能和一些实用程序类。 JDBC处理期间抛出的异常将转换为 org.springframework.dao 包中定义的异常。这意味着使用Spring JDBC抽象层的代码不需要实现JDBC或RDBMS特定的错误处理。所有已翻译的异常都是未选中的,这使您可以选择捕获可以恢复的异常,同时允许将其他异常传播给调用方。见 Section 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 。在给定此类提供的 Connection 的情况下, PreparedStatementCreator 回调接口会创建一个预准备语句,从而提供SQL和任何必要的参数。 CallableStatementCreator 接口也是如此,它创建了可调用语句。 RowCallbackHandler 接口从 ResultSet 的每一行中提取值。

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

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

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

JdbcTemplate类用法的示例

本节提供了 JdbcTemplate 类使用的一些示例。这些示例并非 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);

查询和填充单个域对象:

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更新(INSERT / UPDATE / DELETE)

您使用 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 类的实例就是线程安全的。这很重要,因为这意味着您可以配置 JdbcTemplate 的单个实例,然后将此共享引用安全地注入多个DAO(或存储库)。 JdbcTemplate 是有状态的,因为它维护对 DataSource 的引用,但此状态不是会话状态。

使用 JdbcTemplate 类(以及关联的 NamedParameterJdbcTemplate 类)时的常见做法是在Spring配置文件中配置 DataSource ,然后依赖注入共享 DataSource bean到DAO类中; JdbcTemplate 是在 DataSource 的setter中创建的。这导致DAO看起来部分如下:

public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

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

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

相应的配置可能如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    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>

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

<?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 实例就是线程安全的。如果您的应用程序访问多个数据库,您可能需要多个 JdbcTemplate 实例,这需要多个 DataSources ,然后多个不同的 JdbcTemplates

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(即,遵循 the JavaBean conventions 的类的实例),并使用包装的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

有关在应用程序上下文中使用 NamedParameterJdbcTemplate 类的指导,另请参阅 the section called “JdbcTemplate best practices”

19.2.3 SQLExceptionTranslator

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

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

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

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

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

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

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

  • 应用了错误代码匹配。

  • 使用后备翻译器。 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 ,映射中的每个条目表示该行的列值。如果您向上面的示例添加一个方法来检索所有行的列表,它将如下所示:

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传递,也可以作为对象数组传递。因此,原语应该显式地包装在原始包装类中或使用自动装箱。

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 作为其第一个参数,这是指定所需insert语句的方式。另一个参数是 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规范的一部分,是一个通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息;这是设置数据源的管理员的责任。您最有可能在开发和测试代码时填充这两个角色,但您不必知道如何配置 生产环境 数据源。

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

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

仅使用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配置:

<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配置:

<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 实现的 abstract 基类,它实现了所有 DataSource 实现共有的代码。如果您正在编写自己的 DataSource 实现,则扩展 AbstractDataSource 类。

19.3.5 SingleConnectionDataSource

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

如果任何客户端代码在假设池化连接时调用 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 -aware持久性代码都应该起作用。但是,即使在测试环境中,使用诸如 commons-dbcp 之类的JavaBean样式连接池也非常容易,因此使用这样的连接池几乎总是优先于 DriverManagerDataSource

19.3.7 TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy 是目标 DataSource 的代理,它包装目标 DataSource 以添加对Spring管理的事务的认识。在这方面,它类似于Java EE服务器提供的事务性JNDI DataSource

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

(有关详细信息,请参阅 TransactionAwareDataSourceProxy javadocs。)

19.3.8 DataSourceTransactionManager

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

需要应用程序代码来通过 DataSourceUtils.getConnection(DataSource) 而不是Java EE的标准 DataSource.getConnection 来检索JDBC连接。它会抛出未经检查的 org.springframework.dao 异常,而不是检查 SQLExceptions 。像 JdbcTemplate 这样的所有框架类都隐式使用这种策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同 - 因此可以在任何情况下使用它。

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

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

19.3.9 NativeJdbcExtractor

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

NativeJdbcExtractor 有各种各样的风格以匹配您的执行环境:

  • SimpleNativeJdbcExtractor

  • C3P0NativeJdbcExtractor

  • CommonsDbcpNativeJdbcExtractor

  • JBossNativeJdbcExtractor

  • WebLogicNativeJdbcExtractor

  • WebSphereNativeJdbcExtractor

  • XAPoolNativeJdbcExtractor

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

19.4 JDBC批处理操作

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

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

您可以通过实现特殊接口 BatchPreparedStatementSetter 的两个方法来完成 JdbcTemplate 批处理,并将其作为 batchUpdate 方法调用中的第二个参数传递。使用 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 接口,该接口允许您在输入源耗尽后中断批处理。 isBatchExhausted 方法允许您发出批次结束的信号。

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

JdbcTemplateNamedParameterJdbcTemplate 都提供了另一种提供批量更新的方法。您可以将调用中的所有参数值作为列表提供,而不是实现特殊的批处理接口。框架循环遍历这些值并使用内部预处理语句setter。 API会根据您是否使用命名参数而有所不同。对于命名参数,您提供了一个 SqlParameterSource 数组,该批处理的每个成员都有一个条目。您可以使用 SqlParameterSourceUtils.createBatch convenience方法创建此数组,传入一个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语句中为每个占位符分配一个条目,并且它们的顺序必须与SQL语句中定义的顺序相同。

使用经典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的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据以构造实际的insert语句。

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 是您可以信赖的基类。如果您有多个自动生成的列,或者生成的值是非数字的,则可以使用从 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
}

另一种选择是 MapSqlParameterSource ,它类似于Map,但提供了一种可以链接的更方便的 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
}

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

19.5.5 使用SimpleJdbcCall调用存储过程

SimpleJdbcCall 类利用数据库中的元数据来查找 inout 参数的名称,这样您就不必显式声明它们。如果您愿意,可以声明参数,或者如果您没有自动映射到Java类的参数(如 ARRAYSTRUCT )。第一个示例显示了一个简单的过程,该过程仅从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 参数包含您正在查找的actor的 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 。将为输入值提供的名称与存储过程中声明的参数名称的名称相匹配非常重要。该案例不必匹配,因为您使用元数据来确定应如何在存储过程中引用数据库对象。存储过程的源中指定的内容不一定是它存储在数据库中的方式。某些数据库将名称转换为全部大写,而其他数据库使用小写或使用指定的大小写。

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

execute 方法的最后一部分创建一个Actor实例,用于返回检索到的数据。同样,使用 out 参数的名称非常重要,因为它们在存储过程中声明。在这种情况下存储在结果映射中的 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 对象作为输入。有关如何定义 SqlParameter 的详细信息,请参阅下一节。

如果您使用的数据库不是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

要定义SimpleJdbc类的参数以及 Section 19.6, “Modeling JDBC operations as Java objects” 中涵盖的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 值并且还返回一个值。

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

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

19.5.8 使用SimpleJdbcCall调用存储的函数

您调用存储函数的方式与调用存储过程几乎相同,只是提供函数名而不是过程名。您使用 withFunctionName 方法作为配置的一部分来指示我们要调用函数,并生成函数调用的相应字符串。一个专门的执行调用 executeFunction, 用于执行该函数,它将函数返回值作为指定类型的对象返回,这意味着您不必从结果映射中检索返回值。名为 executeObject 的类似便捷方法也可用于只有一个 out 参数的存储过程。以下示例基于名为 get_actor_name 的存储函数,该函数返回actor的全名。以下是此函数的MySQL源代码:

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

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

public class JdbcActorDao implements ActorDao {

    private 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 实现的顺序相匹配。指定的名称仍用于将处理的结果列表存储在从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 。因为要映射到的类遵循JavaBean规则,所以可以使用通过传入所需类来创建的 BeanPropertyRowMapper 来映射到 newInstance 方法。

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,因为此调用不接受任何参数。然后从结果映射中检索Actors列表并返回给调用者。

19.6 将JDBC操作建模为Java对象

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

许多Spring开发人员认为下面描述的各种RDBMS操作类(StoredProcedure类除外)通常可以用直接的JdbcTemplate调用替换。编写一个直接在JdbcTemplate上调用方法的DAO方法通常更简单(而不是将查询封装为完整的类)。但是,如果您从使用RDBMS操作类获得可测量的值,请继续使用这些类。

19.6.1 SqlQuery

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

19.6.2 MappingSqlQuery

MappingSqlQuery 是一个可重用的查询,其中具体的子类必须实现抽象 mapRow(..) 方法,以将提供的 ResultSet 的每一行转换为指定类型的对象。以下示例显示了一个自定义查询,该查询将 t_actor 关系中的数据映射到 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 参数化。此客户查询的构造函数将 DataSource 作为唯一参数。在此构造函数中,使用 DataSource 和应执行的SQL调用超类上的构造函数以检索此查询的行。此SQL将用于创建 PreparedStatement ,因此它可能包含在执行期间传递的任何参数的占位符。您必须使用传入 SqlParameterdeclareParameter 方法声明每个参数。 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检索客户。由于我们只想要返回一个对象,我们只需调用带有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中定义。此类提供了许多类似于查询对象的 execute(..) 方法的 update(..) 方法。 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 for I nOut 参数,这些参数为过程提供 in 值并且还返回一个值。

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

下面是一个简单DAO的示例,它使用 StoredProcedure 来调用函数 sysdate() ,该函数随任何Oracle数据库一起提供。要使用存储过程功能,您必须创建一个扩展 StoredProcedure 的类。在此示例中, StoredProcedure 类是内部类,但如果需要重用 StoredProcedure ,则将其声明为顶级类。此示例没有输入参数,但使用类 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>());
    }
}

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

TitleMapper 类为提供的 ResultSet 中的每一行将 ResultSet 映射到 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;
    }
}

GenreMapper 类为提供的 ResultSet 中的每一行将 ResultSet 映射到 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的定义中具有一个或多个输入参数的存储过程,您可以编写一个强类型的 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 类提供的更高抽象。所有这些方法都使用了用于实际管理LOB(大对象)数据的 LobHandler 接口的实现。 LobHandler 通过 getLobCreator 方法提供对 LobCreator 类的访问,用于创建要插入的新LOB对象。

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

  • BLOB

  • byte[] - getBlobAsBytessetBlobAsBytes

  • InputStream - getBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

  • String - getClobAsStringsetClobAsString

  • InputStream - getClobAsAsciiStreamsetClobAsAsciiStream

  • Reader - getClobAsCharacterStreamsetClobAsCharacterStream

下一个示例显示如何创建和插入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) { 
        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 ,在此示例中为普通 DefaultLobHandler

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

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

如果从DefaultLobHandler.getLobCreator()返回的LobCreator上调用setBlobAsBinaryStream,setClobAsAsciiStream或setClobAsCharacterStream方法,则可以选择为contentLength参数指定负值。如果指定的内容长度为负,则DefaultLobHandler将使用set-stream方法的JDBC 4.0变体而不使用length参数;否则,它会将指定的长度传递给驱动程序。请参阅正在使用的JDBC驱动程序的文档,以验证是否支持流式传输LOB而不提供内容长度。

现在是时候从数据库中读取LOB数据了。同样,您使用 JdbcTemplate 具有相同的实例变量 lobHandler 和对 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子句的值列表

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

传递许多值时要小心。 JDBC标准不保证您可以为表达式列表使用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 ,如以下示例所示,或 ArrayDescriptor

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 的另一个用途是将值数组传递给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>

上述配置创建了一个嵌入式HSQL数据库,该数据库填充了来自类路径根目录中的 schema.sqltest-data.sql 资源的SQL。此外,作为最佳实践,将为嵌入式数据库分配唯一生成的名称。嵌入式数据库作为 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 选择嵌入式数据库类型

使用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 使用嵌入式数据库测试数据访问逻辑

嵌入式数据库提供了一种轻量级的方法来测试数据访以下是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重用时,使用这样的模板对于一次性可能很有用。但是,如果您希望创建在测试套件中共享的嵌入式数据库,请考虑使用 Spring TestContext Framework 并将嵌入式数据库配置为Spring ApplicationContext 中的bean,如 Section 19.8.2, “Creating an embedded database using Spring XML”Section 19.8.3, “Creating an embedded database programmatically” 中所述。

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名称空间元素和Java Config的 EmbeddedDatabaseBuilder 内部使用)将将嵌入数据库的名称设置为 "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 实现,例如连接池来管理嵌入式数据库连接。

我们鼓励您在 jira.spring.io 为Spring社区贡献回扩展。

19.9 初始化DataSource

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中的资源(例如 classpath*:/com/foo/**/sql/*-data.sql )。如果使用模式,脚本将按其URL或文件名的词汇顺序执行。

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

但是,为了更好地控制现有数据的创建和删除,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 的单独组件。当应用程序上下文启动时, SmartLifecycle 可以自动启动 autoStartup 标志已设置,并且可以通过在封闭上下文中调用 ConfigurableApplicationContext.start() 手动启动 Lifecycle

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

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

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

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

Updated at: 5 months ago
18.3. 注释用于配置DAO或Repository类Table of content20. 对象关系映射(ORM)数据访问
Comment
You are not logged in.

There are no comments.