Spring 4 官方文档学习(十)数据访问之JDBC

说明:未修订版,阅读起来极度困难

1、Spring框架JDBC的介绍

Spring JDBC - who does what?

动作 Spring
定义连接参数  
打开连接  
指定SQL语句  
声明参数,提供参数值  
准备、执行语句  
迭代结果(如果有)  
操作每个迭代  
处理任何异常  
处理事务  
关闭连接、语句、结果集  

一句话,Spring框架负责所有的低级别细节操作。

1.1、选择JDBC数据库访问的路径

所有的路径都需要兼容JDBC 2.0的驱动,部分特性可能需要JDBC 3.0 的驱动。

JdbcTemplate 是经典的Spring JDBC 路径,最流行。最低级的路径,所有其他的底层都是用一个JdbcTemplate。

NamedParameterJdbcTemplate 封装了一个JdbcTemplate,使用带名字的参数取代了传统的JDBC 占位符(?)。该路径,提供了更好的文档,当你有很多参数时,更易于使用。

SimpleJdbcInsert和SimpleJdbcCall 优化了数据库元数据,限制了必要数据的数量。该路径简化了编码,你只要提供表名或procedure并提供参数和列匹配的映射即可。这只有在数据库提供了足够的元数据时才起作用。否则需要显式的提供足够的数据。

RDBM Objects 包括 MappingSqlQuery、SqlUpdate和StoredProcedure,要求在数据访问层初始化时创建可复用的、线程安全的对象。

1.2、package层级

Spring框架的JDBC抽象框架由四个不同的包组成:core、datasource、object以及support。

org.springframework.jdbc.core 包含JdbcTemplate类和其各种回调接口,以及一组相关的类。子包jdbc.core.simple包含了SimpleJdbcInsert 和 SimpleJdbcCall类。子包jdbc.core.namedparam 包含了NamedParameterJdbcTemplate类和相关支持类。

jdbc.datasource包含了简化DataSource使用的工具类,以及各种简单的DataSource实现(可以用于Java EE 容器之外的测试和运行)。子包jdbc.datasource.embedded支持使用Java数据库引擎创建内嵌的数据库,如HSQL、H2、Derby。

jdbc.object包含了代表RDBMS查询、更新,以及stored-procedure(存储过程)。

This approach is modeled by JDO, although objects returned by queries are naturally disconnected from the database. This higher level of JDBC abstraction depends on the lower-level abstraction in the org.springframework.jdbc.core package.

 

The org.springframework.jdbc.support package provides SQLException translation functionality and some utility classes. Exceptions thrown during JDBC processing are translated to exceptions defined in the org.springframework.dao package. This means that code using the Spring JDBC abstraction layer does not need to implement JDBC or RDBMS-specific error handling. All translated exceptions are unchecked, which gives you the option of catching the exceptions from which you can recover while allowing other exceptions to be propagated to the caller.

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

2.1、JdbcTemplate

处理资源的创建和释放。执行core JDBC工作流的基本任务,例如语句的创建和执行,让应用代码只需要提供SQL和抽取结果即可。

当你使用JdbcTemplate时,你只需要实现回调接口们。PreparedStatementCreator回调接口会根据给定的Connection和SQL以及任何必要的参数来创建预处理语句。CallableStatementCreator接口也是一样,只不过是创建callable语句。RowCallbackHandler接口会从ResultSet的每一行中提取值。

JdbcTemplate能在DAO实现内使用,使用DataSource实例化。

在Spring IoC容器中,DataSource总是需要被配置的!

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 = ?",
        new Object[]{1212L}, String.class);
// 将结果映射到domain object
Actor actor = this.jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        new Object[]{1212L},
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });
// 将查询结果映射到一组domain objects。和上面相比,实际上只有等号左边变化了。
List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

或者,上面的可以这样写:

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

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

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

调用一个简单的存储过程:

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

JdbcTemplate 最佳实践

JdbcTemplate 实例 是线程安全的!就是说,你只需要定义一个实例即可,随便注入并使用!

通常的实践是配置一个DataSource,然后将其注入到DAO类中。JdbcTemplate通过setDataSource(..)创建:

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
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 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类,可以让你的DAO类继承它,然后自动继承了setDataSource(..)方法。

只有在访问多个数据库时才需要创建多个JdbcTemplate实例!

2.2、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); // this

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
// 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); // 换成map了,一样

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

还可以,从domain object中获取参数 -- 不是写入domain object!!!如下:

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

