《Spring 5 官方文档》15.使用JDBC实现数据访问

15.1 介绍Spring JDBC框架

表格13.1很清楚的列举了Spring框架针对JDBC操作做的一些抽象和封装。里面区分了哪些操作Spring已经帮你做好了、哪些操作是应用开发者需要自己负责的.

表13.1. Spring JDBC – 框架和应用开发者各自分工

操作 Spring 开发者
定义连接参数 X
打开连接 X
指定SQL语句 X
声明参数和提供参数值 X
准备和执行语句 X
返回结果的迭代(如果有) X
具体操作每个迭代 X
异常处理 X
事务处理 X
关闭连接、语句和结果集 X

一句话、Spring帮你屏蔽了很多JDBC底层繁琐的API操作、让你更方便的开发

15.1.1 选择一种JDBC数据库访问方法

JDBC数据库访问有几种基本的途径可供选择。除了JdbcTemplate的三种使用方式外,新的SimpleJdbcInsert和SimplejdbcCall调用类通过优化数据库元数据(来简化JDBC操作),还有一种更偏向于面向对象的RDBMS对象风格的方法、有点类似于JDO的查询设计。即使你已经选择了其中一种方法、你仍然可以混合使用另外一种方法的某一个特性。所有的方法都需要JDBC2.0兼容驱动的支持,一些更高级的特性则需要使用JDBC3.0驱动支持。

  • JdbcTemplate 是经典的Spring JDBC访问方式,也是最常用的。这是“最基础”的方式、其他所有方式都是在 JdbcTemplate的基础之上封装的。
  • NamedParameterJdbcTemplate 在原有JdbcTemplate的基础上做了一层包装支持命名参数特性、用于替代传统的JDBC“?”占位符。当SQL语句中包含多个参数时使用这种方式能有更好的可读性和易用性
  • SimpleJdbcInsert和SimpleJdbcCall操作类主要利用JDBC驱动所提供的数据库元数据的一些特性来简化数据库操作配置。这种方式简化了编码、你只需要提供表或者存储过程的名字、以及和列名相匹配的参数Map。但前提是数据库需要提供足够的元数据。如果数据库没有提供这些元数据,需要开发者显式配置参数的映射关系。
  • RDBMS对象的方式包含MappingSqlQuery, SqlUpdate和StoredProcedure,需要你在初始化应用数据访问层时创建可重用和线程安全的对象。这种方式设计上类似于JDO查询、你可以定义查询字符串,声明参数及编译查询语句。一旦完成这些工作之后,执行方法可以根据不同的传入参数被多次调用。
  • 15.1.2 包层级
    Spring的JDBC框架一共包含4种不同类型的包、包括core,datasource,object和support.

    org.springframework.jdbc.core包含JdbcTemplate 类和它各种回调接口、外加一些相关的类。它的一个子包
    org.springframework.jdbc.core.simple包含SimpleJdbcInsert和SimpleJdbcCall等类。另一个叫org.springframework.jdbc.core.namedparam的子包包含NamedParameterJdbcTemplate及它的一些工具类。详见:
    15.2:“使用JDBC核心类控制基础的JDBC处理过程和异常处理机制
    15.4:“JDBC批量操作
    15.5:“利用SimpleJdbc 类简化JDBC操作”.

    org.springframework.jdbc.datasource包包含DataSource数据源访问的工具类,以及一些简单的DataSource实现用于测试和脱离JavaEE容器运行的JDBC代码。子包org.springfamework.jdbc.datasource.embedded提供Java内置数据库例如HSQL, H2, 和Derby的支持。详见:
    15.3:“控制数据库连接
    15.8:“内置数据库支持”.

    org.springframework.jdbc.object包含用于在RDBMS查询、更新和存储过程中创建线程安全及可重用的对象类。详见15.6: “像Java对象那样操作JDBC”;这种方式类似于JDO的查询方式,不过查询返回的对象是与数据库脱离的。此包针对JDBC做了很多上层封装、而底层依赖于org.springframework.jdbc.core包。

    org.springframework.jdbc.support包含SQLException的转换类和一些工具类。JDBC处理过程中抛出的异常会被转换成org.springframework.dao里面定义的异常类。这意味着SpringJDBC抽象层的代码不需要实现JDBC或者RDBMS特定的错误处理方式。所有转换的异常都没有被捕获,而是让开发者自己处理异常、具体的话既可以捕获异常也可以直接抛给上层调用者
    详见:15.2.3:“SQL异常转换器”.

    15.2 使用JDBC核心类控制基础的JDBC处理过程和异常处理机制

    15.2.1 JdbcTemplate

    JdbcTemplate是JDBC core包里面的核心类。它封装了对资源的创建和释放,可以帮你避免忘记关闭连接等常见错误。它也包含了核心JDBC工作流的一些基础工作、例如执行和声明语句,而把SQL语句的生成以及查询结果的提取工作留给应用代码。JdbcTemplate执行查询、更新SQL语句和调用存储过程,运行结果集迭代和抽取返回参数值。它也可以捕获JDBC异常并把它们转换成更加通用、解释性更强的异常层次结构、这些异常都定义在org.springframework.dao包里面。

    当你在代码中使用了JdbcTemplate类,你只需要实现回调接口。PreparedStatementCreator回调接口通过传入的Connection类(该类包含SQL和任何必要的参数)创建已声明的语句。CallableStatementCreator也提供类似的方式、该接口用于创建回调语句。RowCallbackHandler用于获取结果集每一行的值。

    可以在DAO实现类中通过传入DataSource引用来完成JdbcTemplate的初始化;也可以在Spring IOC容器里面配置、作为DAO bean的依赖Bean配置。

    备注:DataSource最好在Spring IOC容器里作为Bean配置起来。在上面第一种情况下,DataSource bean直接传给相关的服务;第二种情况下DataSource bean传递给JdbcTemplate bean。

    JdbcTemplate中使用的所有SQL以“DEBUG”级别记入日志(一般情况下日志的归类是JdbcTemplate对应的全限定类名,不过如果需要对JdbcTemplate进行定制的话,可能是它的子类名)

    JdbcTemplate 使用示例

    这一节提供了JdbcTemplate类的一些使用例子。这些例子没有囊括JdbcTemplate可提供的所有功能;全部功能和用法请详见相关的javadocs.

    查询 (SELECT)

    下面是一个简单的例子、用于获取关系表里面的行数

    int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
    

    使用绑定变量的简单查询:

    int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
    		"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
    

    String查询:

    String lastName = this.jdbcTemplate.queryForObject(
    		"select last_name from t_actor where id = ?",
    		new Object[]{1212L}, String.class);
    

    查询和填充领域模型:

    Actor actor = this.jdbcTemplate.queryForObject(
    		"select first_name, last_name from t_actor where id = ?",
    		new Object[]{1212L},
    		new RowMapper<Actor>() {
    			public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
    				Actor actor = new Actor();
    				actor.setFirstName(rs.getString("first_name"));
    				actor.setLastName(rs.getString("last_name"));
    				return actor;
    			}
    		});
    

    查询和填充多个领域对象:

    List<Actor> actors = this.jdbcTemplate.query(
    		"select first_name, last_name from t_actor",
    		new RowMapper<Actor>() {
    			public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
    				Actor actor = new Actor();
    				actor.setFirstName(rs.getString("first_name"));
    				actor.setLastName(rs.getString("last_name"));
    				return actor;
    			}
    		});
    

    如果上面的两段代码实际存在于相同的应用中,建议把RowMapper匿名类中重复的代码抽取到单独的类中(通常是一个静态类),方便被DAO方法引用。例如,上面的代码例子更好的写法如下:

    public List<Actor> findAllActors() {
    	return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
    }
    
    private static final class ActorMapper implements RowMapper<Actor> {
    
    	public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
    		Actor actor = new Actor();
    		actor.setFirstName(rs.getString("first_name"));
    		actor.setLastName(rs.getString("last_name"));
    		return actor;
    	}
    }
    

    使用jdbcTemplate实现增删改

    你可以使用update(..)方法实现插入,更新和删除操作。参数值可以通过可变参数或者封装在对象内传入。

    this.jdbcTemplate.update(
    		"insert into t_actor (first_name, last_name) values (?, ?)",
    		"Leonor", "Watling");
    
    this.jdbcTemplate.update(
    		"update t_actor set last_name = ? where id = ?",
    		"Banjo", 5276L);
    
    this.jdbcTemplate.update(
    		"delete from actor where id = ?",
    		Long.valueOf(actorId));
    

    其他jdbcTemplate操作

    你可以使用execute(..)方法执行任何SQL,甚至是DDL语句。这个方法可以传入回调接口、绑定可变参数数组等。

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

    下面的例子调用一段简单的存储过程。更复杂的存储过程支持文档后面会有描述。

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

    JdbcTemplate 最佳实践
    JdbcTemplate实例一旦配置之后是线程安全的。这点很重要因为这样你就能够配置JdbcTemplate的单例,然后安全的将其注入到多个DAO中(或者repositories)。JdbcTemplate是有状态的,内部存在对DataSource的引用,但是这种状态不是会话状态。

    使用JdbcTemplate类的常用做法是在你的Spring配置文件里配置好一个DataSource,然后将其依赖注入进你的DAO类中(NamedParameterJdbcTemplate也是如此)。JdbcTemplate在DataSource的Setter方法中被创建。就像如下DAO类的写法一样:

    public class JdbcCorporateEventDao implements CorporateEventDao {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	// JDBC-backed implementations of the methods on the CorporateEventDao follow...
    }
    

    相关的配置是这样的:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="
    		http://www.springframework.org/schema/beans
    		http://www.springframework.org/schema/beans/spring-beans.xsd
    		http://www.springframework.org/schema/context
    		http://www.springframework.org/schema/context/spring-context.xsd">
    
    	<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
    		<property name="dataSource" ref="dataSource"/>
    	</bean>
    
    	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    		<property name="driverClassName" value="${jdbc.driverClassName}"/>
    		<property name="url" value="${jdbc.url}"/>
    		<property name="username" value="${jdbc.username}"/>
    		<property name="password" value="${jdbc.password}"/>
    	</bean>
    
    	<context:property-placeholder location="jdbc.properties"/>
    
    </beans>
    

    另一种替代显式配置的方式是使用component-scanning和注解注入。在这个场景下需要添加@Repository注解(添加这个注解可以被component-scanning扫描到),同时在DataSource的Setter方法上添加@Autowired注解:

    @Repository
    public class JdbcCorporateEventDao implements CorporateEventDao {
    
    	private JdbcTemplate jdbcTemplate;
    
    	@Autowired
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	// JDBC-backed implementations of the methods on the CorporateEventDao follow...
    }
    

    相关的XML配置如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="
    		http://www.springframework.org/schema/beans
    		http://www.springframework.org/schema/beans/spring-beans.xsd
    		http://www.springframework.org/schema/context
    		http://www.springframework.org/schema/context/spring-context.xsd">
    
    	<!-- Scans within the base package of the application for @Component classes to configure as beans -->
    	<context:component-scan base-package="org.springframework.docs.test" />
    
    	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    		<property name="driverClassName" value="${jdbc.driverClassName}"/>
    		<property name="url" value="${jdbc.url}"/>
    		<property name="username" value="${jdbc.username}"/>
    		<property name="password" value="${jdbc.password}"/>
    	</bean>
    
    	<context:property-placeholder location="jdbc.properties"/>
    
    </beans>
    

    如果你使用Spring的JdbcDaoSupport类,许多JDBC相关的DAO类都从该类继承过来,这个时候相关子类需要继承JdbcDaoSupport类的setDataSource方法。当然你也可以选择不从这个类继承,JdbcDaoSupport本身只是提供一些便利性。

    无论你选择上面提到的哪种初始方式,当你在执行SQL语句时一般都不需要重新创建JdbcTemplate 实例。JdbcTemplate一旦被配置后其实例都是线程安全的。当你的应用需要访问多个数据库时你可能也需要多个JdbcTemplate实例,相应的也需要多个DataSources,同时对应多个JdbcTemplates配置。

    15.2.2 NamedParameterJdbcTemplate
    NamedParameterJdbcTemplate 提供对JDBC语句命名参数的支持,而普通的JDBC语句只能使用经典的 ‘?’参数。NamedParameterJdbcTemplate内部包装了JdbcTemplate,很多功能是直接通过JdbcTemplate来实现的。本节主要描述NamedParameterJdbcTemplate不同于JdbcTemplate 的点;即通过使用命名参数来操作JDBC

    // some JDBC-backed DAO class...
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }
    
    public int countOfActorsByFirstName(String firstName) {
    
    	String sql = "select count(*) from T_ACTOR where first_name = :first_name";
    
    	SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
    
    	return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
    }
    

    上面代码块可以看到SQL变量中命名参数的标记用法,以及namedParameters变量的相关赋值(类型为MapSqlParameterSource)

    除此以外,你还可以在NamedParameterJdbcTemplate中传入Map风格的命名参数及相关的值。NamedParameterJdbcTemplate类从NamedParameterJdbcOperations接口实现的其他方法用法是类似的,这里就不一一叙述了。

    下面是一个Map风格的例子:

    // some JDBC-backed DAO class...
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }
    
    public int countOfActorsByFirstName(String firstName) {
    
    	String sql = "select count(*) from T_ACTOR where first_name = :first_name";
    
    	Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
    
    	return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
    }
    

    与NamedParameterJdbcTemplate相关联的SqlParameterSource接口提供了很有用的功能(两者在同一个包里面)。在上面的代码片段中你已经看到了这个接口的一个实现例子(就是MapSqlParameterSource类)。SqlParameterSource类是NamedParameterJdbcTemplate
    类的数值值来源。MapSqlParameterSource实现非常简单、只是适配了java.util.Map,其中Key就是参数名字,Value就是参数值。

    另外一个SqlParameterSource 的实现是BeanPropertySqlParameterSource类。这个类封装了任意一个JavaBean(也就是任意符合JavaBen规范的实例),在这个实现中,使用了JavaBean的属性作为命名参数的来源。

    public class Actor {
    
    	private Long id;
    	private String firstName;
    	private String lastName;
    
    	public String getFirstName() {
    		return this.firstName;
    	}
    
    	public String getLastName() {
    		return this.lastName;
    	}
    
    	public Long getId() {
    		return this.id;
    	}
    
    	// setters omitted...
    
    }
    
    // some JDBC-backed DAO class...
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    	this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }
    
    public int countOfActors(Actor exampleActor) {
    
    	// notice how the named parameters match the properties of the above 'Actor' class
    	String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";
    
    	SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
    
    	return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
    }
    

    之前提到过NamedParameterJdbcTemplate本身包装了经典的JdbcTemplate模板。如果你想调用只存在于JdbcTemplate类中的方法,你可以使用getJdbcOperations()方法、该方法返回JdbcOperations接口,通过这个接口你可以调用内部JdbcTemplate的方法。

    NamedParameterJdbcTemplate 类在应用上下文的使用方式也可见:“JdbcTemplate最佳实践

    15.2.3 SQLExceptionTranslator

    SQLExceptionTranslator接口用于在SQLExceptions和spring自己的org.springframework.dao.DataAccessException之间做转换,要处理批量更新或者从文件中这是为了屏蔽底层的数据访问策略。其实现可以是比较通用的(例如,使用JDBC的SQLState编码),或者是更精确专有的(例如,使用Oracle的错误类型编码)

    SQLExceptionTranslator 接口的默认实现是SQLErrorCodeSQLExceptionTranslator,该实现使用的是指定数据库厂商的错误编码,因为要比SQLState的实现更加精确。错误码转换过程基于JavaBean类型的SQLErrorCodes。这个类通过SQLErrorCodesFactory创建和返回,SQLErrorCodesFactory是一个基于sql-error-codes.xml配置内容来创建SQLErrorCodes的工厂类。该配置中的数据库厂商代码基于Database MetaData信息中返回的数据库产品名(DatabaseProductName),最终使用的就是你正在使用的实际数据库中错误码。

    SQLErrorCodeSQLExceptionTranslator按以下的顺序来匹配规则:

    备注:SQLErrorCodesFactory是用于定义错误码和自定义异常转换的缺省工厂类。错误码参照Classpath下配置的sql-error-codes.xml文件内容,相匹配的SQLErrorCodes实例基于正在使用的底层数据库的元数据名称

  • 是否存在自定义转换的子类。通常直接使用SQLErrorCodeSQLExceptionTranslator就可以了,因此此规则一般不会生效。只有你真正自己实现了一个子类才会生效。
  • 是否存在SQLExceptionTranslator接口的自定义实现,通过SQLErrorCodes类的customSqlExceptionTranslator属性指定
  • SQLErrorCodes的customTranslations属性数组、类型为CustomSQLErrorCodesTranslation类实例列表、能否被匹配到
  • 错误码被匹配到
  • 使用兜底的转换器。SQLExceptionSubclassTranslator是缺省的兜底转换器。如果此转换器也不存在的话只能使用SQLStateSQLExceptionTranslator
  • 你可以继承SQLErrorCodeSQLExceptionTranslator:

    public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
    
    	protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) {
    		if (sqlex.getErrorCode() == -12345) {
    			return new DeadlockLoserDataAccessException(task, sqlex);
    		}
    		return null;
    	}
    }
    

    这个例子中,特定的错误码-12345被识别后单独转换,而其他的错误码则通过默认的转换器实现来处理。在使用自定义转换器时,有必要通过setExceptionTranslator方法传入JdbcTemplate ,并且使用JdbcTemplate来做所有的数据访问处理。下面是一个如何使用自定义转换器的例子

    private JdbcTemplate jdbcTemplate;
    
    public void setDataSource(DataSource dataSource) {
    
    	// create a JdbcTemplate and set data source
    	this.jdbcTemplate = new JdbcTemplate();
    	this.jdbcTemplate.setDataSource(dataSource);
    
    	// create a custom translator and set the DataSource for the default translation lookup
    	CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
    	tr.setDataSource(dataSource);
    	this.jdbcTemplate.setExceptionTranslator(tr);
    
    }
    
    public void updateShippingCharge(long orderId, long pct) {
    	// use the prepared JdbcTemplate for this update
    	this.jdbcTemplate.update("update orders" +
    		" set shipping_charge = shipping_charge * ? / 100" +
    		" where id = ?", pct, orderId);
    }
    

    自定义转换器需要传入dataSource对象为了能够获取sql-error-codes.xml定义的错误码

    15.2.4 执行SQL语句

    执行一条SQL语句非常方便。你只需要依赖DataSource和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))");
    	}
    }
    

    15.2.5 查询
    一些查询方法会返回一个单一的结果。使用queryForObject(..)返回结果计数或特定值。当返回特定值类型时,将Java类型作为方法参数传入、最终返回的JDBC类型会被转换成相应的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(..),返回一个列表,列表每一项是一个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}]
    

    15.2.6 更新数据库

    下面的例子根据主键更新其中一列值。在这个例子中,一条SQL语句包含行参数的占位符。参数值可以通过可变参数或者对象数组传入。元数据类型需要显式或者自动装箱成对应的包装类型

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

    15.2.7 获取自增Key
    update()方法支持获取数据库自增Key。这个支持已成为JDBC3.0标准之一、更多细节详见13.6章。这个方法使用PreparedStatementCreator作为其第一个入参,该类可以指定所需的insert语句。另外一个参数是KeyHolder,包含了更新操作成功之后产生的自增Key。这不是标准的创建PreparedStatement 的方式。下面的例子可以在Oracle上面运行,但在其他平台上可能就不行了。

    final String INSERT_SQL = "insert into my_test (name) values(?)";
    final String name = "Rob";
    
    KeyHolder keyHolder = new GeneratedKeyHolder();
    jdbcTemplate.update(
    	new PreparedStatementCreator() {
    		public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
    			PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"});
    			ps.setString(1, name);
    			return ps;
    		}
    	},
    	keyHolder);
    
    // keyHolder.getKey() now contains the generated key
    

    15.3 控制数据库连接
    15.3.1 DataSource

    Spring用DataSource来保持与数据库的连接。DataSource是JDBC规范的一部分同时是一种通用的连接工厂。它使得框架或者容器对应用代码屏蔽连接池或者事务管理等底层逻辑。作为开发者,你无需知道连接数据库的底层逻辑;这只是创建datasource的管理员该负责的模块。在开发测试过程中你可能需要同时扮演双重角色,但最终上线时你不需要知道生产数据源是如何配置的。

    当使用Spring JDBC时,你可以通过JNDI获取数据库数据源、也可以利用第三方依赖包的连接池实现来配置。比较受欢迎的三方库有Apache Jakarta Commons DBCP 和 C3P0。在Spring产品内,有自己的数据源连接实现,但仅仅用于测试目的,同时并没有使用到连接池。

    这一节使用了Spring的DriverManagerDataSource实现、其他更多的实现会在后面提到。

    注意:仅仅使用DriverManagerDataSource类只是为了测试目的、因为此类没有连接池功能,因此在并发连接请求时性能会比较差

    通过DriverManagerDataSource获取数据库连接的方式和传统JDBC是类似的。首先指定JDBC驱动的类全名,DriverManager 会据此来加载驱动类。接下来、提供JDBC驱动对应的URL名称。(可以从相应驱动的文档里找到具体的名称)。然后传入用户名和密码来连接数据库。下面是一个具体配置DriverManagerDataSource连接的Java代码块:

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

    15.3.2 DataSourceUtils
    DataSourceUtils类是一个方便有用的工具类,提供了从JNDI获取和关闭连接等有用的静态方法。它支持线程绑定的连接、例如:使用DataSourceTransactionManager的时候,将把数据库连接绑定到当前的线程上。

    15.3.3 SmartDataSource
    实现SmartDataSource接口的实现类需要能够提供到关系数据库的连接。它继承了DataSource接口,允许使用它的类查询是否在某个特定的操作后需要关闭连接。这在当你需要重用连接时比较有用。

    15.3.4 AbstractDataSource
    AbstractDataSource是Spring DataSource实现的基础抽象类,封装了DataSource的基础通用功能。你可以继承AbstractDataSource自定义DataSource 实现。

    15.3.5 SingleConnectionDataSource
    SingleConnectionDataSource实现了SmartDataSource接口、内部封装了一个在每次使用后都不会关闭的单一连接。显然,这种场景下无法支持多线程。

    为了防止客户端代码误以为数据库连接来自连接池(就像使用持久化工具时一样)错误的调用close方法,你应将suppressClose设置为true。这样,通过该类获取的将是代理连接(禁止关闭)而不是原有的物理连接。需要注意你不能将这个类强制转换成Oracle等数据库的原生连接。

    这个类主要用于测试目的。例如,他使得测试代码能够脱离应用服务器,很方便的在单一的JNDI环境下调试。和DriverManagerDataSource相反,它总是重用相同的连接,这是为了避免在测试过程中创建过多的物理连接。

    15.3.6 DriverManagerDataSource
    DriverManagerDataSource类实现了标准的DataSource接口,可以通过Java Bean属性来配置原生的JDBC驱动,并且每次都返回一个新的连接。

    这个实现对于测试和JavaEE容器以外的独立环境比较有用,无论是作为一个在Spring IOC容器内的DataSource Bean,或是在单一的JNDI环境中。由于Connection.close()仅仅只是简单的关闭数据库连接,因此任何能够操作DataSource的持久层代码都能很好的工作。但是,使用JavaBean类型的连接池,比如commons-dbcp往往更简单、即使是在测试环境下也是如此,因此更推荐commons-dbcp。

    15.3.7 TransactionAwareDataSourceProxy

    TransactionAwareDataSourceProxy会创建一个目标DataSource的代理,内部包装了DataSource,在此基础上添加了Spring事务管理功能。有点类似于JavaEE服务器中提供的JNDI事务数据源。

    注意:一般情况下很少用到这个类,除非现有代码在被调用的时候需要一个标准的 JDBC DataSource接口实现作为参数。在这种场景下,使用proxy可以仍旧重用老代码,同时能够有Spring管理事务的能力。更多的场景下更推荐使用JdbcTemplate和DataSourceUtils等更高抽象的资源管理类.

    (更多细节请查看TransactionAwareDataSourceProxy的JavaDoc)
    15.3.8 DataSourceTransactionManager
    DataSourceTransactionManager类实现了PlatformTransactionManager接口。它将JDBC连接从指定的数据源绑定到当前执行的线程中,
    允许一个线程连接对应一个数据源。

    应用代码需要通过DataSourceUtils.getConnection(DataSource) 来获取JDBC连接,而不是通过JavaEE标准的DataSource.getConnection来获取。它会抛出org.springframework.dao的运行时异常而不是编译时SQL异常。所有框架类像JdbcTemplate都默认使用这个策略。如果不需要和这个 DataSourceTransactionManager类一起使用,DataSourceUtils 提供的功能跟一般的数据库连接策略没有什么两样,因此它可以在任何场景下使用。

    DataSourceTransactionManager支持自定义隔离级别,以及JDBC查询超时机制。为了支持后者,应用代码必须在每个创建的语句中使用JdbcTemplate或是调用DataSourceUtils.applyTransactionTimeout(..)方法

    在单一的资源使用场景下它可以替代JtaTransactionManager,不需要要求容器去支持JTA。如果你严格遵循连接查找的模式的话、可以通过配置来做彼此切换。JTA本身不支持自定义隔离级别!

    15.4 JDBC批量操作
    大多数JDBC驱动在针对同一SQL语句做批处理时能够获得更好的性能。批量更新操作可以节省数据库的来回传输次数。

    15.4.1 使用JdbcTemplate来进行基础的批量操作
    通过JdbcTemplate 实现批处理需要实现特定接口的两个方法,BatchPreparedStatementSetter,并且将其作为第二个参数传入到batchUpdate方法调用中。使用getBatchSize提供当前批量操作的大小。使用setValues方法设置语句的Value参数。这个方法会按getBatchSize设置中指定的调用次数。下面的例子中通过传入列表来批量更新actor表。在这个例子中整个列表使用了批量操作:

    public class JdbcActorDao implements ActorDao {
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public int[] batchUpdate(final List<Actor> actors) {
    		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
    }
    

    如果你需要处理批量更新或者从文件中批量读取,你可能需要确定一个合适的批处理大小,但是最后一次批处理可能达不到这个大小。在这种场景下你可以使用InterruptibleBatchPreparedStatementSetter接口,允许在输入流耗尽之后终止批处理,isBatchExhausted方法使得你可以指定批处理结束时间。

    15.4.2 对象列表的批量处理
    JdbcTemplate和NamedParameterJdbcTemplate都提供了批量更新的替代方案。这个时候不是实现一个特定的批量接口,而是在调用时传入所有的值列表。框架会循环访问这些值并且使用内部的SQL语句setter方法。你是否已声明参数对应API是不一样的。针对已声明参数你需要传入qlParameterSource数组,每项对应单次的批量操作。你可以使用SqlParameterSource.createBatch方法来创建这个数组,传入JavaBean数组或是包含参数值的Map数组。

    下面是一个使用已声明参数的批量更新例子:

    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
    }
    

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

    上面所有的批量更新方法都返回一个数组,包含具体成功的行数。这个计数是由JDBC驱动返回的。如果拿不到计数。JDBC驱动会返回-2。

    15.4.3 多个批处理操作
    上面最后一个例子更新的批处理数量太大,最好能再分割成更小的块。最简单的方式就是你多次调用batchUpdate来实现,但是可以有更优的方法。要使用这个方法除了SQL语句,还需要传入参数集合对象,每次Batch的更新数和一个ParameterizedPreparedStatementSetter去设置预编译SQL语句的参数值。框架会循环调用提供的值并且将更新操作切割成指定数量的小批次。

    下面的例子设置了更新批次数量为100的批量更新操作:

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    	}
    
    	public int[][] batchUpdate(final Collection<Actor> actors) {
    		int[][] updateCounts = jdbcTemplate.batchUpdate(
    				"update t_actor set first_name = ?, last_name = ? where id = ?",
    				actors,
    				100,
    				new ParameterizedPreparedStatementSetter<Actor>() {
    					public void setValues(PreparedStatement ps, Actor argument) throws SQLException {
    						ps.setString(1, argument.getFirstName());
    						ps.setString(2, argument.getLastName());
    						ps.setLong(3, argument.getId().longValue());
    					}
    				});
    		return updateCounts;
    	}
    
    	// ... additional methods
    
    }
    

    这个调用的批量更新方法返回一个包含int数组的二维数组,包含每次更新生效的行数。第一层数组长度代表批处理执行的数量,第二层数组长度代表每个批处理生效的更新数。每个批处理的更新数必须和所有批处理的大小匹配,除非是最后一次批处理可能小于这个数,具体依赖于更新对象的总数。每次更新语句生效的更新数由JDBC驱动提供。如果更新数量不存在,JDBC驱动会返回-2

    15.5 利用SimpleJdbc类简化JDBC操作

    SimpleJdbcInsert类和SimpleJdbcCall类主要利用了JDBC驱动所提供的数据库元数据的一些特性来简化数据库操作配置。这意味着可以在前端减少配置,当然你也可以覆盖或是关闭底层的元数据处理,在代码里面指定所有的细节。

    15.5.1 利用SimpleJdbcInsert插入数据
    让我们首先看SimpleJdbcInsert类可提供的最小配置选项。你需要在数据访问层初始化方法里面初始化SimpleJdbcInsert类。在这个例子中,初始化方法是setDataSource。你不需要继承SimpleJdbcInsert,只需要简单的创建其实例同时调用withTableName设置数据库名。

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
    	}
    
    	public void add(Actor actor) {
    		Map<String, Object> parameters = new HashMap<String, Object>(3);
    		parameters.put("id", actor.getId());
    		parameters.put("first_name", actor.getFirstName());
    		parameters.put("last_name", actor.getLastName());
    		insertActor.execute(parameters);
    	}
    
    	// ... additional methods
    }
    

    代码中的execute只传入java.utils.Map作为唯一参数。需要注意的是Map里面用到的Key必须和数据库中表对应的列名一一匹配。这是因为我们需要按顺序读取元数据来构造实际的插入语句。

    15.5.2 使用SimpleJdbcInsert获取自增Key
    接下来,我们对于同样的插入语句,我们并不传入id,而是通过数据库自动获取主键的方式来创建新的Actor对象并插入数据库。 当我们创建SimpleJdbcInsert实例时, 我们不仅需要指定表名,同时我们通过usingGeneratedKeyColumns方法指定需要数据库自动生成主键的列名。

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		Map<String, Object> parameters = new HashMap<String, Object>(2);
    		parameters.put("first_name", actor.getFirstName());
    		parameters.put("last_name", actor.getLastName());
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    }
    

    执行插入操作时第二种方式最大的区别是你不是在Map中指定ID,而是调用executeAndReturnKey方法。这个方法返回java.lang.Number对象,可以创建一个数值类型的实例用于我们的领域模型中。你不能仅仅依赖所有的数据库都返回一个指定的Java类;java.lang.Number是你可以依赖的基础类。如果你有多个自增列,或者自增的值是非数值型的,你可以使用executeAndReturnKeyHolder 方法返回的KeyHolder

    15.5.3 使用SimpleJdbcInsert指定列
    你可以在插入操作中使用usingColumns方法来指定特定的列名

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingColumns("first_name", "last_name")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		Map<String, Object> parameters = new HashMap<String, Object>(2);
    		parameters.put("first_name", actor.getFirstName());
    		parameters.put("last_name", actor.getLastName());
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    }
    

    这里插入操作的执行和你依赖元数据决定更新哪个列的方式是一样的。

    15.5.4 使用SqlParameterSource 提供参数值
    使用Map来指定参数值没有问题,但不是最便捷的方法。Spring提供了一些SqlParameterSource接口的实现类来更方便的做这些操作。
    第一个是BeanPropertySqlParameterSource,如果你有一个JavaBean兼容的类包含具体的值,使用这个类是很方便的。他会使用相关的Getter方法来获取参数值。下面是一个例子:

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    
    }
    

    另外一个选择是使用MapSqlParameterSource,类似于Map、但是提供了一个更便捷的addValue方法可以用来做链式操作。

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcInsert insertActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.insertActor = new SimpleJdbcInsert(dataSource)
    				.withTableName("t_actor")
    				.usingGeneratedKeyColumns("id");
    	}
    
    	public void add(Actor actor) {
    		SqlParameterSource parameters = new MapSqlParameterSource()
    				.addValue("first_name", actor.getFirstName())
    				.addValue("last_name", actor.getLastName());
    		Number newId = insertActor.executeAndReturnKey(parameters);
    		actor.setId(newId.longValue());
    	}
    
    	// ... additional methods
    
    }
    

    上面这些例子可以看出、配置是一样的,区别只是切换了不同的提供参数的实现方式来执行调用。
    15.5.5 利用SimpleJdbcCall调用存储过程

    SimpleJdbcCall利用数据库元数据的特性来查找传入的参数和返回值,这样你就不需要显式去定义他们。如果你喜欢的话也以自己定义参数,尤其对于某些参数,你无法直接将他们映射到Java类上,例如ARRAY类型和STRUCT类型的参数。下面第一个例子展示了一个存储过程,从一个MySQL数据库返回Varchar和Date类型。这个存储过程例子从指定的actor记录中查询返回first_name,last_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;
    

    in_id 参数包含你正在查找的actor记录的id.out参数返回从数据库表读取的数据

    SimpleJdbcCall 和SimpleJdbcInsert定义的方式比较类似。你需要在数据访问层的初始化代码中初始化和配置该类。相比StoredProcedure类,你不需要创建一个子类并且不需要定义能够在数据库元数据中查找到的参数。下面是一个使用上面存储过程的SimpleJdbcCall配置例子。除了DataSource以外唯一的配置选项是存储过程的名字

    public class JdbcActorDao implements ActorDao {
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcCall procReadActor;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		this.procReadActor = new SimpleJdbcCall(dataSource)
    				.withProcedureName("read_actor");
    	}
    
    	public Actor readActor(Long id) {
    		SqlParameterSource in = new MapSqlParameterSource()
    				.addValue("in_id", id);
    		Map out = procReadActor.execute(in);
    		Actor actor = new Actor();
    		actor.setId(id);
    		actor.setFirstName((String) out.get("out_first_name"));
    		actor.setLastName((String) out.get("out_last_name"));
    		actor.setBirthDate((Date) out.get("out_birth_date"));
    		return actor;
    	}
    
    	// ... additional methods
    
    }
    

    调用代码包括创建包含传入参数的SqlParameterSource。这里需要重视的是传入参数值名字需要和存储过程中定义的参数名称相匹配。有一种场景不需要匹配、那就是你使用元数据去确定数据库对象如何与存储过程相关联。在存储过程源代码中指定的并不一定是数据库中存储的格式。有些数据库会把名字转成大写、而另外一些会使用小写或者特定的格式。

    execute方法接受传入参数,同时返回一个Map包含任意的返回参数,Map的Key是存储过程中指定的名字。在这个例子中它们是out_first_name, out_last_name 和 out_birth_date

    execute 方法的最后一部分使用返回的数据创建Actor对象实例。再次需要强调的是Out参数的名字必须是存储过程中定义的。结果Map中存储的返回参数名必须和数据库中的返回参数名(不同的数据库可能会不一样)相匹配,为了提高你代码的可重用性,你需要在查找中区分大小写,或者使用Spring里面的LinkedCaseInsensitiveMap。如果使用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
    
    }
    

    通过这样的配置,你就可以无需担心返回参数值的大小写问题。

    15.5.6 为SimpleJdbcCall显式定义参数

    你已经了解如何通过元数据来简化参数配置,但如果你需要的话也可以显式指定参数。这样做的方法是在创建SimpleJdbcCall类同时通过declareParameters方法进行配置,这个方式可以传入一系列的SqlParameter。下面的章节会详细描述如何定义一个SqlParameter

    备注:如果你使用的数据库不是Spring支持的数据库类型的话显式定义就很有必要了。当前Spring支持以下数据库的存储过程元数据查找能力:Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle, 和 Sybase. 我们同时对某些数据库内置函数支持元数据特性:比如:MySQL、Microsoft SQL Server和Oracle。

    你可以选择显式定义一个、多个,或者所有参数。当你没有显式定义参数时元数据参数仍然会被使用。当你不想用元数据查找参数功能、只想指定参数时,需要调用withoutProcedureColumnMetaDataAccess方法。假设你针对同一个数据函数定义了两个或多个不同的调用方法签名,在每一个给定的签名中你需要使用useInParameterNames来指定传入参数的名称列表。下面是一个完全自定义的存储过程调用例子

    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
    }
    

    两个例子的执行结果是一样的,区别是这个例子显式指定了所有细节,而不是仅仅依赖于数据库元数据。
    15.5.7 如何定义SqlParameters

    如何定义SimpleJdbc类和RDBMS操作类的参数,详见15.6: “像Java对象那样操作JDBC”,
    你需要使用SqlParameter或者是它的子类。通常需要在构造器中定义参数名和SQL类型。SQL类型使用java.sql.Types常量来定义。
    我们已经看到过类似于如下的定义:

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

    上面第一行SqlParameter 定义了一个传入参数。IN参数可以同时在存储过程调用和SqlQuery查询中使用,它的子类在下面的章节也有覆盖。

    上面第二行SqlOutParameter定义了在一次存储过程调用中使用的返回参数。还有一个SqlInOutParameter类,可以用于输入输出参数。也就是说,它既是一个传入参数,也是一个返回值。

    备注:参数只有被定义成SqlParameter和SqlInOutParameter才可以提供输入值。不像StoredProcedure类为了考虑向后兼容允许定义为SqlOutParameter的参数可以提供输入值

    对于输入参数,除了名字和SQL类型,你可以定义数值区间或是自定义数据类型名。针对输出参数,你可以使用RowMapper处理从REF游标返回的行映射。另外一种选择是定义SqlReturnType,可以针对返回值作自定义处理。

    15.5.8 使用SimpleJdbcCall调用内置存储函数

    调用存储函数几乎和调用存储过程的方式是一样的,唯一的区别你提供的是函数名而不是存储过程名。你可以使用withFunctionName方法作为配置的一部分表示我们想要调用一个函数,以及生成函数调用相关的字符串。一个特殊的execute调用,executeFunction,用来指定这个函数并且返回一个指定类型的函数值,这意味着你不需要从结果Map获取返回值。存储过程也有一个名字为executeObject的便捷方法,但是只要一个输出参数。下面的例子基于一个名字为get_actor_name的存储函数,返回actor的全名。下面是这个函数的Mysql源代码:

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

    我们需要在初始方法中创建SimpleJdbcCall来调用这个函数

    public class JdbcActorDao implements ActorDao {
    
    	private JdbcTemplate jdbcTemplate;
    	private SimpleJdbcCall funcGetActorName;
    
    	public void setDataSource(DataSource dataSource) {
    		this.jdbcTemplate = new JdbcTemplate(dataSource);
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		jdbcTemplate.setResultsMapCaseInsensitive(true);
    		this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
    				.withFunctionName("get_actor_name");
    	}
    
    	public String getActorName(Long id) {
    		SqlParameterSource in = new MapSqlParameterSource()
    				.addValue("in_id", id);
    		String name = funcGetActorName.executeFunction(String.class, in);
    		return name;
    	}
    
    	// ... additional methods
    
    }
    

    execute方法返回一个包含函数调用返回值的字符串

    15.5.9 从SimpleJdbcCall返回ResultSet/REF游标

    调用存储过程或者函数返回结果集会相对棘手一点。一些数据库会在JDBC结果处理中返回结果集,而另外一些数据库则需要明确指定返回值的类型。两种方式都需要循环迭代结果集做额外处理。通过SimpleJdbcCall,你可以使用returningResultSet方法,并定义一个RowMapper的实现类来处理特定的返回值。 当结果集在返回结果处理过程中没有被定义名称时,返回的结果集必须与定义的RowMapper的实现类指定的顺序保持一致。 而指定的名字也会被用作返回结果集中的名称。

    下面的例子使用了一个不包含输入参数的存储过程并且返回t_actor标的所有行。下面是这个存储过程的Mysql源代码:

    CREATE PROCEDURE read_all_actors()
    BEGIN
     SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
    END;
    

    调用这个存储过程你需要定义RowMapper。因为我们定义的Map类遵循JavaBean规范,所以我们可以使用BeanPropertyRowMapper作为实现类。 通过将相应的class类作为参数传入到newInstance方法中,我们可以创建这个实现类。

    public class JdbcActorDao implements ActorDao {
    
    	private SimpleJdbcCall procReadAllActors;
    
    	public void setDataSource(DataSource dataSource) {
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		jdbcTemplate.setResultsMapCaseInsensitive(true);
    		this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
    				.withProcedureName("read_all_actors")
    				.returningResultSet("actors",
    				BeanPropertyRowMapper.newInstance(Actor.class));
    	}
    
    	public List getActorsList() {
    		Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
    		return (List) m.get("actors");
    	}
    
    	// ... additional methods
    
    }
    

    execute调用传入一个空Map,因为这里不需要传入任何参数。从结果Map中提取Actors列表,并且返回给调用者。

    15.6 像Java对象那样操作JDBC

    org.springframework.jdbc.object包能让你更加面向对象化的访问数据库。举个例子,用户可以执行查询并返回一个list, 该list作为一个结果集将把从数据库中取出的列数据映射到业务对象的属性上。你也可以执行存储过程,包括更新、删除、插入语句。

    备注:许多Spring的开发者认为下面将描述的各种RDBMS操作类(StoredProcedure类除外)可以直接被JdbcTemplate代替; 相对于把一个查询操作封装成一个类而言,直接调用JdbcTemplate方法将更简单而且更容易理解。但这仅仅是一种观点而已, 如果你认为可以从直接使用RDBMS操作类中获取一些额外的好处,你不妨根据自己的需要和喜好进行不同的选择。

    15.6.1 SqlQuery

    SqlQuery类主要封装了SQL查询,本身可重用并且是线程安全的。子类必须实现newRowMapper方法,这个方法提供了一个RowMapper实例,用于在查询执行返回时创建的结果集迭代过程中每一行映射并创建一个对象。SqlQuery类一般不会直接使用;因为MappingSqlQuery子类已经提供了一个更方便从列映射到Java类的实现。其他继承SqlQuery的子类有MappingSqlQueryWithParameters和UpdatableSqlQuery。

    15.6.2 MappingSqlQuery
    MappingSqlQuery是一个可重用的查询类,它的子类必须实现mapRow(..)方法,将结果集返回的每一行转换成指定的对象类型。下面的例子展示了一个自定义的查询例子,将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 = ?");
    		super.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,因此它可能包含任何在执行过程中传入参数的占位符。你必须在SqlParameter中使用declareParameter方法定义每个参数。SqlParameter使用java.sql.Types定义名字和JDBC类型。在你定义了所有的参数后,你需要调用compile方法,语句被预编译后方便后续的执行。这个类在编译后是线程安全的,一旦在DAO初始化时这些实例被创建后,它们可以作为实例变量一直被重用。

    private ActorMappingQuery actorMappingQuery;
    
    @Autowired
    public void setDataSource(DataSource dataSource) {
    	this.actorMappingQuery = new ActorMappingQuery(dataSource);
    }
    
    public Customer getCustomer(Long id) {
    	return actorMappingQuery.findObject(id);
    }
    

    这个例子中的方法通过唯一的传入参数id获取customer实例。因为我们只需要返回一个对象,所以就简单的调用findObject类就可以了,这个方法只需要传入id参数。如果我们需要一次查询返回一个列表的话,就需要使用传入可变参数数组的执行方法。

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

    15.6.3 SqlUpdate
    SqlUpdate封装了SQL的更新操作。和查询一样,更新对象是可以被重用的,就像所有的rdbms操作类,更新操作能够传入参数并且在SQL定义。类似于SqlQuery诸多execute(..)方法,这个类提供了一系列update(..)方法。SQLUpdate类不是抽象类,它可以被继承,比如,实现自定义的更新方法。但是你并不需要继承SqlUpdate类来达到这个目的,你可以更简单的在SQL中设置自定义参数来实现。

    import java.sql.Types;
    
    import javax.sql.DataSource;
    
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.SqlUpdate;
    
    public class UpdateCreditRating extends SqlUpdate {
    
    	public UpdateCreditRating(DataSource ds) {
    		setDataSource(ds);
    		setSql("update customer set credit_rating = ? where id = ?");
    		declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
    		declareParameter(new SqlParameter("id", Types.NUMERIC));
    		compile();
    	}
    
    	/**
    	 * @param id for the Customer to be updated
    	 * @param rating the new value for credit rating
    	 * @return number of rows updated
    	 */
    	public int execute(int id, int rating) {
    		return update(rating, id);
    	}
    }
    

    15.6.4 StoredProcedure
    StoredProcedure类是所有RDBMS存储过程的抽象类。该类提供了多种execute(..)方法,其访问类型都是protected的。

    为了定义一个存储过程类,你需要使用SqlParameter或者它的一个子类。你必须像下面的代码例子那样在构造函数中指定参数名和SQL类型。SQL类型使用java.sql.Types 常量定义。

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

    SqlParameter的第一行定义了一个输入参数。输入参数可以同时被存储过程调用和使用SqlQuery的查询语句使用,他的子类会在下面的章节提到。

    第二行SqlOutParameter 参数定义了一个在存储过程调用中使用的输出参数。SqlInOutParameter 还有一个InOut参数,该参数提供了一个输入值,同时也有返回值。

    对应输入参数,除了名字和SQL类型,你还能指定返回区间数值类型和自定义数据库类型。对于输出参数你可以使用RowMapper来处理REF游标返回的行映射关系。另一个选择是指定SqlReturnType,能够让你定义自定义的返回值类型。

    下面的程序演示了如何调用Oracle中的sysdate()函数。为了使用存储过程函数你需要创建一个StoredProcedure的子类。在这个例子中,StoredProcedure是一个内部类,但是如果你需要重用StoredProcedure你需要定义成一个顶级类。这个例子没有输入参数,但是使用SqlOutParameter类定义了一个时间类型的输出参数。execute()方法执行了存储过程,并且从结果集Map中获取返回的时间数据。结果集Map中包含每个输出参数对应的项,在这个例子中就只有一项,使用了参数名作为key.

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

    下面是一个包含两个输出参数的存储过程例子。

    import oracle.jdbc.OracleTypes;
    import org.springframework.jdbc.core.SqlOutParameter;
    import org.springframework.jdbc.object.StoredProcedure;
    
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    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(..)的SqlOutParameter参数, 该参数使用RowMapper接口的实现。这是一种非常方便有效的重用方式。两种RowMapper实现的代码如下:

    TitleMapper类将返回结果集的每一行映射成Title类

    import org.springframework.jdbc.core.RowMapper;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    import com.foo.domain.Title;
    
    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类将返回结果集的每一行映射成Genre类

    import org.springframework.jdbc.core.RowMapper;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    import com.foo.domain.Genre;
    
    public final class GenreMapper implements RowMapper<Genre> {
    
    	public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
    		return new Genre(rs.getString("name"));
    	}
    }
    

    为了将参数传递给RDBMS中定义的一个或多个输入参数给存储过程,你可以定义一个强类型的execute(..)方法,该方法将调用基类的protected execute(Map parameters)方法。例如:

    import oracle.jdbc.OracleTypes;
    import org.springframework.jdbc.core.SqlOutParameter;
    import org.springframework.jdbc.core.SqlParameter;
    import org.springframework.jdbc.object.StoredProcedure;
    
    import javax.sql.DataSource;
    
    import java.sql.Types;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    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);
    	}
    }
    

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

    Spring JDBC框架提供了多个方法来处理常见的参数和数据问题

    15.7.1 为参数设置SQL的类型信息

    通常Sping通过传入的参数类型决定SQL的参数类型。可以在设定参数值的时候显式提供SQL类型。有些场景下设置NULL值是有必要的。

    你可以通过以下方式来设置SQL类型信息:

  • 许多JdbcTemplate的更新和查询方法需要传入额外的int数组类型的参数。这个数组使用java.sql.Types类的常量值来确定相关参数的SQL类型。每个参数会有对应的类型项。
  • 你可以使用SqlParameterValue类来包装需要额外信息的参数值。针对每个值创建一个新的实例,并且在构造函数中传入SQL类型和参数值。你还可以传入数值类型的可选区间参数
  • 对于那些使用命名参数的情况,使用SqlParameterSource类型的类比如BeanPropertySqlParameterSource ,或MapSqlParameterSource。他们都具备了为命名参数注册SQL类型的功能。
  • 15.7.2 处理BLOB和CLOB对象

    你可以存储图片,其他类型的二进制数据,和数据库里面的大块文本。这些大的对象叫做BLOBS(全称:Binary Large OBject;用于二进制数据)和CLOBS(全称:Character Large OBject;用于字符数据)。在Spring中你可以使用JdbcTemplate直接处理这些大对象,并且也可以使用RDBMS对象提供的上层抽象类,或者使用SimpleJdbc类。所有这些方法使用LobHandler接口的实现类来处理LOB数据的管理(全称:Large Object)。LobHandler通过getLobCreator方法提供了对LobCreator 类的访问,用于创建新的LOB插入对象。

    LobCreator/LobHandler提供了LOB输入和输出的支持:

    BLOB:
      byte[] — getBlobAsBytes和setBlobAsBytes
      InputStream - getBlobAsBinaryStream和setBlobAsBinaryStream
    
    CLOB:
      String - getClobAsString和setClobAsString
      InputStream - getClobAsAsciiStream和setClobAsAsciiStream
      Reader - getClobAsCharacterStream和setClobAsCharacterStream
    

    下面的例子展示了如何创建和插入一个BLOB。后面的例子我们将举例如何从数据库中将BLOB数据读取出来

    这个例子使用了JdbcTemplate和AbstractLobCreatingPreparedStatementCallback的实现类。它主要实现了一个方法,setValues.这个方法提供了用于在你的SQL插入语句中设置LOB列的LobCreator。

    针对这个例子我们假定有一个变量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、这里例子中传入的lobHandler 使用了默认实现类DefaultLobHandler
    2、使用setClobAsCharacterStream传入CLOB的内容
    3、使用setBlobAsBinaryStream传入BLOB的内容

    备注:如果你调用从DefaultLobHandler.getLobCreator()返回的LobCreator的setBlobAsBinaryStream, setClobAsAsciiStream, 或者setClobAsCharacterStream方法,其中contentLength参数允许传入一个负值。如果指定的内容长度是负值,DefaultLobHandler会使用JDBC4.0不带长度参数的set-stream方法,或者直接传入驱动指定的长度;JDBC驱动对未指定长度的LOB流的支持请参见相关文档

    下面是从数据库读取LOB数据的例子。我们这里再次使用JdbcTempate并使用相同的DefaultLobHandler实例。

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

    1、使用getClobAsString获取CLOB的内容
    2、使用getBlobAsBytes获取BLOC的内容

    15.7.3 传入IN语句的列表值

    SQL标准允许基于一个带参数列表的表达式进行查询,一个典型的例子是select * from T_ACTOR where id in (1, 2, 3). 这样的可变参数列表没有被JDBC标准直接支持;你不能定义可变数量的占位符(placeholder),只能定义固定变量的占位符,或者你在动态生成SQL字符串的时候需要提前知晓所需占位符的数量。NamedParameterJdbcTemplate 和 JdbcTemplate 都使用了后面那种方式。当你传入参数时,你需要传入一个java.util.List类型,支持基本类型。而这个list将会在SQL执行时替换占位符并传入参数。

    备注:在传入多个值的时候需要注意。JDBC标准不保证你能在一个in表达式列表中传入超过100个值,不少数据库会超过这个值,但是一般都会有个上限。比如Oracle的上限是1000.

    除了值列表的元数据值,你可以创建java.util.List的对象数组。这个列表支持多个在in语句内定义的表达式例如
    select * from T_ACTOR where (id, last_name) in ((1, ‘Johnson’), (2, ‘Harrop’\))。当然有个前提你的数据库需要支持这个语法。

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

    当你调用存储过程时有时需要使用数据库特定的复杂类型。为了兼容这些类型,当存储过程调用返回时Spring提供了一个SqlReturnType 来处理这些类型,SqlTypeValue用于存储过程的传入参数。

    下面是一个用户自定义类型ITEM_TYPE的Oracle STRUCT对象的返回值例子。SqlReturnType有一个方法getTypeValue必须被实现。而这个接口的实现将被用作SqlOutParameter声明的一部分。

    final TestItem = new TestItem(123L, "A test item",
    		new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
    
    declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
    	new SqlReturnType() {
    		public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String typeName) throws SQLException {
    			STRUCT struct = (STRUCT) cs.getObject(colIndx);
    			Object[] attr = struct.getAttributes();
    			TestItem item = new TestItem();
    			item.setId(((Number) attr[0]).longValue());
    			item.setDescription((String) attr[1]);
    			item.setExpirationDate((java.util.Date) attr[2]);
    			return item;
    		}
    	}));
    

    你可以使用SqlTypeValue 类往存储过程传入像TestItem那样的Java对象。你必须实现SqlTypeValue接口的createTypeValue方法。你可以使用传入的连接来创建像StructDescriptors这样的数据库指定对象。下面是相关的例子。

    SqlTypeValue value = new AbstractSqlTypeValue() {
    	protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
    		StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
    		Struct item = new STRUCT(itemDescriptor, conn,
    		new Object[] {
    			testItem.getId(),
    			testItem.getDescription(),
    			new java.sql.Date(testItem.getExpirationDate().getTime())
    		});
    		return item;
    	}
    };
    

    SqlTypeValue会加入到包含输入参数的Map中,用于执行存储过程调用。

    SqlTypeValue 的另外一个用法是给Oracle的存储过程传入一个数组。Oracle内部有它自己的ARRAY类,在这些例子中一定会被使用,你可以使用SqlTypeValue来创建Oracle ARRAY的实例,并且设置到Java ARRAY类的值中。

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

    15.8 内嵌数据库支持
    org.springframework.jdbc.datasource.embedded包包含对内嵌Java数据库引擎的支持。如对HSQL, H2, and Derby原生支持,你还可以使用扩展API来嵌入新的数据库内嵌类型和Datasource实现。

    15.8.1 为什么使用一个内嵌数据库?

    内嵌数据库因为比较轻量级所以在开发阶段比较方便有用。包括配置比较容易,启动快,方便测试,并且在开发阶段方便快速设计SQL操作

    15.8.2 使用Spring配置来创建内嵌数据库

    如果你想要将内嵌的数据库实例作为Bean配置到Spring的ApplicationContext中,使用spring-jdbc命名空间下的embedded-database tag

    <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数据库,并且在classpath下面配置schema.sql和test-data.sql资源。同时,作为一种最佳实践,内嵌数据库会被制定一个唯一生成的名字。内嵌数据库在Spring容器中作为javax.sql.DataSource Bean类型存在,并且能够被注入到所需的数据库访问对象中。

    15.8.3 使用编程方式创建内嵌数据库

    EmbeddedDatabaseBuilder提供了创建内嵌数据库的流式API。当你在独立的环境中或者是在独立的集成测试中可以使用这种方法创建一个内嵌数据库,下面是一个例子:

    EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
        .generateUniqueName(true)
        .setType(H2)
        .setScriptEncoding("UTF-8")
        .ignoreFailedDrops(true)
        .addScript("schema.sql")
        .addScripts("user_data.sql", "country_data.sql")
        .build();
    
    // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
    
    db.shutdown()
    

    更多支持的细节请参见:EmbeddedDatabaseBuilder 的JavaDoc

    EmbeddedDatabaseBuilder 也可以使用Java Config类来创建内嵌数据库,下面是一个例子:

    @Configuration
    public class DataSourceConfig {
    
        @Bean
        public DataSource dataSource() {
            return new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("user_data.sql", "country_data.sql")
                .build();
        }
    }
    

    15.8.4 选择内嵌数据库的类型
    使用HSQL
    spring支持HSQL 1.8.0及以上版本。HSQL是缺省默认内嵌数据库类型。如果显式指定HSQL,设置embedded-database Tag的type属性值为HSQL。如果使用builderAPI.调用EmbeddedDatabaseType.HSQL的setType(EmbeddedDatabaseType)方法。

    使用H2
    Spring也支持H2数据库。设置embedded-database tag的type类型值为H2来启用H2。如果你使用了builder API。调用setType(EmbeddedDatabaseType) 方法设置值为EmbeddedDatabaseType.H2。

    使用Derby
    Spring也支持 Apache Derby 10.5及以上版本,设置embedded-database tag的type属性值为BERBY来开启DERBY。如果你使用builder API,调用setType(EmbeddedDatabaseType)方法设置值为EmbeddedDatabaseType.DERBY.

    15.8.5 使用内嵌数据库测试数据访问层逻辑

    内嵌数据库提供了数据访问层代码的轻量级测试方案,下面是使用了内嵌数据库的数据访问层集成测试模板。使用这样的模板当内嵌数据库不需要在测试类中被重用时是有用的。不过,当你希望创建可以在test集中共享的内嵌数据库。考虑使用 Spring TestContext测试框架,同时在Spring ApplicationContext中将内嵌数据库配置成一个Bean,具体参见15.8.2节, “使用Spring配置来创建内嵌数据库” 和15.8.3节, “使用编程方式创建内嵌数据库”.

    public class DataAccessIntegrationTestTemplate {
    
    	private EmbeddedDatabase db;
    
    	@Before
    	public void setUp() {
    		// creates an HSQL in-memory database populated from default scripts
    		// classpath:schema.sql and classpath:data.sql
    		db = new EmbeddedDatabaseBuilder()
    				.generateUniqueName(true)
    				.addDefaultScripts()
    				.build();
    	}
    
    	@Test
    	public void testDataAccess() {
    		JdbcTemplate template = new JdbcTemplate(db);
    		template.query( /* ... */ );
    	}
    
    	@After
    	public void tearDown() {
    		db.shutdown();
    	}
    
    }
    

    15.8.6 生成内嵌数据库的唯一名字
    开发团队在使用内嵌数据库时经常碰到的一个错误是:当他们的测试集想对同一个数据库创建额外的实例。这种错误在以下场景经常发生,XML配置文件或者@Configuration类用于创建内嵌数据库,并且相关的配置在同样的测试集的多个测试场景下都被用到(例如,在同一个JVM进程中)。例如,针对内嵌数据库的不同集成测试的ApplicationContext配置的区别只在当前哪个Bean定义是有效的。

    这些错误的根源是Spring的EmbeddedDatabaseFactory工厂( XML命名空间和Java Config对象的EmbeddedDatabaseBuilder内部都用到了这个)会将内嵌数据库的名字默认设置成”testdb”.针对的场景,内嵌数据库通常设置成和Bean Id相同的名字。(例如,常用像“dataSource”的名字)。结果,接下来创建内嵌数据库的尝试都没创建一个新的数据库。相反,同样的JDBC链接URL被重用。创建内嵌数据的库的尝试往往从同一个配置返回了已存在的内嵌数据库实例。

    为了解决这个问题Spring框架4.2 提供了生成内嵌数据库唯一名的支持。例如:

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

    15.8.7 内嵌数据库扩展支持

    Spring JDBC 内嵌数据库支持以下两种扩展支持:

  • 实现EmbeddedDatabaseConfigurer支持新的内嵌数据库类型。
  • 实现DataSourceFactory支持新的DataSource实现,例如管理内嵌数据库连接的连接池
  • 欢迎贡献内部扩展给Spring社区,相关网址见:jira.spring.io.

    15.9 初始化Datasource
    org.springframework.jdbc.datasource.init用于支持初始化一个现有的DataSource。内嵌数据库提供了创建和初始化Datasource的一个选项,但是有时你需要在另外的服务器上初始化实例。

    15.9.1 使用Spring XML来初始化数据库

    如果你想要初始化一个数据库你可以设置DataSource Bean的引用,使用spring-jdbc命名空间下的initialize-database标记

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

    上面的例子执行了数据库的两个脚本:第一个脚本创建了一个Schema,第二个往表里插入了一个测试数据集。脚本路径可以使用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语句会失败掉。这个失败的SQL DROP语句会被忽略,但是其他错误会抛出一个异常。这在你的SQL语句不支持DROP …​ IF EXISTS(或类似语法)时比较有用,但是你想要在重新创建时无条件的移除所有的测试数据。在这个案例中第一个脚本通常是一系列DROP语句的集合,然后是一系列Create语句

    ignore-failures选项可以被设置成NONE (默认值), DROPS (忽略失败的删除), or ALL (忽略所有失败).

    每个语句都应该以;隔开,或者;字符不存在于所用的脚本的话使用新的一行。你可以全局控制或者单针对一个脚本控制,例如:

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

    在这个例子中,两个test-data脚本使用@@作为语句分隔符,并且只有db-schema.sql使用;.配置指定默认分隔符是@@,并且将db-schema脚本内容覆盖默认值。

    如果你需要从XML命名空间获取更多控制,你可以直接简单的使用DataSourceInitializer,并且在你的应用中将其定义为一个模块

    初始化依赖数据库的其他模块

    大多数应用只需要使用数据库初始器基本不会遇到其他问题了:这些基本在Spring上下文初始化之前不会使用到数据库。如果你的应用不属于这种情况则需要使用阅读余下的内容。

    数据库初始器依赖于Datasource实例,并且在初始化callback方法中执行脚本(类似于XML bean定义中的init-method,组件中的@Postconstruct方法,或是在实现了InitializingBean类的afterPropertiesSet()方法)。如果有其他bean也依赖了同样的数据源同时也在初始化回调模块中使用数据源,那么可能会存在问题因为数据还没有初始化。

    一个例子是在应用启动时缓存需要从数据库Load并且初始化数据。

    为了解决这个问题你有两个选择:改变你的缓存初始化策略将其移到更后面的阶段,或者确保数据库会首先初始化。

    如果你可以控制应用的初始化行为那么第一个选项明显更容易。下面是使用这个方式的一些建议,包括:

  • 缓存做懒加载,并且只在第一次访问的时候初始化,这样会提高你应用启动时间。
  • 让你的缓存或者其他独立模块通过实现Lifecycle或者SmartLifecycle接口来初始化缓存。当应用上下文启动时如果autoStartup有设置,SmartLifecycle会被自动启动,而Lifecycle 可以在调用ConfigurableApplicationContext.start()时手动启动。
  • 使用Spring的ApplicationEvent或者其他类似的自定义监听事件来触发缓存初始化。ContextRefreshedEvent总是在Spring容器加载完毕的时候使用(在所有Bean被初始化之后)。这类事件钩子(hook)机制在很多时候非常有用(这也是SmartLifecycle的默认工作机制)
  • 第二个选项也可以很容易实现。建议如下:

  • 依赖Spring Beanfatory的默认行为,Bean会按照注册顺序被初始化。你可以通过在XML配置中设置顺序来指定你应用模块的初始化顺序,确保数据库和数据库初始化会首先被执行。
  • 将Datasource和业务模块隔离,通过将它们放在各自独立的ApplicationContext上下文实例来控制其启动顺序。(例如:父上下文包含DataSource,子上下文包含业务模块)。这种结构在Spring Web应用中很常用,同时也是一种通用做法
  • 原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 《Spring 5 官方文档》15.使用JDBC实现数据访问

    何一昕

    Work@Taobao.com

    专注高并发和大数据领域
    FavoriteLoading添加本文到我的收藏
    • Trackback 关闭
    • 评论 (1)
    1. 文章的url不要用中文,下次发布的时候注意一下。

    您必须 登陆 后才能发表评论

    return top