20220507 3. Data Access - Data Access with JDBC

前言

文档地址

Spring Framework JDBC 抽象提供的价值可能最好通过下表中列出的操作序列来展示。该表显示了 Spring 负责哪些操作以及您负责哪些操作。

操作 Spring You
定义连接参数 Y
打开连接 Y
指定 SQL 语句 Y
声明参数并提供参数值 Y
准备并运行 statement Y
设置循环以遍历结果(如果有) Y
定义每次迭代需要做的工作 Y
处理任何异常 Y
处理事务 Y
关闭连接(connection)、语句(statement)和结果集(resultset) Y

选择 JDBC 数据库的访问方法

您可以从多种方法中进行选择,以构成 JDBC 数据库访问的基础。除了三种 JdbcTemplate 风格,新的 SimpleJdbcInsertSimpleJdbcCall 方法优化数据库的元数据和 RDBMS 对象样式采用类似于 JDO 的查询设计的一个更面向对象的方法。一旦您开始使用这些方法之一,您仍然可以混合搭配以包含来自不同方法的功能。所有方法都需要兼容 JDBC 2.0 的驱动程序,一些高级功能需要 JDBC 3.0 驱动程序。

  • JdbcTemplate 是经典且最流行的 Spring JDBC 方法。它是“最低级别”的,所有其他方式都在底层使用 JdbcTemplate
  • NamedParameterJdbcTemplate 包装 JdbcTemplate 以提供命名参数而不是传统的 JDBC ? 占位符。当 SQL 语句有多个参数时,此方法可提供更好的易用性
  • SimpleJdbcInsertSimpleJdbcCall 优化数据库元数据以限制必要配置的数量。这种方式简化了编码,因此您只需提供表或存储过程的名称,并提供与列名称匹配的参数映射。这仅在数据库提供足够的元数据时才有效。如果数据库不提供此元数据,则必须提供参数的显式配置
  • RDBMS 对象(包括 MappingSqlQuerySqlUpdateStoredProcedure )要求您在数据访问层初始化期间创建可重用和线程安全的对象。此方式类似 JDO 查询,在其中定义查询字符串、声明参数并编译查询。这种方式下, execute(…)update(…) ,和 findObject(…) 方法可以用不同的参数值进行多次调用

包层次结构

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

  • coreorg.springframework.jdbc.core 包中包含 JdbcTemplate 类及其各种回调接口,以及各种相关的类。名为 org.springframework.jdbc.core.simple 的子包中包含 SimpleJdbcInsertSimpleJdbcCall 类。名为 org.springframework.jdbc.core.namedparam 的子包中包含 NamedParameterJdbcTemplate 类和相关的支持类。请参阅 使用 JDBC 核心类控制基本 JDBC 处理和错误处理JDBC 批处理操作使用 SimpleJdbc 类简化 JDBC 操作
  • datasourceorg.springframework.jdbc.datasource 包中包含易于访问 DataSource 的实用程序类和各种简单的 DataSource 实现,可用于在 Java EE 容器之外测试和运行未经修改的 JDBC 代码。名为 org.springfamework.jdbc.datasource.embedded 的子包支持使用 Java 数据库引擎(如 HSQL、H2 和 Derby)创建嵌入式数据库。请参阅 控制数据库连接嵌入式数据库支持
  • objectorg.springframework.jdbc.object 包中包含将 RDBMS 查询、更新和存储过程表示为线程安全、可重用对象的类。请参阅 将 JDBC 操作建模为 Java 对象 。这种方法是由 JDO 建模的,尽管查询返回的对象自然与数据库断开连接。这种较高级别的 JDBC 抽象取决于 org.springframework.jdbc.core 包中的较低级别抽象
  • supportorg.springframework.jdbc.support 包提供 SQLException 翻译功能和一些实用程序类。JDBC 处理期间抛出的异常被转换为 org.springframework.dao 包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有已翻译的异常都是非检查异常,这使您可以选择捕获可以从中恢复的异常,同时将其他异常传播给调用者。请参阅 使用 SQLExceptionTranslator

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

