spring实战 通过Spring和JDBC征服数据库

345 阅读18分钟

通过Spring和JDBC征服数据库

Spring的数据访问哲学

服务对象本身并不会处理数据访问,而是将数据访问委托给Repository。Repository接口确保其与服务对象的松耦合

服务对象通过接口来访问Repository。这样做会有几个好处。第一,它使得服务对象易于测试,因为它们不再与特定的数据访问实现绑定在一起。实际上,你可以为这些数据访问接口创建mock实现,这样无需连接数据库就能测试服务对象,而且会显著提升单元测试的效率并排除因数据不一致所造成的测试失败。

数据访问层是以持久化技术无关的方式来进行访问的。持久化方式的选择独立于Repository,同时只有数据访问相关的方法才通过接口进行暴露。这可以实现灵活的设计,并且切换持久化框架对应用程序其他部分所带来的影响最小。如果将数据访问层的实现细节渗透到应用程序的其他部分中,那么整个应用程序将与数据访问层耦合在一起,从而导致僵化的设计。

了解Spring的数据访问异常体系

可能导致抛出SQLException的常见问题包括:

  • 应用程序无法连接数据库
  • 要执行的查询存在语法错误
  • 查询中所使用的表和/或列不存在
  • 试图插入或更新的数据违反了数据库约束

SQLException的问题在于捕获到它的时候该如何处理。事实上,能够触发SQLException的问题通常是不能在catch代码块中解决的。大多数抛出SQLException的情况表明发生了致命性错误。如果应用程序不能连接到数据库,这通常意味着应用不能继续使用了。类似地,如果查询时出现了错误,那在运行时基本上也是无能为力。

即使对某些SQLException有处理方案,还是要捕获SQLException并查看其属性才能获知问题根源的更多信息。这是因为SQLException被视为处理数据访问所有问题的通用异常。对于所有的数据访问问题都会抛出SQLException,而不是对每种可能的问题都会有不同的异常类型。

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

Spring为读取和写入数据库的几乎所有错误都提供了异常。Spring的数据访问异常要比下表所列的还要多。

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

看!不用写catch代码块

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

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

数据访问模板化

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

Spring的模板类处理数据访问的固定部分——事务控制、管理资源以及处理异常。同时,应用程序相关的数据访问——语句、绑定参数以及整理结果集——在回调的实现中处理。事实证明,这是一个优雅的架构,因为你只需关心自己的数据访问逻辑即可。

针对不同的持久化平台,Spring提供了多个可选的模板。如果直接使用JDBC,那你可以选择JdbcTemplate。如果你希望使用对象关系映射框架,那HibernateTemplate或JpaTemplate可能会更适合你。

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

配置数据源

无论选择Spring的哪种数据访问方式,你都需要配置一个数据源的引用。Spring提供了在Spring上下文中配置数据源bean的多种方式,包括:

  • 通过JDBC驱动程序定义的数据源
  • 通过JNDI查找的数据源
  • 连接池的数据源

使用JNDI数据源

Spring应用程序经常部署在Java EE应用服务器中,如WebSphere、JBoss或甚至像Tomcat这样的Web容器中。这些服务器允许你配置通过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/SpitterDS");
    jndiObjectFB.setResourceRef(true);
    jndiObjectFB.setProxyInterFace(javax.sql.DataSource.class);
    return jndiObjectFB;
}

使用数据源连接池

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

这些连接池中的大多数都能配置为Spring的数据源,在一定程度上与Spring自带的DriverManagerDataSource或SingleConnectionDataSource很类似。例如,如下就是配置DBCPBasicDataSource的方式:

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

如果喜欢Java配置的话,连接池形式的DataSourcebean可以声明如下:

@Bean
public BasicDataSource dataSource() {
    BasicDataSource ds = new BasicDataSource();
    ds.setDriverClassName("org.h2.driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    ds.setInitialSize("5");
    ds.setMaxActive(10);
    return ds;
}

前四个属性是配置BasicDataSource所必需的。属性driverClassName指定了JDBC驱动类的全限定类名。在这里我们配置的是H2数据库的数据源。属性url用于设置数据库的JDBC URL。最后,username和password用于在连接数据库时进行认证。以上四个基本属性定义了BasicDataSource的连接信息。除此以外,还有多个配置数据源连接池的属性。下表列出了DBCP BasicDataSource最有用的一些池配置属性:

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

基于JDBC驱动的数据源

在Spring中,通过JDBC驱动定义数据源是最简单的配置方式。Spring提供了三个这样的数据源类(均位于org.springframework.jdbc.datasource包中)供选择:

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

以上这些数据源的配置与DBCPBasicDataSource的配置类似。例如,如下就是配置DriverManagerDataSource的方法:

@Bean
public DataSource dataSource() {
    DriverManagerClassName ds = new DriverManagerClassName();
    ds.setDriverClassName("org.h2.Driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    return ds;
}

如果使用XML的话,DriverManagerDataSource可以按照如下的方式配置:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" p:driverClassName="org.h2.Driver" p:url="jdbc:h2:tcp://localhost/~/spitter" p:username="sa" p:passowrd="" />

与具备池功能的数据源相比,唯一的区别在于这些数据源bean都没有提供连接池功能,所以没有可配置的池相关的属性。

尽管这些数据源对于小应用或开发环境来说是不错的,但是要将其用于生产环境,你还是需要慎重考虑。因为SingleConnectionDataSource有且只有一个数据库连接,所以不适合用于多线程的应用程序,最好只在测试的时候使用。而DriverManagerDataSource和SimpleDriverDataSource尽管支持多线程,但是在每次请求连接的时候都会创建新连接,这是以性能为代价的。鉴于以上的这些限制,强烈建议应该使用数据源连接池。

使用嵌入式的数据源

嵌入式数据库作为应用的一部分运行,而不是应用连接的独立数据库服务器。尽管在生产环境的设置中,它并没有太大的用处,但是对于开发和测试来讲,嵌入式数据库都是很好的可选方案。这是因为每次重启应用或运行测试的时候,都能够重新填充测试数据。

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:jdbc="http://www.springframework.org/schema/jdbc" xmlns:c="http://www.springframework.org/schema/c" 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="com/habuma/spitter/db/jdbc/schema.sql" />
        <jdbc:script location="com/habuma/spitter/db/jdbc/test-data.sql" />
    </jdbc:embedded-database>
    ...
</beans>

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

在<jdbc:embedded-database >中,可以不配置也可以配置多个元素来搭建数据库。上述程序中包含了两 个元素:第一个引用了schema.sql,它包含了在数据库中创建表的SQL;第二个引用了test-data.sql,用来将测试数据填充 到数据库中。

除了搭建嵌入式数据库以外,<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).addScript("classpath:schema.sql").addScript("classpath:test-data.sql").build();
}

可以看到,setType()方法等同于<jdbc:embedded-database >元素中的type属性,此外,我们这里用addScript()代替<jdbc:script >元素来指定初始化SQL。

使用profile选择数据源

对于开发期来说,<jdbc:embedded-database >元素是很合适的,而在QA环境中,可能希望使用DBCP的BasicDataSource,在生产部署环境下,可能需要使用<jee:jndi-lookup >。

package com.habuma.spittr.config;
import org.apache.commons.dbcp.BasicDataSource;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;

@Configuration
public class DataSourceConfiguration {
    @Profile("development") //开发数据源
    @Bean
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).addScript("classpath:schema.sql").addScript("classpath:test-data.sql").build();
    }
    
    @Profile("qa") //QA数据源
    @Bean
    public DataSource Data() {
        BasicDataSource ds = new BasicDataSource();
    ds.setDriverClassName("org.h2.driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    ds.setInitialSize("5");
    ds.setMaxActive(10);
    return ds;
    }
    
    @Profile("production") //生产数据源
    @Bean
    public DataSource dataSource() {
        JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
    jndiObjectFB.setJndiName("jdbc/SpitterDS");
    jndiObjectFB.setResourceRef(true);
    jndiObjectFB.setProxyInterFace(javax.sql.DataSource.class);
    return jndiObjectFB;
    }
}

通过使用profile功能,会在运行时选择数据源,这取决于哪一个profile处于激活状态。如上述程序配置所示,当且仅当developmentprofile处于激活状态时,会创建嵌入式数据库,当且仅当qa profile处于激活状态时,会创建DBCP BasicDataSource,当且仅当productionprofile处于激活状态时,会从JNDI获取数据源。

<?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:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema.jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd http://www.springframework.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" 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" />
    </beans>
    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="/jdbc/spitterDs" resource-ref="true" />
    </beans>
</beans>

在Spring中使用JDBC

应对失控的JDBC代码

private static final String SQL_INSERT_SPITTER = "insert into spitter (username, password, fullname) values (?, ?, ?)";
private DataSource dataSource;
public void addSpitter(Spitter spitter) {
    Connection conn = null;
    PrepareStatement stmt = null;
    try {
        conn = dataSource.getConnection();
        stmt = conn.prepareStatement(SQL_INSERT_SPITTER);
        stmt.setString(1, spitter.getUsername());
        stmt.setString(2, spitter.getPassword());
        stmt.setString(3, spitter.getFullName());
        stmt.execute();
    } catch(SQLException e) {
        //do something...not sure what, though
    } finally {
        try {
            if(stmt != null) {
            	smst.close();
        	}
        	if(conn != null) {
            	conn.close();
        	}
        } catch(SQLException e) {
          //I'm even less sure about what to do here  
        }
        
    }
}
private static final String SQL_SELECT_SPITTER = "select id, username, fullname from spitter where id = ?";
private DataSource dataSource;
public void findOne(long id) {
    Connection conn = null;
    PrepareStatement stmt = null;
    ResultSet rs = null;
    try {
        conn = dataSource.getConnection();
        stmt = conn.prepareStatement(SQL_SELECT_SPITTER);
       stmt.setLong(1, id);
        rs = stmt.executeQuery();
        Spitter spitter = null;
        if (rs.next()) {
            spitter = new Spitter();
            spitter.setId(rs.getLong("id"));
            spitter.setUsername(rs.getString("username"));
            spitter.setPassword(rs.getString("password"));
            spitter.setFullName(rs.getString("fullname"));
            return spitter;
        }
    } catch(SQLException e) {
        //do something...not sure what, though
    } finally {
        try {
            if(stmt != null) {
            	smst.close();
        	}
        	if(conn != null) {
            	conn.close();
        	}
        } catch(SQLException e) {
          //I'm even less sure about what to do here  
        }
        
    }
}

