1 Spring 的数据访问哲学

为了避免持久化的逻辑分散到应用的各个组件中,最好将数据访问的功能放到一个或多个专注于此项任务的组件中。这样的组件通常称为数据访问对象(data access object,DAO)或 Repository。

1.1 Spring 的数据访问异常体系

一方面,JDBC 的异常体系过于简单了, SQLException 表示在尝试访问数据库的时出现了问题,但是这个异常却没有告诉你哪里出错了以及如何进行处理。另一方面,Hibernate 的异常体系是其本身所独有的。我们需要的数据访问异常要具有描述性而且又与特定的持久化框架无关。

Spring 所提供的平台无关的持久化异常

Spring JDBC 提供的数据访问异常体系解决了以上的两个问题。不同于 JDBC,Spring 提供了多个数据访问异常,分别描述了它们抛出时所对应的问题。表 10.1 对比了 Spring 的部分数据访问异常以及 JDBC 所提供的异常。

JDBC 的异常Spring 的数据访问异常
BatchUpdateException
DataTruncation
SQLException
SQLWarning
BadSqlGrammarException
CannotAcquireLockException
CannotSerializeTransactionException
CannotGetJdbcConnectionException
CleanupFailureDataAccessException
ConcurrencyFailureException
DataAccessException
DataAccessResourceFailureException
DataIntegrityViolationException
DataRetrievalFailureException
DataSourceLookupApiUsageException
DeadlockLoserDataAccessException
DuplicateKeyException
EmptyResultDataAccessException
IncorrectResultSizeDataAccessException
IncorrectUpdateSemanticsDataAccessException
InvalidDataAccessApiUsageException
InvalidDataAccessResourceUsageException
InvalidResultSetAccessException
JdbcUpdateAffectedIncorrectNumberOfRowsException
LbRetrievalFailureException
BatchUpdateException
DataTruncation
SQLException SQLWarning
NonTransientDataAccessResourceException
OptimisticLockingFailureException
PermissionDeniedDataAccessException
PessimisticLockingFailureException
QueryTimeoutException
RecoverableDataAccessException
SQLWarningException
SqlXmlFeatureNotImplementedException
TransientDataAccessException
TransientDataAccessResourceException
TypeMismatchDataAccessException
UncategorizedDataAccessException
UncategorizedSQLException

尽管 Spring 的异常体系比 JDBC 简单的 SQLException 丰富得多,但它并没有与特定的持久化方式相关联。这意味着我们可以使用 Spring 抛出一致的异常,而不用关心所选择的持久化方案。这有助于我们将所选择持久化机制与数据访问层隔离开来。

看!不用写 catch 代码块

表 10.1 中没有体现出来的一点就是这些异常都继承自 DataAccessException。DataAccessException 的特殊之处在于它是一个非检查型异常。换句话说,没有必要捕获 Spring 所抛出的数据访问异常(当然,如果你想捕获的话也是完全可以的)。

DataAccessException 只是 Sping 处理检查型异常和非检查型异常哲学的一个范例。Spring 认为触发异常的很多问题是不能在 catch 代码块中修复的。Spring 使用了非检查型异常,而不是强制开发人员编写 catch 代码块(里面经常是空的)。这把是否要捕获异常的权力留给了开发人员。

为了利用 Spring 的数据访问异常,我们必须使用 Spring 所支持的数据访问模板。

1.2 数据访问模块化

Spring 将数据访问过程中固定的和可变的部分明确划分为两个不同的类:模板(template)和回调(callback)。模板管理过程中固定的部分,而回调处理自定义的数据访问代码。图 10.2 展现了这两个类的职责。

10.2 模板与回调.jpg

针对不同的持久化平台,Spring 提供了多个可选的模板。

模板类 (org. springframework.*)用途
jca. cci. core. CciTemplateJCA CCI 连接
jdbc.core.JdbcTemplateJDBC 连接
jdbc. core. namedparam. NamedParameterJdbcTemplate支持命名参数的 JDBC 连接
jdbc.core.simple.SimpleJdbcTemplate通过 Java 5 简化后的 JDBC 连接(Spring 3.1 中已经废弃)
orm.hibernate3.HibernateTemplateHibernet 3.x 以上的 Session
orm.ibatis.SqlMapClientTemplateiBATIS SqlMap 客户端
orm.jdo.JdoTemplateJava 数据对象(Java Data Object)实现
orm.jpa.JpaTemplateJava 持久化 API 的实体管理器

2 配置数据源

2.1 使用 JNDI 数据源

利用 Spring,我们可以像使用 Spring bean 那样配置 JNDI 中数据源的引用并将其装配到需要的类中。位于 jee 命名空间下的 <jee:jndi-lookup> 元素可以用于检索 JNDI 中的任何对象(包括数据源)并将其作为 Spring 的 bean。例如,如果应用程序的数据源配置在 JNDI 中,我们可以使用 <jee:jndi-lookup> 元素将其装配到 Spring 中,如下所示:

<jee:jndi-lookup id="dataSource" jndi-name="/jdbc/SpitterDS" resource-ref="true" />

其中 jndi-name 属性用于指定 JNDI 中资源的名称。如果只设置了 jndi-name 属性,那么就会根据指定的名称查找数据源。但是,如果应用程序运行在 Java 应用服务器中,你需要将 resource-ref 属性设置为 true,这样给定的 jndi-name 将会自动添加 “java: comp/env/” 前缀。

如果想使用 Java 配置的话,那我们可以借助 JndiObjectFactoryBean 从 JNDI 中查找 DataSource:

@Bean
public JndiObjectFactoryBean dataSource() {
  JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
  jndiObjectFB.setJndiName("jdbc/SpittrDS");
  jndiObjectFB.setResourceRef(true);
  jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
  return jndiObjectFB;
}

2.2 使用数据源连接池

如果你不能从 JNDI 中查找数据源,那么下一个选择就是直接在 Spring 中配置数据源连接池。尽管 Spring 并没有提供数据源连接池实现,但是我们有多项可用的方案,包括如下开源的实现:

配置如下,@Bean 配置亦可。

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
      p:driverClassName="org.h2.Driver"
      p:url="jdbc:h2:tcp://localhost/~/spitter"
      p:username="sa"
      p:password=""
      p:initialSize="5"
      p:maxActive="10" />

前四个属性是配置 BasicDataSource 所必需的。属性 driverClassName 指定了 JDBC 驱动类的全限定类名。在这里我们配置的是 H2 数据库的数据源。属性 url 用于设置数据库的 JDBC URL。最后,username 和 password 用于在连接数据库时进行认证。

以上四个基本属性定义了 BasicDataSource 的连接信息。除此以外,还有多个配置数据源连接池的属性。表 10.3 列出了 DBCP BasicDataSource 最有用的一些池配置属性:

池配置属性所指定的内容
initialSize池启动时创建的连接数量
maxActive同一时间可从池中分配的最多连接数。如果设置为 0,表示无限制
maxIdle池里不会被释放的最多空闲连接数。如果设置为 0,表示无限制
maxOpenPreparedStatements在同一时间能够从语句池中分配的预处理语句 (prepared statement)的最大数量。如果设置为 0,表示无限制
maxWait在抛出异常之前,池等待连接回收的最大时间(当没有可用连接时)。如果设置为 -1,表示无限等待
minEvictableIdleTimeMillis连接在池中保持空闲而不被回收的最大时间
minIdle在不创建新连接的情况下,池中保持空闲的最小连接数
poolPreparedStatements是否对预处理语句(prepared statement)进行池管理 (布尔值)

2.3 基于 JDBC 驱动的数据源

Spring 提供了三个这样的数据源类(均位于 org. springframework. jdbc. datasource 包中)供选择:

  • DriverManagerDataSource:在每个连接请求时都会返回一个新建的连接。与 DBCP 的 BasicDataSource 不同, 由 DriverManagerDataSource 提供的连接并没有进行池化管理;
  • SimpleDriverDataSource:与 DriverManagerDataSource 的工作方式类似,但是它直接使用 JDBC 驱动,来解决在特定环境下的类加载问题,这样的环境包括 OSGi 容器;
  • SingleConnectionDataSource:在每个连接请求时都会返回同一个的连接。

但是上述都不支持数据库连接池,所以不采用。

2.4 使用嵌入式的数据源

每次重启应用或运行测试的时候,都能够重新填充测试数据。Spring 的 jdbc 命名空间能够简化嵌入式数据库的配置。例如,如下的程序清单展现了如何使用 jdbc 命名空间来配置嵌入式的 H2 数据库,它会预先加载一组测试数据。

<?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:c="http://www.springframework.org/schema/c"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xsi:schemaLocation="http://www.springframework.org/schema/jdbc
	    http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd">
  ...
  <jdbc:embedded-database id="dataSource" type="H2">
    <jdbc:script location="classpath:spittr/db/jdbc/schema.sql" />
    <jdbc:script location="classpath:spittr/db/jdbc/test-data.sql" />
  </jdbc:embedded-database>
  ...
</beans>

<jdbc:embedded-database> 的 type 属性设置为 H2,表明嵌入式数据库应该是 H2 数据库(要确保 H2 位于应用的类路径下)。还可以将 type 设置为 DERBY,以使用嵌入式的 Apache Derby 数据库。

<jdbc:embedded-database> 中,我们可以不配置也可以配置多个 <jdbc:script> 元素来搭建数据库。