使用 JdbcTemplate

JdbcTemplate 是 JDBC 核心包中的中心类。它处理资源的创建和释放,帮助您避免常见错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务(如语句创建和执行),让应用程序代码来提供 SQL 和提取结果。JdbcTemplate 类提供以下功能:

  • 运行 SQL 查询
  • 更新语句和存储过程调用
  • ResultSet 执行迭代并提取返回的参数值
  • 捕获 JDBC 异常并将它们转换为 org.springframework.dao 包中定义的通用的异常层次结构(请参阅一致的异常层次结构

使用 JdbcTemplate 时,你只需要实现回调接口,给它们一个明确定义的契约。给定由 JdbcTemplate 类提供的 ConnectionPreparedStatementCreator 回调接口创建一个 prepared 语句,提供 SQL 和任何必要的参数。对于创建可调用语句的 CallableStatementCreator 接口也是如此 。 RowCallbackHandler 接口从 ResultSet 的每一行中提取值。

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

DataSource 应该始终被配置为 Spring IoC 容器的 bean 。其一,bean 直接提供给服务;其二,它被提供给准备好的模板。

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

  • org.springframework.jdbc.core.JdbcTemplate
查询 ( 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 = ?",
        String.class, 1212L);

查询返回单个对象:

Actor actor = jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        (resultSet, rowNum) -> {
            Actor newActor = new Actor();
            newActor.setFirstName(resultSet.getString("first_name"));
            newActor.setLastName(resultSet.getString("last_name"));
            return newActor;
        },
        1212L);

查询返回对象列表:

List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        (resultSet, rowNum) -> {
            Actor actor = new Actor();
            actor.setFirstName(resultSet.getString("first_name"));
            actor.setLastName(resultSet.getString("last_name"));
            return actor;
        });

抽取 RowMapper

private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
};

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query( "select first_name, last_name from t_actor", actorRowMapper);
}
使用 JdbcTemplate 更新 ( INSERT , UPDATE , 和 DELETE )

您可以使用 update(..) 方法执行插入、更新和删除操作。参数值通常作为可变参数或者对象数组提供。

插入:

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 t_actor where id = ?",
        Long.valueOf(actorId));
JdbcTemplate 的其他操作

可以使用 execute(..) 方法执行任意 SQL 。因此,该方法通常用于 DDL 语句。它重载了带有回调接口、绑定变量数组等的变体。

创建一个表:

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

调用存储过程:

this.jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        Long.valueOf(unionId));
JdbcTemplate 最佳实践

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

使用 JdbcTemplate 类(和关联的 NamedParameterJdbcTemplate 类)时的常见做法是在 Spring 配置文件中配置 DataSource ,然后将该共享 DataSource bean 依赖注入到 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 配置:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://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 以及随后多个不同配置的 JdbcTemplate 实例。

使用 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate 类通过使用命名参数来支持 JDBC 语句,而不是只使用常规的占位符 ?NamedParameterJdbcTemplate 类包装并委托 JdbcTemplate 来完成大部分工作。本节仅描述 NamedParameterJdbcTemplate 类中与 JdbcTemplate 自身不同的部分——使用命名参数编写 JDBC 语句。以下示例显示了如何使用 NamedParameterJdbcTemplate

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

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

public int countOfActorsByFirstName(String firstName) {
    String sql = "select count(*) 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);
}

SqlParameterSourceNamedParameterJdbcTemplate 的命名参数值的来源。MapSqlParameterSource 类是一个简单的实现,它是一个围绕 java.util.Map 的适配器,其中的键是参数名称,值是参数值。

另一个 SqlParameterSource 实现是 BeanPropertySqlParameterSource 类。此类包装任意 JavaBean(即,遵守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...

}

下面的示例使用 NamedParameterJdbcTemplate 返回前面示例中显示的类的成员计数:

// 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 实现了 JdbcOperations 接口

  • org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
  • org.springframework.jdbc.core.JdbcOperations