2.3、SQLExceptionTranslator

一个接口,其实现类能够在SQLExceptions和Spring自己的org.springframework.dao.DataAccessException之间转换。其实现可以是泛型的也可以是具体的。

SQLErrorCodeSQLExceptionTranslator 是默认使用的实现。

2.4、执行语句

执行一个SQL语句只需要少量的代码。

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

2.5、运行查询

一些查询方法返回单个值。使用queryForObject(..)从一行中获取一个计数或特定的值。后者将返回的JDBC类型转成指定的Java类型。如果类型转换无效,会抛出InvalidDataAccessApiUsageException异常。

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() { // 返回int
        return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
    }

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

除了单个结果的查询方法,有几个方法返回一个列表。最泛型的方法是queryForList(..),会返回List<Map<col_name_type, col_value_type>>。如下:

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

2.6、更新数据库

下面的例子示意了更新某个特定主键对应的列。参数值可以通过变参数或数组传入。primitive类型会自动装箱。

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

2.7、获取自动生成的键 auto-generated keys -- 就是mybatis的<selectKey>

JDBC 3.0标准支持使用update()方法获取数据库生成的主键。该方法使用一个PreparedStatementCreator作为其第一个参数。另一个参数是一个KeyHolder,其包含了update成功时生成的key。

下面的例子在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

3、控制数据库连接

3.1、DataSource

Spring通过一个DataSource来获取到数据库的连接。DataSource是JDBC标准的一部分,是一个连接工厂。它允许容器或框架隐藏连接池和事务管理。

当使用Spring的JDBC层,你是从JNDI获取一个data source,或者,你使用第三方连接池实现来配置自己的data source。流行的连接池实现包括DBCP和C3P0. Spring中的实现仅用于测试目的,不提供pooling功能。

本部分使用Spring的DriverMangerDataSource实现和几个其他实现。

DriverMangerDataSource 仅用于测试目的!

DriverManagerDataSource dataSource = new DriverManagerDataSource(); // 实际工作中不要使用这个!!!
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
<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的基本的连接和配置:

<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"/>
<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.2、DataSourceUtils

帮助类,提供了静态方法从JNDI获取连接,以及关闭连接(如有必要)。支持线程绑定的连接,例如DataSourceTransactionManager。

3.3、SmartDataSource

扩展了DataSource接口,允许类使用它查询连接是否应该在某操作之后被关闭。如果你明确会复用一个连接,它会很有效率的。

3.4、AbstractDataSource

是Spring DataSource实现的抽象类。

3.5、SingleConnectionDataSource

是SmartDataSource的实现,封装了一个单独的Connection,每次使用都不会关闭,所以不具备多线程功能。

测试类。

3.6、DriverManagerDataSource

是标准DataSource的实现,通过bean properties配置简单的JDBC驱动,每次返回一个新的Connection!

在测试情况以及Java EE容器之外的环境下很有用。 -- 不要在生产环境中使用!

3.7、TransactionAwareDataSourceProxy

是对目标DataSource的代理,封装了目标DataSource to add awareness of Spring-managed transactions。从这个角度看,类似于一个Java EE服务器提供的事务性的JNDIDataSource。

-- 很少很少用到这个类!

3.8、DataSourceTransactionManager

是PlatformTransactionManager的单JDBC datasources的实现。它将一个特定data source的JDBC连接绑定到当前执行的线程,允许每个data source一个线程。

需要代码使用DataSourceUtils.getConnection(DataSource)代替DataSource.getConnection来获取JDBC连接。它会抛出unchecked org.springframework.dao 异常,而不是SQLExceptions。所有的框架类如JdbcTemplate都隐式的使用这种策略。

DataSourceTransactionManager 支持自定义隔离级别、超时时间(用于每个JDBC语句查询)。为了支持后者,代码中应该使用JdbcTemplate或者DataSourceUtils.applyTransactionTimeout(..)方法来为每个创建的语句设置超时时间。

该实现可以替代JtaTransactionManager -- 在单资源的情况下,因为此时不需要容器支持JTA。二者的切换仅仅是配置的问题。

3.9、NativeJdbcExtractor

有时候,你需要访问供应商独有的JDBC方法,而非标准JDBC API。这时你可以使用NativeJdbcExtractor配置你的JdbcTemplate或者OracleLobHandler。

