青年IT男

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。微信公众号:青年IT男。

Spring 5 中文解析数据存储篇-JDBC数据存储(上)

Spring 5 中文解析数据存储篇-JDBC数据存储(上)

下表概述的操作序列可能最好地显示了Spring框架JDBC抽象提供的值。该表显示了Spring负责哪些操作以及哪些操作是你需要做的。

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

Spring框架负责处理使JDBC成为如此乏味的API的所有底层细节。

3.1 选择一种用于JDBC数据库访问的方法

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

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

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

  • core: org.springframework.jdbc.core包含JdbcTemplate类及其各种回调接口,以及各种相关类。名为org.springframework.jdbc.core.simple的子包包含SimpleJdbcInsertSimpleJdbcCall类。另一个名为org.springframework.jdbc.core.namedparam的子包包含NamedParameterJdbcTemplate类和相关的支持类。请参阅使用JDBC核心类控制基本JDBC处理和错误处理、JDBC批处理操作和使用SimpleJdbc类简化JDBC操作
  • datasource: org.springframework.jdbc.datasource包含一个实用程序类,用于轻松访问DataSource和各种简单DataSource实现,可用于在Java EE容器之外测试和运行未修改的JDBC代码。名为org.springfamework.jdbc.datasource.embedded的子包提供了对使用Java数据库引擎(例如HSQLH2Derby)创建嵌入式数据库的支持。请参阅控制数据库连接嵌入式数据库支持。
  • object: org.springframework.jdbc.object包含一些类,这些类将RDBMS查询、更新和存储过程表示为线程安全的可重用对象。请参阅将JDBC操作建模为Java对象。尽管查询返回的对象自然会与数据库断开连接,但是JDO对此方法进行了建模。较高级别的JDBC抽象依赖于org.springframework.jdbc.core包中的较低级别的抽象。
  • support: org.springframework.jdbc.support包提供了SQLException转换功能和一些实用程序类。JDBC处理期间引发的异常将转换为org.springframework.dao包中定义的异常。这意味着使用Spring JDBC抽象层的代码不需要实现JDBCRDBMS特定的错误处理。所有转换的异常均为uncheck异常,这使你可以选择捕获可从中恢复的异常,同时将其他异常传递到调用方。请参见使用SQLExceptionTranslator
3.3 使用JDBC核心类控制JDBC处理和错误处理

本节介绍如何使用JDBC核心类控制JDBC处理,包括错误处理。它包括以下主题:

3.3.1 使用JdbcTemplate

JdbcTemplateJDBCcore包中的核心类。它处理资源的创建和释放,这有助于你避免常见的错误,例如忘记关闭连接。它执行核心JDBC工作流程的基本任务(例如,语句创建和执行),而使应用程序代码提供SQL并提取结果。JdbcTemplate类:

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

当将JdbcTemplate用于代码时,只需实现回调接口,即可为它们提供明确定义的约定。给定JdbcTemplate类提供的ConnectionPreparedStatementCreator回调接口将创建一条准备好的语句,提供SQL和任何必要的参数。对于CallableStatementCreator接口(创建可调用语句)也是如此。 RowCallbackHandler接口从ResultSet的每一行提取值。

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

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

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

以下各节提供了JdbcTemplate用法的一些示例。这些示例不是JdbcTemplate暴露的所有功能的详尽列表。请参考附带的javadoc