使用 SQLExceptionTranslator

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

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

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

  1. 由子类实现的任何自定义转换。通常使用提供的具体 SQLErrorCodeSQLExceptionTranslator ,因此此规则不适用。仅当您实际提供了子类实现时,它才适用
  2. 作为 SQLErrorCodes 类的 customSqlExceptionTranslator 属性提供的 SQLExceptionTranslator 接口的任何自定义实现
  3. 搜索 CustomSQLErrorCodesTranslation 类的实例列表(为 SQLErrorCodes 类的 customTranslations 属性提供 )以查找匹配项
  4. 应用错误代码匹配
  5. 使用后背翻译器。SQLExceptionSubclassTranslator 是默认的后备翻译器。如果此翻译不可用,则下一个后备翻译器是 SQLStateSQLExceptionTranslator

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

您可以扩展 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);
}
  • org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
  • org.springframework.jdbc.support.SQLExceptionTranslator
  • org.springframework.jdbc.support.SQLErrorCodes
  • sql-error-codes.xml

运行语句( Statement )

运行 SQL 语句只需要 DataSourceJdbcTemplate

创建新表:

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

运行查询

某些查询方法返回单个值。要从一行中检索计数或特定值,请使用 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}]

更新数据库

以下示例更新某行的列数据:

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

在前面的示例中,SQL 语句具有行参数的占位符。您可以将参数值作为可变参数或作为对象数组传递。

获取自动生成的主键值

update() 便利方法支持获取数据库生成的主键。这种支持是 JDBC 3.0 标准的一部分。该方法将 PreparedStatementCreator 作为其第一个参数,这就是指定所需插入语句的方式。另一个参数是 KeyHolder ,它包含从更新成功返回时生成的 key 。没有标准的单一方法来创建适当的 PreparedStatement (这解释了为什么方法签名是这样的)。以下示例适用于 Oracle,但可能不适用于其他平台:

final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";

KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
    PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
    ps.setString(1, name);
    return ps;
}, keyHolder);

// keyHolder.getKey() now contains the generated key

控制数据库连接( Connection )

使用 DataSource

Spring 通过 DataSource 获取连接。DataSource 是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架对应用程序代码隐藏连接池和事务管理问题。作为开发人员,您无需了解有关如何连接到数据库的详细信息。

在使用 Spring 的 JDBC 层时,可以从 JNDI 获取数据源,也可以使用第三方提供的连接池实现来配置自己的数据源。传统的选择是带有 bean 样式的 DataSource 类的 Apache Commons DBCP 和 C3P0 ;对于现代 JDBC 连接池,请考虑使用 Builder 样式的 API 的 HikariCP 。

您应该仅将 DriverManagerDataSourceSimpleDriverDataSource 类用于测试目的!当对一个连接发出多个请求时,这些变体不提供池化并且性能不佳。

以下部分使用 Spring 的 DriverManagerDataSource 实现。

配置 DriverManagerDataSource

  1. 通过 DriverManagerDataSource 获得连接,就像通常获取 JDBC 连接一样
  2. 指定 JDBC 驱动程序的完全限定类名,以便 DriverManager 可以加载驱动程序类
  3. 提供因 JDBC 驱动程序而不同的的 URL
  4. 提供连接到数据库的用户名和密码

以下示例显示了如何在 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 配置:

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

使用 DataSourceUtils

DataSourceUtils 类是一个方便且功能强大的辅助类,提供 static 方法来从 JNDI 获取和关闭连接。它支持线程绑定连接,例如 DataSourceTransactionManager

实现 SmartDataSource

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

扩展 AbstractDataSource

AbstractDataSource 是 Spring DataSource 实现的 abstract 基类。它实现了所有 DataSource 实现通用的代码。如果您编写自己的 DataSource 实现,则应该扩展 AbstractDataSource 类。

使用 SingleConnectionDataSource