NativeJdbcExtractor 有不同变体:

  • SimpleNativeJdbcExtractor
  • C3P0NativeJdbcExtractor
  • CommonsDbcpNativeJdbcExtractor
  • JBossNativeJdbcExtractor
  • WebLogicNativeJdbcExtractor
  • WebSphereNativeJdbcExtractor
  • XAPoolNativeJdbcExtractor

多数情况下,针对为封装的Connection对象,只需要使用SimpleNativeJdbcExtractor。

4、JDBC 批处理操作

当你的批处理需要调用相同的预处理语句时,多数JDBC驱动提供了性能改进。

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

JdbcTemplate批处理需要实现BatchPreparedStatementSetter接口的两个方法,并将其作为第二个参数传入你的batchUpdate(..)方法。使用getBatchSize()方法来提供当前batch的尺寸。使用setValues(..)方法来为预处理语句的参数设置。 该方法会被调用n次 -- 你在getBatchSize()方法中设置的次数。例子:

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) {
        int[] updateCounts = 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();
                    }
                });
        return updateCounts;
    }

    // ... additional methods
}

如果你在处理一个更新stream或者从文件中读取的stream,可能会预先设置一个batch size,但最后的batch可能不会满足batch size。这种情况下,可以使用InterruptibleBatchPreparedStatementSetter接口,其isBatchExhausted()方法允许你判断批处理的结束。

4.2、针对对象列表的批处理操作

JdbcTemplate和NamedParameterJdbcTemplate 都有另一种提供batch update的方法。不需要实现特定的batch接口,只需要以列表形式提供所有的参数值。框架会遍历这些值并使用一个内部的预处理语句SETTER。其API视你使用的命名参数不同而不同。For the named parameters you provide an array of SqlParameterSource, one entry for each member of the batch. You can use the SqlParameterSource.createBatch method to create this array, passing in either an array of JavaBeans or an array of Maps containing the parameter values.

例子(使用命名参数):

public class JdbcActorDao implements ActorDao {
    private NamedParameterTemplate namedParameterJdbcTemplate; //

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

    public int[] batchUpdate(final List<Actor> actors) {
        SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray());
        int[] updateCounts = namedParameterJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                batch);
        return updateCounts;
    }

    // ... additional methods
}

使用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);
        }
        int[] updateCounts = jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?", // ? 占位符
                batch);
        return updateCounts;
    }

    // ... additional methods

}

上面两个例子最后都会返回一个int数组,每个值都代表一条语句执行后影响的行数。如果不可用,会返回-2

4.3、多个批处理的批处理操作

当批处理非常大时,你可能想将其拆分成几个小的批处理。当然,你可以使用上面的方法,多次调用batchUpdate方法即可,但这里有一个更方便的方法。该方法除了使用SQL语句,还需要一个对象的集合 -- 包含了参数、每个batch中update的数量、以及一个ParameterizedPreparedStatementSetter 来为参数设值。框架会遍历提供的值,并将update调用拆分成指定大小的batch中。

例子:

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, // this, size of each batch
                new ParameterizedPreparedStatementSetter<Actor>() { // this
                    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数组的数组。不再解释。

5、使用SimpleJdbc类来简化JDBC操作

SimpleJdbcInsert和SimpleJdbcCall类利用了可以从JDBC驱动获取的数据库的元数据,从而提供了简化的配置。

5.1、使用SimpleJdbcInsert 插入数据

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor; // this

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

    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
}

5.2、使用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") // here
                .usingGeneratedKeyColumns("id"); // here
    }

    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); // here
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

如果返回多个自动生成的键,可以使用executeAndReturnKeyHolder,这会返回一个KeyHolder。

5.3、为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")
                .usingColumns("first_name", "last_name") // here
                .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

}

5.4、使用SqlParameterSource提供参数值

使用Map提供参数值很不错,但不是最方便的。Spring提供了一组SqlParameterSource接口的实现,很不错。第一个是BeanPropertySqlParameterSource,适用于JavaBean类。例子:

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); // here
        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() // this
                .addValue("first_name", actor.getFirstName()) // 链式编程
                .addValue("last_name", actor.getLastName()); // 链式编程
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods

}

5.5、使用SimpleJdbcCall调用存储过程

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-simple-jdbc-call-1

5.6、为SimpleJdbcCall显式声明参数

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-simple-jdbc-call-2

5.7、如何定义SqlParameter

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-params

为了SimpleJdbc类还有RDBMS的操作类定义参数,可以使用SqlParameter或其任一子类。通常需要在构造器中指定参数名和SQL类型。SQL类型使用java.sql.Types中的常量。

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

略。。。