除了搭建嵌入式数据库以外,<jdbc:embedded-database> 元素还会暴露一个数据源,我们可以像使用其他的数据源那样来使用它。在这里,id 属性被设置成了 dataSource,这也是所暴露数据源的 bean ID。因此,当我们需要 javax.sql.DataSource 的时候,就可以注入 dataSource bean。

如果使用 Java 来配置嵌入式数据库时,不会像 jdbc 命名空间那么简便,我们可以使用 EmbeddedDatabaseBuilder 来构建 DataSource:

@Bean
public DataSource dataSource() {
  return new EmbeddedDatabaseBuilder()
    .setType(EmbeddedDatabaseType.H2)
    .setScript("classpath:schema.sql")
    .setScript("classpath:text-data.sql")
    .build();
}

2.5 使用 profile 选择数据源

@Configuration
public class DataSourceConfiguration {
  @Profile("development")
  @Bean
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder().build();
  }
 
  @Profile("qa")
  @Bean
  public DataSource Data() {
    BasicDataSource ds = new BasicDataSource();
    return ds;
  }
 
  @Profile("production")
  @Bean
  public DataSource dataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    return (Datasource) jndiObjectFactoryBean.getObject();
  }
}
<?xral versions"1.0" encodings"UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:p="http://www.springframework.org/schetna/p"
       xsi:schemaLocation="http://www.springframework.org/schema/jdbc
           http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
           http://www.sprinframework.org/schema/jee
           http://www.springframework.org/schema/jee/spring-jee-3.1.xsd
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
  <beans profile="development">
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="com/habuma/spitter/db/jdbc/schema.sql" />
      <jdbc:script location="com/habuma/spitter/db/jdbc/test-data.sql" />
    </jdbc:embedded-database>
  </beans>
 
  <beans profile="qa">
    <bean id="dataSource" classs="org.apache.commons.dbcp.BasicDataSource"
          p:driverClassName="org.h2.Driver"
          p:url="jdbc:h2:tcp://localhost/~/spitter"
          p:username="sa"
          p:password=""
          p:initialSize="5"
          p:maxActive="10" />
    </beans>
 
    <beans profile="production">
      <jee:jndi-lookup id="dataSource" jndi-name="/jdbc/SpitterDS" resource-ref="true" />
    </beans>
</beans>

3 Spring JDBC 模板

Spring 的 JDBC 框架承担了资源管理和异常处理的工作,从而简化了 JDBC 代码,让我们只需编写从数据库读写数据的必需代码。

Spring 将数据访问的样板代码抽象到模板类之中。Spring 为 JDBC 提供了三个模板类供选择:

  • JdbcTemplate:最基本的 Spring JDBC 模板,这个模板支持简单的 JDBC 数据库访问功能以及基于索引参数的查询;
  • NamedParameterJdbcTemplate:使用该模板类执行查询时可以将值以命名参数的形式绑定到 SQL 中,而不是使用简单的索引参数;
  • SimpleJdbcTemplate:该模板类利用 Java 5 的一些特性如自动装箱、泛型以及可变参数列表来简化 JDBC 模板的使用。

从 Spring 3.1 开始,SimpleJdbcTemplate 已经被废弃了,其 Java 5 的特性被转移到了 JdbcTemplate 中,并且只有在你需要使用命名参数的时候,才需要使用 NamedParameterJdbcTemplate。

3.1 使用 JdbcTemplate 来插入数据

为了让 JdbcTemplate 正常工作,只需要为其设置 DataSource 就可以了,这使得在 Spring 中配置 JdbcTemplate 非常容易,如下面的 @Bean 方法所示:

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

在这里,DataSource 是通过构造器参数注入进来的。这里所引用的 dataSource bean 可以是 javax. sql. DataSource 的任意实现.

@Repository
public class JdbcSpitterRepository implements SpitterRepository {
	private JdbcOperations jdbcOperations;
	@Inject
	public JdbcSpitterRepository(JdbcOperations jdbcOperations) {
		this.jdbcOperations= jdbcOperations;
	}
}

JdbcOperations 是一个接口,定义了 JdbcTemplate 所实现的操作。通过注入 JdbcOperations,而不是具体的 JdbcTemplate,能够保证 JdbcSpitterRepository 通过 JdbcOperations 接口达到与 JdbcTemplate 保持松耦合。

作为另外一种组件扫描和自动装配的方案,我们可以将 JdbcSpitterRepository 显式声明为 Spring 中的 bean,如下所示:

@Bean
public SpitterRepository spitterRepository(JdbcTemplate jdbcTemplate) {
  return new jdbcSpitterRepository(jdbcTemplate);
}

在 Repository 中具备可用的 JdbcTemplate 后,基于 JdbcTemplate 的 addSpitter () 方法如下:

private void insertSpitter(Spitter spitter) {
  jdbcTemplate.update(INSERT_SPITTER,
    spitter.getUsername(),
    spitter.getPassword(),
    spitter.getFullName(),
    spitter.getEmail(),
    spitter.isUpdateByEmail());
}

这里没有了创建连接和语句的代码,也没有异常处理的代码,只剩下单纯的数据插入代码。

3.2 使用 JdbcTemplate 来读取数据

public Spitter findOne(long id) {
  return jdbcTemplate.queryForObject(
    SELECT_SPITTER + " where id=?", new SpitterRowMapper(), id);
}
 
private static final class SpitterRowMapper implements RowMapper<Spitter> {
  public Spitter mapRow(ResultSet rs, int rowNum) throws SQLException {
	return new Spitter(
	  rs.getLong("id"),
	  rs.getString("username"),
	  rs.getString("password"),
	  rs.getString("fullname"),
	  rs.getString("email"),
	  rs.getBoolean("updateByEmail"));
  }
}

在这个 findOne () 方法中使用了 JdbcTemplate 的 queryForObject () 方法来从数据库查询 Spitter。queryForObject () 方法有三个参数:

  • String 对象,包含了要从数据库中查找数据的 SQL;
  • RowMapper 对象,用来从 ResultSet 中提取数据并构建域对象(本例中为 Spitter);
  • 可变参数列表,列出了要绑定到查询上的索引参数值。

SpitterRowMapper 对象中,它实现了 RowMapper 接口。对于查询返回的每一行数据,JdbcTemplate 将会调用 RowMapper 的 mapRow () 方法,并传入一个 ResultSet 和包含行号的整数。

3.3 在 JdbcTemplate 中使用 Java 8 的 Lambda 表达式

因为 RowMapper 接口只声明了 addRow () 这一个方法,因此它完全符合函数式接口(functional interface)的标准。

public Spitter findOne(long id) {
	return jdbcOperations.queryForObject(
	  SELECT_SPITTER_BY_ID,
	  (rs, rowNum) -> {
	    return new Spitter(
	      rs.getLong("id"),
	      rs.getString("username"),
    	  rs.getString("password"),
	      rs.getString("fullname"),
	      rs.getString("email"),
	      rs.getBoolean("updateByEmail"));
	},
	id);
}

还可以使用 Java 8 的方法引用,在单独的方法中定义映射逻辑:

public Spitter findOne(long id) {
  return jdbcOperations.queryForObejct(
    SELECT_SPITTER_BY_ID, this::mapSpitter, id);
}
 
private Spitter mapSpitter(ResultSet rs, int row) throws Exception {
  return new Spitter(
    rs.getLong("id"),
	rs.getString("username"),
    rs.getString("password"),
	rs.getString("fullname"),
	rs.getString("email"),
	rs.getBoolean("updateByEmail"));
}

3.4 使用命名参数

在清单 10.7 的代码中,addSpitter () 方法使用了索引参数。这意味着我们需要留意查询中参数的顺序,在将值传递给 update () 方法的时候要保持正确的顺序。如果在修改 SQL 时更改了参数的顺序,那我们还需要修改参数值的顺序。

除了这种方法之外,我们还可以使用命名参数。命名参数可以赋予 SQL 中的每个参数一个明确的名字,在绑定值到查询语句的时候就通过该名字来引用参数。例如,假设 SQL_INSERT_SPITTER 查询语句是这样定义的:

private static final Spitter SQL_INSERT_SPITTER =
  "insert into spitter (username, password, fullname) " +
  "values (:username, :password, :fullname)";

NamedParameterJdbcTemplate 是一个特殊的 JDBC 模板类,它支持使用命名参数。在 Spring 中,NamedParameterJdbcTemplate 的声明方式与常规的 JdbcTemplate 几乎完全相同:

@Bean
public NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource) {
  return new NamedParameterJdbcTemplate(dataSource);
}

在这里,我们将 NamedParameterJdbcOperations(NamedParameter-JdbcTemplate 所实现的接口)注入到 Repository 中,用它来替代 JdbcOperations。现在的 addSpitter () 方法如下所示:

private static final String INSERT_SPITTER =
  "insert into Spitter " +
  "  (username, password, fullname, email, updateByEmail) " +
  "values " +
  "  (:username, :password, :fullname, :email, :updateByEmail)";
 
private void addSpitter(Spitter spitter) {
  Map<String, Object> paramMap = new HashMap<String, Object>();
  paramMap.put("username", spitter.getUsername());
  paramMap.put("password", spitter.getPassword());
  paramMap.put("fullname", spitter.getFullName());
  paramMap.put("email", spitter.getEmail());
  paramMap.put("updateByEmail", spitter.isUpdateByEmail());
  jdbcOperations.update(INSERT_SPITTER, paramMap);
}