SingleConnectionDataSource 类是 SmartDataSource 的实现,它封装了一个在每次使用后都不会关闭的 Connection 。不支持多线程。

如果任何客户端代码在假定池连接的情况下调用 close(如使用持久层工具时),则应将 suppressClose 属性设置为 true 。此设置返回一个封装物理连接的关闭代理。请注意,您不能再将其强制转换为原生 Oracle Connection 或类似对象。

SingleConnectionDataSource 主要是一个测试类。结合简单的 JNDI 环境,它通常可以轻松测试应用程序服务器外部的代码。与 DriverManagerDataSource 不通,它重用同一个连接,避免过度创建物理连接。

使用 DriverManagerDataSource

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

此实现对于 Java EE 容器之外的测试和独立环境非常有用,可以作为 Spring IoC 容器中的 DataSource bean 或与简单的 JNDI 环境结合使用。使用 JavaBean 样式的连接池(例如 commons-dbcp )非常容易,即使在测试环境中也是如此,因此使用此类连接池几乎总是优于 DriverManagerDataSource

使用 TransactionAwareDataSourceProxy

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

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

使用 DataSourceTransactionManager

DataSourceTransactionManager 类是针对单个 JDBC 数据源的 PlatformTransactionManager 实现。它将来自指定数据源的 JDBC 连接绑定到当前正在执行的线程,允许一个线程有一个数据源连接。

需要 DataSourceUtils.getConnection(DataSource) 来检索 JDBC 连接, 而不是通过 Java EE 的标准 DataSource.getConnection 。它抛出非检查的 org.springframework.dao 异常而不是检查异常 SQLExceptions 。所有框架类(例如 JdbcTemplate )都隐式地使用此策略。如果没有与此事务管理器一起使用,则查找策略的行为完全类似于常见的策略。因此,它可以在任何情况下使用。

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

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

JDBC 批量操作

如果对同一条语句进行多个批处理调用,大多数 JDBC 驱动程序都可以提供优化。通过将更新分组为批,可以限制到数据库的往返次数。

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

通过实现特殊接口 BatchPreparedStatementSetter 的两个方法,并将该实现作为 batchUpdate 方法调用中的第二个参数传入 JdbcTemplate 来完成批处理。您可以使用 getBatchSize 方法获取当前批次的大小。您可以使用 setValues 方法为准备好的语句的参数设置值。此方法的调用次数为 getBatchSize 返回的次数。以下示例根据列表中的条目更新 t_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 {
                        Actor actor = actors.get(i);
                        ps.setString(1, actor.getFirstName());
                        ps.setString(2, actor.getLastName());
                        ps.setLong(3, actor.getId().longValue());
                    }
                    
                    public int getBatchSize() {
                        return actors.size();
                    }
                });
    }

    // ... additional methods
}

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

使用对象列表的批处理操作

JdbcTemplateNamedParameterJdbcTemplate 都提供了批处理更新的替代方式。您没有实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值并使用内部准备好的语句设置器。API 会有所不同,具体取决于您是否使用命名参数。对于命名参数,您提供一个 SqlParameterSource 数组,为批处理的每个成员提供一个条目。您可以使用方便的方法 SqlParameterSourceUtils.createBatch 来创建这个数组,传入一个 bean 样式的对象数组(具有对应于参数的 getter 方法)、键类型为 StringMap 实例(包含相应的参数作为值)或两者的混合。

以下示例显示了使用命名参数的批量更新:

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

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

在这种情况下,通过在底层 PreparedStatement 上自动设置值,需要从给定的 Java 类型派生每个值的相应 JDBC 类型。虽然这通常效果很好,但也有可能出现问题(例如,包含 null 值的 Map )。默认情况下,Spring 会在这种情况下调用 ParameterMetaData.getParameterType ,这对于您的 JDBC 驱动程序来说可能代价很高。如果遇到性能问题(如 Oracle 12c、JBoss 和 PostgreSQL 报告的那样),您应该使用最新的驱动程序版本并考虑将 spring.jdbc.getParameterType.ignore 属性设置为 true (作为 JVM 系统属性或通过 Spring Properties 机制)。