5.8、使用SimpleJdbcCall调用stored function

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-simple-jdbc-call-3

5.9、从SimpleJdbcCall返回ResultSet/REF 游标

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-simple-jdbc-call-4

6、将JDBC操作模型化为Java对象

org.springframework.jdbc.object

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-object

6.1、SqlQuery

6.2、MappingSqlQuery

6.3、SqlUpdate

6.4、StoredProcedure

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

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-parameter-handling

7.1、为参数提供SQL type信息

一般,Spring是基于传入的参数类型决定参数的SQL type。但还可以显式的提供SQL type。有时候为了正确设置NULL值,这样做很有必要。

有以下几种方式:

  • JdbcTemplate的很多update和query方法会接收一个额外的参数:int数组形式。该数组用于表明相应参数的SQL类型,其值来自java.sql.Types类的常量。
  • 还可以使用SqlParameterValue类来封装需要额外信息的参数值。为每个值创建一个实例,将SQL type和参数值传入构造器。You can also provide an optional scale parameter for numeric values.
  • 当使用命名参数时,使用SqlParameterSource类的子类:BeanPropertySqlParameterSource 或 MapSqlParameterSource。它们都有为任意命名参数值注册SQL type的方法。

7.2、处理BLOB 和 CLOB对象

你可以在数据库中存储图像、其他二进制数据,还有大量的文本。这些大对象称为:BLOB(Binary Large Object) -- 针对二进制数据;CLOB (Character Large Object) -- 针对字符数据。在Spring中你可以使用JdbcTemplate处理这些大对象 (In Spring you can handle these large objects by using the JdbcTemplate directly and also when using the higher abstractions provided by RDBMS Objects and the SimpleJdbc classes.)。所有这些途径都是用了LobHandler接口的实现。LobHandler可以访问LobCreator类以创建新的LOB对象--使用getLobCreator()方法,从而插入数据库。

LobCreator/LobHandler 为LOB的输入输出提供下列支持:

  • BLOB
    • byte[] -- getBlobAsBytes, setBlobAsBytes
    • InputStream -- getBlobAsBinaryStream, setBlobAsBinaryStream
  • CLOB
    • String -- getClobAsString, setClobAsString
    • InputStream -- getClobAsAsciiStream, setClobAsAsciiStream
    • Reader -- getClobAsCharacterStream, setClobAsCharacterStream

下面的例子使用了JdbcTemplate以及AbstractLobCreatingPreparedStatementCallback的一个实现。它实现了setValues方法,该方法提供了一个LobCreator可以为LOB列设值。

下面的例子假定已经有一个变量lobHandler,且已经设置成DefaultLobHandler的实例。

final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
    "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
    new AbstractLobCreatingPreparedStatementCallback(lobHandler) { // 1
        protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
            ps.setLong(1, 1L);
            lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); // 2
            lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); // 3
        }
    }
);
blobIs.close();
clobReader.close();

  1. Pass in the lobHandler that in this example is a plain DefaultLobHandler.
  2. Using the method setClobAsCharacterStream, pass in the contents of the CLOB.
  3. Using the method setBlobAsBinaryStream, pass in the contents of the BLOB.

If you invoke the setBlobAsBinaryStream, setClobAsAsciiStream, or setClobAsCharacterStream method on the LobCreator returned from DefaultLobHandler.getLobCreator(), you can optionally specify a negative value for the contentLength argument. If the specified content length is negative, the DefaultLobHandler will use the JDBC 4.0 variants of the set-stream methods without a length parameter; otherwise, it will pass the specified length on to the driver.

上面的例子是写入LOB数据,下面看看读取LOB数据:

List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
    new RowMapper<Map<String, Object>>() {
        public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
            Map<String, Object> results = new HashMap<String, Object>();
            String clobText = lobHandler.getClobAsString(rs, "a_clob");  // 1
results.put("CLOB", clobText); byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); // 2
results.put("BLOB", blobBytes); return results; } });

1 Using the method getClobAsString, retrieve the contents of the CLOB.

2 Using the method getBlobAsBytes, retrieve the contents of the BLOB.

7.3、为IN语句传入值列表

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-in-clause

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

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-complex-types

8、内嵌数据库支持

原生支持HSQL, H2, Derby。可以扩展。

8.1、为什么使用内嵌数据库?

开发阶段很有用,因为其轻量级特性。易配置,启动快,便于测试,以及快速的改进SQL。

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.sql和test-data.sql中的SQL语句。