上述2个程序除了SQL字符串和创建语句的那一行,它们是完全相同的。同样,这里也使用大量代码来完成一件简单的事情,而且有很多重复的代码。

private static final String SQL_UPDATE_SPITTER = "update spitter set username = ?, password = ?, fullname = ? where id = ?";
private DataSource dataSource;
public void saveSpitter(Spitter spitter) {
    Connection conn = null;
    PrepareStatement stmt = null;
    try {
        conn = dataSource.getConnection();
        stmt = conn.prepareStatement(SQL_UPDATE_SPITTER);
        stmt.setString(1, spitter.getUsername());
        stmt.setString(2, spitter.getPassword());
        stmt.setString(3, spitter.getFullName());
        stmt.setLong(4, spitter.getId())
        stmt.execute();
    } catch(SQLException e) {
        //do something...not sure what, though
    } finally {
        try {
            if(rs != null){
                try {
                    rs.close();
                } catch(SQLException e) {
                    
                }
            }
            if(stmt != null) {
            	try {
                    smst.close();
                } catch(SQLException e) {
                    
                }
        	}
        	if(conn != null) {
            	try {
                    conn.close();
                } catch (SQLException e) {
                    
                }
        	}    
    }
        return null;
}

这段代码与插入和更新的样例一样冗长,甚至更为复杂。这就好像Pareto法则被倒了过来:只有20%的代码是真正用于查询数据的,而80%代 码都是样板代码。

使用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方法所示:

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

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

现在,可以将jdbcTemplate装配到Repository中并使用它来访问数据库。例如,SpitterRepository使用了JdbcTemplate:

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

在这里,JdbcSpitterRepository类上使用了@Repository注解,这表明它将会在组件扫描的时候自动创建。它的构造器上使用了@Inject注解,因此在创建的时候,会自动获得一个JdbcOperations对象。JdbcOperations是一个接口,定义了JdbcTemplate所实现的操作。通过注入JdbcOperations,而不是具体的JdbcTemplate,能够保证JdbcSpitterRepository通 过JdbcOperations接口达到与JdbcTemplate保持松耦合。

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

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

在Repository中具备可用的JdbcTemplate后,可以极大地简化addSpitter()方法。基于JdbcTemplate的addSpitter()方法如下:

public void addSpitter(Spitter spitter) {
    jdbcOperations.update(INSERT_APITTER,
                          spitter.getUsername(),
                          spitter.getPassword(),
                          spitter.getFullName(),
                          spitter.getEmail(),
                          spitter.isUpdateByEmail());
}

不能因为看不到这些样板代码,就意味着它们不存在。样板代码被巧妙地隐藏到JDBC模板类中了。当update()方法被调用的时候JdbcTemplate将会获取连接、创建语句并执行插入SQL。

使用JdbcTemplate来读取数据

JdbcTemplate也简化了数据的读取操作。下列程序展现了新版本的findOne()方法,它使用了JdbcTemplate的回调,实现根据ID查询Spitter,并将结果集映射为Spitter对象。

public Spitter findOne(long id) {
    return jdbcOperations.queryForObject(SELECT_SPITTER_BY_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("fullname"), rs.getString("email"), rs.getBoolean("updateByEmail"));
        }
    }

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

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

正奇妙的事情发生在SpitterRowMapper对象中,它实现了RowMapper接口。对于查询返回的每一行数据,JdbcTemplate将会调用RowMapper的mapRow()方法,并传入一个ResultSet和包含行号的整数。在SpitterRowMapper的mapRow()方法中,创建了Spitter对象并将ResultSet中的值填充进去。

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

因为RowMapper接口只声明了addRow()这一个方法,因此它完全符合函数式接口(functional interface)的标准。这意味着如果使用Java 8来开发应用的话,可以使用Lambda来表达RowMapper的实现,而不必再使用具体的实现类了。

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("fullname"), rs.getString("email"), rs.getBoolean("updateByEmail"));
    }, id);
}

我们可以看到,Lambda表达式要比完整的RowMapper实现更为易读,不过它们的功能是相同的。Java会限制RowMapper中的Lambda表达式,使其满足所传入的参数。

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

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

使用命名参数

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

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

private static final String 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(NamedParameterJdbcTemplate所实现的接口)注入到Repository中,用它来替代JdbcOperations。现在的addSpitter()方法如下所示:

private static final String INSERT_SPITTER = "insert into Spitter(username, password, fullname, email, updateByEmail) values(:username, :password, :fullname, :email, :updateByEmail)";

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

这个版本的addSpitter()比前一版本的代码要长一些。这是因为命名参数是通过java.util.Map来进行绑定的。不过,每行代码都关注于往数据库中插入Spitter对象。这个方法的核心功能并不会被资源管理或异常处理这样的代码所充斥。