或者,您可以考虑显式指定相应的 JDBC 类型,可以通过 BatchPreparedStatementSetter(如前面所示)、通过为基于 List<Object[]> 调用提供的显式类型数组、通过对自定义 MapSqlParameterSource 实例上的 registerSqlType 调用,或通过甚至为 null 值从 Java 声明的属性类型派生 SQL 类型的 BeanPropertySqlParameterSource

多批次的批处理操作

前面的批量更新示例处理的批次太大,以至于您希望将它们分成几个较小的批次。您可以使用前面提到的方法通过多次调用 batchUpdate 方法来做到这一点,但现在有一个更方便的方法。除了 SQL 语句之外,此方法还需要一个包含参数的 Collection 对象、每个批次要进行的更新次数,以及设置准备好的语句的参数值的 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,
                (PreparedStatement ps, Actor actor) -> {
                    ps.setString(1, actor.getFirstName());
                    ps.setString(2, actor.getLastName());
                    ps.setLong(3, actor.getId().longValue());
                });
        return updateCounts;
    }

    // ... additional methods
}

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

使用 SimpleJdbc 简化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall 类通过利用可通过 JDBC 驱动被检索的数据库元数据提供了一个简化的配置。这意味着您预先配置很少,但如果您希望在代码中提供所有详细信息,您可以覆盖或关闭元数据处理。

  • org.springframework.jdbc.core.simple.SimpleJdbcInsert
  • org.springframework.jdbc.core.simple.SimpleJdbcCall

使用 SimpleJdbcInsert 插入数据

我们首先查看具有最少配置选项的 SimpleJdbcInsert 类。您应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert 。对于本例,初始化方法是 setDataSource 方法。您不需要子类化 SimpleJdbcInsert 。相反,您可以使用 withTableName 方法创建一个新实例并设置表名。此类的配置方法遵循流式(fluid)样式,返回 SimpleJdbcInsert 实例,它允许您链接所有配置方法。以下示例仅使用一个配置方法:

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource 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.util.Map 作为它的唯一参数。这里要注意的重要一点是,用于 Map 的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的插入语句。

使用 SimpleJdbcInsert 获取自动生成的主键值

下一个示例使用与前一个相同的插入,但它不是传入id,而是检索自动生成的主键并将其设置在新的 Actor 对象上。创建 SimpleJdbcInsert 的时候,除了指定表名外,还指定了 usingGeneratedKeyColumns 方法生成的 key 列的名称。以下清单显示了它的工作原理:

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource 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
}

主要区别是,你不需要添加 idMap ,而是调用 executeAndReturnKey 方法。这将返回一个 java.lang.Number 对象。如果您有多个自动生成的列或生成的值是非数字的,您可以使用从 executeAndReturnKeyHolder 方法返回的 KeyHolder

SimpleJdbcInsert 指定列

通过使用 usingColumns 方法指定列名列表来限制插入的列 ,如以下示例所示:

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource 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
}

插入的执行与依赖元数据来确定要使用哪些列相同。

使用 SqlParameterSource 提供参数值

使用 Map 提供参数值不是最方便的。Spring 提供了一些您可以使用的 SqlParameterSource 接口实现。第一个是 BeanPropertySqlParameterSource ,针对有一个包含参数值的的符合 JavaBean 的类。它使用相应的 getter 方法来提取参数值。以下示例显示了如何使用 BeanPropertySqlParameterSource

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource 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 SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource 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
}

SimpleJdbcCall 调用存储过程

SimpleJdbcCall 类使用数据库中的元数据查找名称为 inout 的参数,使您不必明确声明他们。如果您愿意这样做,或者如果存在没有自动映射到 Java 类的参数(例如 ARRAYSTRUCT ),可以声明参数。