另外,作为最佳实践,内嵌数据库会被赋予一个独特的生成的名字。内嵌数据库在Spring容器中是一种javax.sql.DataSource类型的bean,可以注入任何dao中。

8.3、编码式创建一个内嵌数据库

类似这样:

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

还可以这样:

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

8.4、选择内嵌数据库类型

使用HSQL

Spring支持HSQL 1.8.0及以上版本。如果没有指定,默认就是HSQL。需要指定的话,将embedded-database标签的type属性设值为HSQL即可。

如果使用builder API,调用setType(EmbeddedDatabaseType.HSQL) 即可。

其他:H2,Derby。类似HSQL。

8.5、使用内嵌数据库测试数据访问逻辑

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

}

Embedded databases provide a lightweight way to test data access code. The following is a data access integration test template that uses an embedded database. Using a template like this can be useful for one-offs when the embedded database does not need to be reused across test classes. However, if you wish to create an embedded database that is shared within a test suite, consider using the Spring TestContext Framework and configuring the embedded database as a bean in the Spring ApplicationContext as described in Section 19.8.2, “Creating an embedded database using Spring XML” and Section 19.8.3, “Creating an embedded database programmatically”.

8.6、为内嵌数据库生成一个唯一的名字

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-embedded-database-unique-names

8.7、扩展内嵌数据库支持

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-embedded-database-extension

9、初始化一个DataSource

org.springframework.jdbc.datasource.init包提供了初始化已有DataSource的支持。内嵌数据库支持 提供了为一个应用创建和初始化一个DataSource的选项,但是,有时候你需要在服务器或者其他地方初始化一个实例。

9.1、使用Spring XML初始化一个数据库

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

该数据库初始化器的默认行为是无条件的执行提供的脚本。这通常不是你想要的,例如,如果你要针对一个已有数据的数据库进行初始化。偶然删除数据的可能可以通过以下常见模式(先创建表,再插入数据,如果已经存在表,那第一步就会失败)来降低。

然而,为了获取针对已有数据的创建和删除的更多控制,XML命名空间提供了一些额外的选项。第一个选项就是切换是否初始化。这可以根据环境来设置(例如从系统properties中获取一个boolean值),如下:

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

The second option to control what happens with existing data is to be more tolerant of failures. 你可以控制初始化器的能力,让其在执行scripts中的SQL时忽略特定错误。例如:

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

上面的例子是说,有时候我们期望scripts能够在一个空的数据库上执行 ,scripts中的一些DROP语句会执行失败。这些失败的SQL DROP语句会被忽略,但其他失败会导致异常。当你的SQL dialect不支持 DROP ... IF EXISTS ,但你又想在重建数据前无条件的清除所有测试数据时,这会很有用。

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>

上面是用 @@作为通用的分隔符,又给db-schema.sql设置了分号分隔符。

如果需要更多控制,使用DataSourceInitializer即可。

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

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-client-component-initialization

数据库初始化器依赖于一个DataSource实例,且会执行在其初始化回调(类似@PostConstruct)中提供的scripts。如果其他beans依赖相同的data source,并也在初始化回调中使用它,那么可能会导致问题,因为数据还没有初始化!常见的例子是在应用启动时就迫切的初始化并加载数据的缓存。

围绕这个问题,你有两个选择:改变你的缓存初始化策略,不要迫切初始化,而是延迟初始化,或者确保数据库初始化器已经初始化了。

第一个选择可能很简单 -- 如果你负责这个应用。一些建议:

  • 让缓存懒初始化--第一次使用时再初始化。
  • 让你的缓存或者初始化缓存的组件实现Lifecycle或SmartLifecycle。当应用上下文启动时,SmartLifecyle会被自动启动--如果它的autoStartup被设置了,可以在该上下文中手动调用ConfigurableApplicationContext.start()来启动Lifecycle
  • 使用ApplicationEvent或类似的观察者机制来触发缓存初始化。ContextRefreshedEvent通常是个很有用的钩子。

第二个选择可能同样简单。一些建议:

  • 依赖Spring BeanFactory的默认行为--beans以注册顺序被初始化。你可以很简单的安排顺序--一组<import/>,让数据库放在前面。
  • 分离DataSource和使用其的业务组件,通过将它们放在不同的ApplicationContext实例中来控制它们的启动顺序。--例如,父上下文包含DataSource,子上下文包含业务组件。这种结构在Spring web应用中很常见,但可用于更多地方。
原文地址:https://www.cnblogs.com/larryzeal/p/5997550.html