查询(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 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 lambda表达式中存在的重复项并将它们提取到单个字段中,然后可以根据需要由DAO方法引用,这是有意义的。

例如,使用前面编写的代码段,如下所示:

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

参考代码:org.liyong.dataaccess.starter.JdbcTemplateTransactionManagerIocContainer

其他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类中。 JdbcTemplate在数据源的设置器中创建。这导致类似于以下内容的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 //1
public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

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

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
  1. @Repository注释类。
  2. @Autowired注释DataSource setter方法。
  3. 使用DataSource创建一个新的JdbcTemplate

以下示例显示了相应的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实例,这需要多个数据源,然后需要多个不同配置的JdbcTemplate实例。

参考代码:org.liyong.dataaccess.starter.JdbcTemplateBestTransactionManagerIocContainer

3.3.2 使用NamedParameterJdbcTemplate

与仅使用经典占位符(?)编程的JDBC语句相反,NamedParameterJdbcTemplate类增加了使用命名参数对JDBC语句进行编程的支持。NamedParameterJdbcTemplate类包装JdbcTemplate并将其委托给包装的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);
}

SqlParameterSource接口是与NamedParameterJdbcTemplate相关的一个不错的功能(并且存在于同一Java包中)。你已经在前面的代码片段之一(MapSqlParameterSource类)中看到了此接口的实现示例。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最佳实践,以获取有关在应用程序上下文中使用NamedParameterJdbcTemplate类的指导。

参考代码:org.liyong.dataaccess.starter.NamedParameterTransactionManagerIocContainer

标记位置
3.3.3 使用SQLExceptionTranslator

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

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

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

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

默认情况下,使用SQLErrorCodesFactory定义错误代码和自定义异常转换。从类路径的名为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);
}

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

3.3.4 运行语句

运行SQL语句需要很少的代码。你需要一个数据源和一个JdbcTemplate,包括JdbcTemplate提供的便捷方法。下面的示例显示了创建一个新表的最小但功能齐全的类需要包含的内容:

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

public class ExecuteAStatement {

    private JdbcTemplate jdbcTemplate;

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

    public void doExecute() {
        this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
    }
}
3.3.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}]
3.3.6 更新数据库

下面的示例更新某个主键的列:

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语句具有用于行参数的占位符。你可以将参数值作为可变参数或作为对象数组传递。因此,你应该在基本包装器类中显式包装基类型,或者应该使用自动装箱。

3.3.7 检索自动生成的主键

update()便捷方法支持检索由数据库生成的主键。此支持是JDBC 3.0标准的一部分。有关详细信息,请参见规范的第13.6章。该方法将PreparedStatementCreator作为其第一个参数,这是指定所需插入语句的方式。另一个参数是KeyHolder,它包含从更新成功返回时生成的主键。没有标准的单一方法来创建适当的PreparedStatement(这说明了为什么方法签名就是这样)。以下示例在Oracle上有效,但在其他平台上可能不适用:

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

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

// keyHolder.getKey() now contains the generated key
3.4 控制数据库连接

本节内容包括:

3.4.1 使用DataSource

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

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

你仅应将DriverManagerDataSource和SimpleDriverDataSource类(包含在Spring发行版中)用于测试!当发出多个连接请求时,这些变体不提供缓冲池,并且性能不佳。

以下部分使用Spring的DriverManagerDataSource实现。稍后将介绍其他几种DataSource变体。

要配置DriverManagerDataSource:

  1. 通常与JDBC连接一样,获得与DriverManagerDataSource的连接。
  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和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"/>
3.4.2 使用DataSourceUtils

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

3.4.4 扩张AbstractDataSource

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

3.4.5 使用SingleConnectionDataSource

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

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

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

3.4.6 使用DriverManagerDataSource

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

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

3.4.7 使用TransactionAwareDataSourceProxy

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

除非需要调用已经存在的代码并通过标准的JDBC DataSource接口实现,否则很少需要使用此类。在这种情况下,你仍然可以使该代码可用,同时使该代码参与Spring管理的事务。通常,最好使用更高级别的资源管理抽象来编写自己的新代码,例如JdbcTemplate或DataSourceUtils。

有关更多详细信息,请参见TransactionAwareDataSourceProxy javadoc。

3.4.8 使用DataSourceTransactionManager

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

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

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

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

3.5 JDBC批量操作

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

3.5.3 使用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方法使你可以发出批处理结束的信号。

3.5.2 批处理操作的对象列表