第一个示例显示了一个简单的存储过程,该过程仅返回 MySQL 数据库中的 VARCHARDATE 格式的标量值。示例过程读取指定的 actor 并以 out 参数的形式返回 first_namelast_name 以及 birth_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;

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

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource 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 参数并返回一个包含以名称为键的任何 out 参数的 Map ,如存储过程中指定的那样。在本例中,他们是 out_first_nameout_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 参数名称的情况下发生冲突。

显式声明用于 SimpleJdbcCall 的参数

在本章前面,我们描述了如何从元数据中推导出参数,但如果您愿意,也可以显式声明它们。可以通过创建和配置 SimpleJdbcCalldeclareParameters 方法,该方法采用可变数目的 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
}

两个例子的执行和最终结果是一样的。第二个示例明确指定所有详细信息,而不是依赖元数据。

如何定义 SqlParameters

要为 SimpleJdbc 类以及 RDBMS 操作类(在 将 JDBC 操作建模为 Java 对象中介绍 )定义参数,您可以使用 SqlParameter 或其子类。为此,您通常在构造函数中指定参数名称和 SQL 类型。SQL 类型是通过使用 java.sql.Types 常量指定的。在本章前面,我们看到了类似于以下内容的声明:

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

带有 SqlParameter 的第一行声明了一个 IN 参数。通过使用 SqlQuery 及其子类,您可以将 IN 参数用于存储过程调用和查询。

第二行(带有 SqlOutParameter )声明要在存储过程调用中使用的 out 参数。还有一个 SqlInOutParameter 声明 InOut 参数(为过程提供 IN 值并返回值的参数)。

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

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

使用 SimpleJdbcCall 调用存储函数

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

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
}

executeFunction 方法用于返回函数调用的 String 返回值。

SimpleJdbcCall 返回 ResultSet 或 REF 游标

调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库需要一个特定类型的显式注册参数 out 。这两种方法都需要额外的处理来循环结果集并处理返回的行。使用 SimpleJdbcCall ,您可以使用 returningResultSet 方法并声明要用于特定参数的 RowMapper 实现。如果在结果处理期间返回结果集,则没有定义名称,因此返回的结果必须与声明 RowMapper 实现的顺序相匹配。指定的名称仍用于在从 execute 语句返回的结果映射中存储已处理的结果列表。

下一个示例(对于 MySQL)使用一个存储过程,它不接受 IN 参数并返回 t_actor 表中的所有行:

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 规则,所以可以使用通过在 newInstance 方法中传入要映射到的所需类而创建的 BeanPropertyRowMapper 。以下示例显示了如何执行此操作:

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadAllActors;

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

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

    // ... additional methods
}

execute 调用传递一个空的 Map ,因为此调用不带任何参数。然后从结果映射中检索参与者列表并返回给调用者。

将 JDBC 操作建模为 Java 对象

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

许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类( StoredProcedure 类除外)通常可以用 JdbcTemplate 直接调用代替。通常,编写直接在 JdbcTemplate 上调用方法的 DAO 方法更简单(而不是将查询封装为成熟的类)。

但是,如果您从使用 RDBMS 操作类中获得了可衡量的价值,那么您应该继续使用这些类。

理解 SqlQuery

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

  • org.springframework.jdbc.object.SqlQuery

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

示例类扩展了 MappingSqlQuery ,泛型类型是 Actor 。构造函数将 DataSource 作为唯一参数。在这个构造函数中,您可以使用 DataSource 和 SQL 调用超类上的构造函数来检索此查询的行。此 SQL 用于创建 PreparedStatement ,因此它可能包含在执行期间传入的任何参数的占位符。您必须通过使用 declareParameter 方法来声明每个参数,并传入一个 SqlParameterSqlParameter 需要一个名称和在 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 。如果我们有一个返回对象列表并接受其他参数的查询,我们将使用其中一种 execute 方法,该方法接受作为可变参数传入的参数值数组。以下示例显示了这样的方法:

public List<Actor> searchForActors(int age, String namePattern) {
    List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
    return actors;
}
  • org.springframework.jdbc.object.MappingSqlQuery

