定义Spring对数据访问的支持
配置数据库资源
使用Spring的JDBC模版
Spring的数据访问哲学
Spring的目标之一就是允许我们在开发应用程序时,能够遵循面向对象(OO)原则中的“针对接口编程”。
为了避免持久化的逻辑分散到应用的各个组件中,最好将数据访问的功能放到一个或多个专注于此项任务的组件中。这样的组件通常称为数据访问对象(data access object,DAO)或Repository。
为了避免应用与特定的数据访问策略耦合在一起,编写良好的Repository应该以接口的方式暴露功能。图10.1展现了设计数据访问层的合理方式。
图10.1 服务对象本身并不会处理数据访问,而是将数据访问委托给Repository。Repository接口确保其与服务对象的松耦合
如果不强制捕获SQLException的话,几乎无法使用JDBC做任何事情。SQLException表示在尝试访问数据库的时出现了问题,但是这个异常却没有告诉你哪里出错了以及如何进行处理
可能导致抛出SQLException的常见问题包括:
- 应用程序无法连接数据库;
- 要执行的查询存在语法错误;
- 查询中所使用的表和/或列不存在;
- 试图插入或更新的数据违反了数据库约束。
一方面,JDBC的异常体系过于简单了——实际上,它算不上一个体系。另一方面,Hibernate的异常体系是其本身所独有的。我们需要的数据访问异常要具有描述性而且又与特定的持久化框架无关。
Spring所提供的平台无关的持久化异常
Spring JDBC提供的数据访问异常体系解决了以上的两个问题。不同于JDBC,Spring提供了多个数据访问异常,分别描述了它们抛出时所对应的问题。表10.1对比了Spring的部分数据访问异常以及JDBC所提供的异常。
尽管Spring的异常体系比JDBC简单的SQLException丰富得多,但它并没有与特定的持久化方式相关联。这意味着我们可以使用Spring抛出一致的异常,而不用关心所选择的持久化方案。这有助于我们将所选择持久化机制与数据访问层隔离开来。
DataAccessException的特殊之处在于它是一个非检查型异常。换句话说,没有必要捕获Spring所抛出的数据访问异常(当然,如果你想捕获的话也是完全可以的)。
DataAccessException只是Sping处理检查型异常和非检查型异常哲学的一个范例。Spring认为触发异常的很多问题是不能在catch代码块中修复的。Spring使用了非检查型异常,而不是强制开发人员编写catch代码块(里面经常是空的)。这把是否要捕获异常的权力留给了开发人员。
为了利用Spring的数据访问异常,我们必须使用Spring所支持的数据访问模板。让我们看一下Spring的模板是如何简化数据访问的。
数据访问模板化
尽管在这个过程中包含多个步骤,但是涉及到旅客的只有几个。承运人负责推动整个流程。你只会在必要的时候进行参与,其余的过程不必关心。这反映了一个强大的设计模式:模板方法模式。
模板方法定义过程的主要框架。
Spring将数据访问过程中固定的和可变的部分明确划分为两个不同的类:模板(template)和回调(callback)。模板管理过程中固定的部分,而回调处理自定义的数据访问代码。图10.2展现了这两个类的职责。
如图所示,Spring的模板类处理数据访问的固定部分——事务控制、管理资源以及处理异常。同时,应用程序相关的数据访问——语句、绑定参数以及整理结果集——在回调的实现中处理。事实证明,这是一个优雅的架构,因为你只需关心自己的数据访问逻辑即可。
针对不同的持久化平台,Spring提供了多个可选的模板。如果直接使用JDBC,那你可以选择JdbcTemplate。如果你希望使用对象关系映射框架,那HibernateTemplate或JpaTemplate可能会更适合你。表10.2列出了Spring所提供的所有数据访问模板及其用途。
配置数据源
Spring提供了在Spring上下文中配置数据源bean的多种方式,包括:
- 通过JDBC驱动程序定义的数据源;
- 通过JNDI查找的数据源;
- 连接池的数据源。
使用JNDI数据源
使用数据源连接池
尽管Spring并没有提供数据源连接池实现,但是我们有多项可用的方案,包括如下开源的实现:
- Apache Commons DBCP (http://jakarta.apache.org/commons/dbcp);
- c3p0 (http://sourceforge.net/projects/c3p0/) ;
- BoneCP (http://jolbox.com/) 。
基于JDBC驱动的数据源:
在Spring中,通过JDBC驱动定义数据源是最简单的配置方式。Spring提供了三个这样的数据源类(均位于org.springframework.jdbc.datasource包中)供选择:
DriverManagerDataSource:在每个连接请求时都会返回一个新建的连接。与DBCP的BasicDataSource不同,由DriverManagerDataSource提供的连接并没有进行池化管理;
SimpleDriverDataSource:与DriverManagerDataSource的工作方式类似,但是它直接使用JDBC驱动,来解决在特定环境下的类加载问题,这样的环境包括OSGi容器;
SingleConnectionDataSource:在每个连接请求时都会返回同一个的连接。尽管SingleConnectionDataSource不是严格意义上的连接池数据源,但是你可以将其视为只有一个连接的池。
以上这些数据源的配置与DBCPBasicDataSource的配置类似。例如,如下就是配置DriverManagerDataSource的方法:
与具备池功能的数据源相比,唯一的区别在于这些数据源bean都没有提供连接池功能,所以没有可配置的池相关的属性。
使用嵌入式的数据源:
除此之外,还有一个数据源是我想对读者介绍的:嵌入式数据库(embedded database)。嵌入式数据库作为应用的一部分运行,而不是应用连接的独立数据库服务器。尽管在生产环境的设置中,它并没有太大的用处,但是对于开发和测试来讲,嵌入式数据库都是很好的可选方案。这是因为每次重启应用或运行测试的时候,都能够重新填充测试数据.
Spring的jdbc命名空间能够简化嵌入式数据库的配置。例如,如下的程序清单展现了如何使用jdbc命名空间来配置嵌入式的H2数据库,它会预先加载一组测试数据。
如果使用Java来配置嵌入式数据库时,不会像jdbc命名空间那么简便,我们可以使用EmbeddedDatabaseBuilder来构建.
使用profile选择数据源
我们已经看到了多种在Spring中配置数据源的方法,我相信你已经找到了一两种适合你的应用程序的配置方式。实际上,我们很可能面临这样一种需求,那就是在某种环境下需要其中一种数据源,而在另外的环境中需要不同的数据源。
借助XML配置,基于profile选择数据源:
在Spring中使用JDBC
应对失控的JDBC代码
使用JDBC在数据库里插入一行数据
使用JDBC更新数据库中的一行
使用JDBC从数据库中查询一行数据
这段代码与插入和更新的样例一样冗长,甚至更为复杂。这就好像Pareto法则被倒了过来:只有20%的代码是真正用于查询数据的,而80%代码都是样板代码。
现在你可以看出,大量的JDBC代码都是用于创建连接和语句以及异常处理的样板代码。既然已经得出了这个观点,我们将不再接受它的折磨,以后你再也不会看到这样令人厌恶的代码了。但实际上,这些样板代码是非常重要的。清理资源和处理错误确保了数据访问的健壮性。如果没有它们的话,就不会发现错误而且资源也会处于打开的状态,这将会导致意外的代码和资源泄露。我们不仅需要这些代码,而且还要保证它是正确的。基于这样的原因,我们才需要框架来保证这些代码只写一次而且是正确的。
使用JDBC模板
Spring的JDBC框架承担了资源管理和异常处理的工作,从而简化了JDBC代码,让我们只需编写从数据库读写数据的必需代码。
正如前面小节所介绍过的,Spring将数据访问的样板代码抽象到模板类之中。Spring为JDBC提供了三个模板类供选择:
- JdbcTemplate:最基本的Spring JDBC模板,这个模板支持简单的JDBC数据库访问功能以及基于索引参数的查询;
- NamedParameterJdbcTemplate:使用该模板类执行查询时可以将值以命名参数的形式绑定到SQL中,而不是使用简单的索引参数;
- SimpleJdbcTemplate:该模板类利用Java 5的一些特性如自动装箱、泛型以及可变参数列表来简化JDBC模板的使用。
以前,在选择哪一个JDBC模板的时候,我们需要仔细权衡。但是从Spring 3.1开始,做这个决定变得容易多了。SimpleJdbcTemplate已经被废弃了,其Java 5的特性被转移到了JdbcTemplate中,并且只有在你需要使用命名参数的时候,才需要使用NamedParameterJdbcTemplate。这样的话,对于大多数的JDBC任务来说,JdbcTemplate就是最好的可选方案,这也是本小节中所关注的方案。
使用JdbcTemplate来插入数据
为了让JdbcTemplate正常工作,只需要为其设置DataSource就可以了,这使得在Spring中配置JdbcTemplate非常容易,如下面的@Bean方法所示:
在这里,DataSource是通过构造器参数注入进来的。这里所引用的dataSourcebean可以是javax.sql.DataSource的任意实现。
现在,我们可以将jdbcTemplate装配到Repository中并使用它来访问数据库。例如,SpitterRepository使用了JdbcTemplate:
在这里,JdbcSpitterRepository类上使用了@Repository注解,这表明它将会在组件扫描的时候自动创建。它的构造器上使用了
@Inject注解,因此在创建的时候,会自动获得一个JdbcOperations对象。JdbcOperations是一个接口,定义了JdbcTemplate所实现的操作。通过注入JdbcOperations,而不是具体的JdbcTemplate,能够保证JdbcSpitterRepository通过JdbcOperations接口达到与JdbcTemplate保持松耦合。
作为另外一种组件扫描和自动装配的方案,我们可以将JdbcSpitterRepository显式声明为Spring中的bean,如下所示:
在Repository中具备可用的JdbcTemplate后,我们可以极大地简化程序清单10.4中的addSpitter()方法。基于JdbcTemplate的addSpitter()方法如下:
在这里,你也看不到对SQLException处理的代码。在内部,JdbcTemplate将会捕获所有可能抛出的SQLException,并将通
用的SQLException转换为表10.1所列的那些更明确的数据访问异常,然后将其重新抛出。因为Spring的数据访问异常都是运行时异常,所以我们不必在addSpring ()方法中进行捕获。
使用JdbcTemplate来读取数据
JdbcTemplate也简化了数据的读取操作。程序清单10.8展现了新版本的findOne()方法,它使用了JdbcTemplate的回调,实现根据ID查询Spitter,并将结果集映射为Spitter对象。
在这个findOne()方法中使用了JdbcTemplate的queryForObject()方法来从数据库查询Spitter。queryForObject()方法有三个参数:
- String对象,包含了要从数据库中查找数据的SQL;
- RowMapper对象,用来从ResultSet中提取数据并构建域对象(本例中为Spitter);
- 可变参数列表,列出了要绑定到查询上的索引参数值。
真正奇妙的事情发生在SpitterRowMapper对象中,它实现了RowMapper接口。对于查询返回的每一行数据,JdbcTemplate将
会调用RowMapper的mapRow()方法,并传入一个ResultSet和包含行号的整数。在SpitterRowMapper的mapRow()方法中,我们
创建了Spitter对象并将ResultSet中的值填充进去。就像addSpitter()那样,findOne()方法也不用写JDBC模板代码。不同于传统的JDBC,这里没有资源管理或者异常处理代码。使用JdbcTemplate的方法只需关注于如何从数据库中获取Spitter对象即可。
在JdbcTemplate中使用Java 8的Lambda表达式
因为RowMapper接口只声明了addRow()这一个方法,因此它完全符合函数式接口(functional interface)的标准。这意味着如果使用Java 8来开发应用的话,我们可以使用Lambda来表达RowMapper的实现,而不必再使用具体的实现类了。
另外,我们还可以使用Java 8的方法引用,在单独的方法中定义映射逻辑:
使用命名参数
在清单10.7的代码中,addSpitter()方法使用了索引参数。这意味着我们需要留意查询中参数的顺序,在将值传递给update()方法的时候要保持正确的顺序。如果在修改SQL时更改了参数的顺序,那我们还需要修改参数值的顺序。
除了这种方法之外,我们还可以使用命名参数。命名参数可以赋予SQL中的每个参数一个明确的名字,在绑定值到查询语句的时候就通过该名字来引用参数。例如,假设SQL_INSERT_SPITTER查询语句是这样定义的:
使用命名参数查询,绑定值的顺序就不重要了,我们可以按照名字来绑定值。如果查询语句发生了变化导致参数的顺序与之前不一致,我们不需要修改绑定的代码
NamedParameterJdbcTemplate是一个特殊的JDBC模板类,它支持使用命名参数。在Spring中NamedParameterJdbcTemplate的声明方式与常规的JdbcTemplate几乎完全相同:
将NamedParameterJdbcOperations(NamedParameterJdbcTemplate所实现的接口)注入到Repository中,用它来替代JdbcOperations。现在的addSpitter()方法如下所示:
这个版本的addSpitter()比前一版本的代码要长一些。这是因为命名参数是通过java.util.Map来进行绑定的。不过,每行代码都关注于往数据库中插入Spitter对象。这个方法的核心功能并不会被资源管理或异常处理这样的代码所充斥
总结:
数据是应用程序的血液。有些数据中心论者甚至主张数据即应用。鉴于数据的重要地位,以健壮、简单和清晰的方式开发应用程序的数据访问部分就显得举足轻重了。在Java中,JDBC是与关系型数据库交互的最基本方式。但是按照规范,JDBC有些太笨重了。Spring能够解除我们使用JDBC中的大多数痛苦,包括消除样板式代码、简化JDBC异常处理,你所需要做的仅仅是关注要执行的SQL语句。
在本章中,我们学习了Spring对数据持久化的支持,以及Spring为JDBC所提供的基于模板的抽象,它能够极大地简化JDBC的使用。