JdbcTemplate和NamedParameterJdbcTemplate都提供了另一种提供批处理更新的方式。无需实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值,并使用一个内部语句setter。API会有所不同,具体取决于你是否使用命名参数。对于命名参数,你提供一个SqlParameterSource数组,该批处理的每个成员都有一个条目。你可以使用SqlParameterSourceUtils.createBatch便捷方法创建此数组,传入一个bean样式的对象数组(带有与参数相对应的getter方法),字符串键Map实例(包含对应的参数作为值),或者混合使用。

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

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类型。尽管这通常效果很好,但存在潜在的问题(例如,包含Map的空值)。在这种情况下,Spring默认情况下会调用ParameterMetaData.getParameterType,这对于JDBC驱动程序可能会很昂贵。如果遇到性能问题,则应使用最新的驱动程序版本,并考虑将spring.jdbc.getParameterType.ignore属性设置为true(作为JVM系统属性或在类路径根目录中的spring.properties文件中)。如关于Oracle 12c(SPR-16139)的报道。

或者,你可以考虑通过BatchPreparedStatementSetter(如前所示),通过为基于“List <Object []>的调用提供的显式类型数组,通过在服务器上的“registerSqlType调用来显式指定相应的JDBC类型。自定义“MapSqlParameterSource实例,或者通过BeanPropertySqlParameterSource实例从Java声明的属性类型中获取SQL类型,即使对于null值也是如此。

3.5.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,
                (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。

Spring核心篇章:

Spring 5 中文解析之核心篇-IoC容器

Spring 5 中文解析核心篇-IoC容器之依赖关系

Spring 5 中文解析核心篇-IoC容器之Bean作用域

Spring 5 中文解析核心篇-IoC容器之自定义Bean性质

Spring 5 中文解析核心篇-IoC容器之BeanDefinition继承与容器拓展点

Spring 5 中文解析核心篇-IoC容器之基于注解的容器配置

Spring 5 中文解析核心篇-IoC容器之类路径扫描和组件管理

Spring 5 中文解析核心篇-IoC容器之JSR330标准注解

Spring 5 中文解析核心篇-IoC容器之基于Java容器配置

Spring 5 中文解析核心篇-IoC容器之Environment抽象

Spring 5 中文解析核心篇-IoC容器之ApplicationContext与BeanFactory

Spring 5 中文解析核心篇-IoC容器之Resources

Spring 5 中文解析核心篇-IoC容器之数据校验、数据绑定和类型转换

Spring 5 中文解析核心篇-IoC容器之SpEL表达式

Spring 5 中文解析核心篇-IoC容器之AOP编程(上)

Spring 5 中文解析核心篇-IoC容器之AOP编程(下)

Spring 5 中文解析核心篇-IoC容器之Spring AOP API

Spring测试篇章:

Spring 5 中文解析测试篇-Spring测试

Spring 5 中文解析核心篇-集成测试之概要和集成测试注解

Spring 5 中文解析核心篇-集成测试之TestContext(上)

Spring 5 中文解析核心篇-集成测试之TestContext(中)

Spring 5 中文解析测试篇-集成测试之TestContext(下)

Spring 5 中文解析测试篇-Spring MVC测试框架

Spring 5 中文解析测试篇-WebTestClient

Spring存储篇章:

Spring 5 中文解析数据存储篇-Spring框架的事物支持模型的优势

Spring 5 中文解析数据存储篇-事务同步和声明式事物管理

Spring 5 中文解析数据存储篇-@Transactional使用

Spring 5 中文解析数据存储篇-编程式事物管理

Spring 5 中文解析数据存储篇-DAO支持

完整电子书地址

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公众号:

技术交流群:

0
青年IT男

个人从事金融行业,就职过易极付、思建科技等重庆一流技术团队,目前就职于某网约车平台负责整个支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。

评论已关闭。

This site is protected by wp-copyrightpro.com