使用 SqlUpdate

SqlUpdate 类封装了一个 SQL 更新。与查询一样,更新对象是可重用的,并且与所有 RdbmsOperation 类一样,更新可以有参数并在 SQL 中定义。此类提供了许多类似于 execute(..)查询对象方法的 update(..) 方法。SQLUpdate 类是具体的。它可以被子类化——例如,添加自定义更新方法。但是,您不必对 SqlUpdate 类进行子类化,因为可以通过设置 SQL 和声明参数轻松地对其进行参数化。以下示例创建一个名为 execute 的自定义更新方法:

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);
    }
}
  • org.springframework.jdbc.object.SqlUpdate

使用 StoredProcedure

StoredProcedure 类是一个用于 RDBMS 存储过程的对象的抽象超类。

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

要为 StoredProcedure 类定义参数,您可以使用 SqlParameter 或它的子类。您必须在构造函数中指定参数名称和 SQL 类型,如以下代码片段所示:

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

SQL 类型是使用 java.sql.Types 常量指定的。

第一行(带有 SqlParameter )声明了一个 IN 参数。您可以将 IN 参数用于存储过程调用和使用 SqlQuery 及其子类的查询。

第二行(带有 SqlOutParameter )声明了要在存储过程调用中使用的 out 参数。还有一个 SqlInOutParameter 声明 InOut参数(为过程提供 in 值并返回值的参数)。

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

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

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 类映射 ResultSetResultSet 提供的各行的 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 类映射 ResultSetResultSet 提供的各行的 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) 方法,如以下示例所示:

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);
    }
}
  • org.springframework.jdbc.object.StoredProcedure

参数和数据值处理的常见问题

为参数提供 SQL 类型信息

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

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

  • JdbcTemplate 的许多 update 和 query 方法都以 int 数组的形式接受额外的参数。该数组用于通过使用 java.sql.Types 类中的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。

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

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

  • java.sql.Types

处理 BLOB 和 CLOB 对象

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

LobCreatorLobHandler 为 LOB 输入和输出提供以下支持:

  • BLOB
    • byte[]getBlobAsBytessetBlobAsBytes
    • InputStreamgetBlobAsBinaryStreamsetBlobAsBinaryStream
  • CLOB
    • StringgetClobAsStringsetClobAsString
    • InputStreamgetClobAsAsciiStreamsetClobAsAsciiStream
    • ReadergetClobAsCharacterStreamsetClobAsCharacterStream

下一个示例显示如何创建和插入 BLOB 。

此示例使用 JdbcTemplateAbstractLobCreatingPreparedStatementCallback 的实现 。它实现了 setValues 方法。此方法提供了我们用来设置 SQL 插入语句中 LOB 列的值的 LobCreator

对于此示例,我们假设有一个变量 ,lobHandler ,已设置为 DefaultLobHandler 的实例。通常通过依赖注入设置此值。

以下示例显示了如何创建和插入 BLOB :

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

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

现在是时候从数据库中读取 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");  
            results.put("CLOB", clobText);
            byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");  
            results.put("BLOB", blobBytes);
            return results;
        }
    });

传入 IN 子句的值列表

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

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

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

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

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

SqlReturnType 接口有一个必须实现的方法(名为 getTypeValue )。此接口用作 SqlOutParameter 声明的一部分。以下示例显示返回用户声明类型 ITEM_TYPE 的 Oracle STRUCT 对象的值 :

public class TestItemStoredProcedure extends StoredProcedure {

    public TestItemStoredProcedure(DataSource dataSource) {
        // ...
        declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
            (CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
                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 实例。以下示例创建一个 StructDescriptor 实例:

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 调用存储过程的输入参数的 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;
    }
};

嵌入式数据库支持

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

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

由于其轻量级的特性,嵌入式数据库在项目的开发阶段非常有用。优点包括易于配置、启动时间短、可测试性以及在开发过程中快速改进 SQL 的能力。

使用 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 容器 ,然后可以根据需要将其注入到数据访问对象中。

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

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

还可以使用 EmbeddedDatabaseBuilder 以 Java 配置来创建嵌入式数据库,如以下示例所示:

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

选择嵌入式数据库类型

使用 HSQL

Spring 支持 HSQL 1.8.0 及更高版本。如果没有明确指定,HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将 embedded-database 标签的 type 属性设置为 HSQL 。如果您使用构建器 API,请使用 EmbeddedDatabaseType.HSQLsetType(EmbeddedDatabaseType)

使用 H2

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

使用 Derby

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

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

嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重复使用时,使用这样的模板对于一次性使用很有用。但是,如果您希望创建在测试套件中共享的嵌入式数据库,请考虑使用 Spring TestContext Framework 并将嵌入式数据库配置为 Spring ApplicationContext 中的 bean 。以下清单显示了测试模板:

public class DataAccessIntegrationTestTemplate {

    private EmbeddedDatabase db;

    @BeforeEach
    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( /* ... */ );
    }

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

}

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

如果开发团队的测试套件无意中尝试重新创建同一数据库的其他实例,则开发团队经常会遇到嵌入式数据库的错误。如果 XML 配置文件或 @Configuration 类负责创建嵌入式数据库,然后在同一测试套件(即同一 JVM 进程)中的多个测试场景中重用相应的配置,则这很容易发生。例如,集成针对嵌入式数据库的测试,ApplicationContext 配置仅在哪些 bean 定义 profile 处于激活状态方面有所不同。

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

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

  • EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
  • EmbeddedDatabaseBuilder.generateUniqueName()
  • <jdbc:embedded-database generate-name="true" … >

扩展嵌入式数据库支持

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

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

初始化 DataSource

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

使用 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 或文件名的词法顺序运行。

数据库初始值设定项的默认行为是无条件运行提供的脚本。这可能并不总是您想要的。例如,如果您针对已包含测试数据的数据库运行脚本。通过遵循先创建表然后插入数据的常见模式(如前面所示),可以降低意外删除数据的可能性。如果表已存在,则第一步将失败。

但是,为了更好地控制现有数据的创建和删除,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 语句会失败。所以失败的 DROP SQL 语句将被忽略,但其他失败将导致异常。如果您的 SQL 方言不支持 DROP … IF EXISTS ,但您想在重新创建之前无条件地删除所有测试数据,这将很有用。在这种情况下,第一个脚本通常是一组 DROP 语句,然后是一组 CREATE 语句。

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

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

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

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

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

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

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

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

如果应用程序在您的控制之下,则更改缓存初始化策略可能很容易。关于如何实现的一些建议包括:

  • 使缓存在第一次使用时延迟初始化,从而缩短应用程序启动时间
  • 让您的缓存或初始化缓存的单独组件实现 LifecycleSmartLifecycle 。当应用程序上下文启动时,您可以通过设置 autoStartup 标志来自动启动 SmartLifecycle ,并且您可以通过调用 ConfigurableApplicationContext.start() 来手动启动 Lifecycle
  • 使用 Spring ApplicationEvent 或类似的自定义观察器机制来触发缓存初始化。当上下文准备好使用时(在所有 bean 已经初始化之后),ContextRefreshedEvent 总是由上下文发布,所以这通常是一个有用的钩子(这是默认情况下 SmartLifecycle 的工作方式)

确保首先初始化数据库初始化器也很容易。关于如何实现的一些建议包括:

  • 依赖 Spring BeanFactory 的默认行为,即 bean 按注册顺序初始化。您可以通过采用 XML 配置中的一组 <import/> 元素来对应用程序模块进行排序的常见做法,并确保首先列出数据库和数据库初始化
  • DataSource 和使用它的业务组件分开,并通过将它们放在单独的 ApplicationContext 实例中来控制它们的启动顺序(例如,父上下文包含 DataSource ,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见
posted @ 2022-06-09 21:19  流星<。)#)))≦  阅读(64)  评论(0编辑  收藏  举报