Spring实战(十)-通过Spring和JDBC征服数据库

本文基于《Spring实战(第4版)》所写。

Spring的数据访问哲学

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

为了避免应用与特定的数据访问策略耦合在一起,编写良好的Repository应该以接口的方式暴露功能。下图展现了设计数据访问层的合理方式。

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

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

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

接口与Spring:接口是实现松耦合代码的关键,并且应将其用于应用程序的各个层,而不仅仅是持久化层。还要说明一点,尽管Spring鼓励使用接口,但这并不是强制的—你可以使用Spring将bean(DAO或其他类型)直接装配到另一个bean的某个属性中,而不需要一定通过接口注入。

为了将数据访问层与应用程序的其他部分隔离开来,Spring采取的方式之一就是提供统一的异常体系,这个异常体系用在了它支持的所有持久化方案中。

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

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

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

SQLException的问题在于捕获到它的时候该如何处理。

Spring JDBC提供的数据访问异常体系具有描述性而且又与特定的持久化框架无关。不同于JDBC,Spring提供了多个数据访问异常,分别描述了它们抛出时所对应的问题。下表对比了Spring的部分数据访问异常以及JDBC所提供的异常。

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

JDBC的异常 Spring的数据访问异常
BatchUpdateException DataTruncation SQLException SQLWarning 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抛出一致的异常,而不用担心所选择的持久化方案。这有助于我们将所选择持久化机制与数据访问层隔离开来。

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

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

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

数据访问模板化

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

Spring的数据访问模板类负责通用的数据访问功能。对于应用程序特定的任务,则会调用自定义的回调对象

如图所示,Spring的模板类处理数据访问的固定部分—事物控制、管理资源以及处理异常。同时,应用程序相关的数据访问—语句、绑定参数以及整理结果集—在回调的实现中处理。你只需要关心自己的数据访问逻辑即可。

针对不同的持久化平台,Spring提供了多个可选的模板。如果直接使用JDBC,那可以选择JdbcTemplate。如果希望使用对象关系映射框架,那HibernateTemplate或JpaTemplate可能会更适合。下表列出了Spring所提供的所有数据访问模板及其用途。

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

Spring为多种持久化框架提供了支持。首先要说明的是Spring所支持的大多数持久化功能都依赖于数据源。因此,在声明模板和Repository之前,我们需要在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,如下所示:

<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很类似。例如,如下就是配置

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://localhost:3306/spitter"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
        <property name="initialSize" value="5"/>
        <property name="maxActive" value="10"/>
    </bean>

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

@Bean
public DruidDataSource dataSource(){
    DruidDataSource ds = new DruidDataSource();
    ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
    ds.setUrl("jdbc:mysql://localhost:3306/spitter");
    ds.setUsername("root");
    ds.setPassword("123456");
    ds.setInitialSize(5);
    ds.setMaxActive(10);
    return ds;
}

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

以上四个基本属性定义了DruidDataSource的连接信息。除此以外,还有多个配置数据源连接池的属性,如下表所示。DruidDataSource配置兼容DBCP,但个别配置的语意有所区别。

配置 缺省值 说明
name 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。
url 连接数据库的url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2
username 连接数据库的用户名
password 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。
driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8 最大连接池数量
maxIdle 8 已经不再使用,配置了也没效果
minIdle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxPoolPreparedStatementPerConnectionSize -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQueryTimeout 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
keepAlive false(1.0.28) 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
timeBetweenEvictionRunsMillis 1分钟(1.0.14) 有两个含义:1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun 30分钟(1.0.14) 不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis 连接保持空闲而不被驱逐的最小时间
connectionInitSqls 物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat;日志用的filter:log4j;防御sql注入的filter:wall
proxyFilters 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系

在我们的示例中,连接池启动时会创建5个连接;当需要的时候,允许DruidDataSource创建新的连接,但最大活跃连接数为10。

基于JDBC驱动的数据源

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

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

如下是配置DriverManagerDataSource的方法:

@Bean
public DataSource dataSource(){
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
    ds.setUrl("jdbc:mysql://localhost:3306/spitter");
    ds.setUsername("root");
    ds.setPassword("123456");
    return ds;
}

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

    <bean id="dataSource" class="com.springframework.jdbc.datasource.DriverManangerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://localhost:3306/spitter"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>

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

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

使用嵌入式的数据源

除此之外,还有一个数据源方案:嵌入式数据库(embedded database)。嵌入式数据库作为应用的一部分运行,而不是应用连接的独立数据库服务器。对于开发和测试来讲,嵌入式数据库是很好的可选方案。

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/aop/spring-jdbc-3.1.xsd
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
...
    <jdbc:embedded-database 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>中,我们可以不配置也可以配置多个<jdbc:script>元素:第一个引用了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();
}

使用profile选择数据源

实际上,我们很可能面临这样一种需求,那就是在某种环境下需要其中一种数据源,而在另外的环境中需要不同的数据源。

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

Spring的bean profile特性恰好就用在这里,所需要做的就是将每个数据源配置在不同的profile中,如下所示:

package com.myapp;

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.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
import com.alibaba.druid.pool.DruidDataSource;

@Configuration
public class DataSourceConfig {
  
  @Bean(destroyMethod = "shutdown")
  @Profile("dev")
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.H2)
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
  }

  @Bean
  @Profile("prod")
  public DataSource jndiDataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/myDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }


   @Bean()
   @Profile("qa")
   public DataSource dataSource(){
       DruidDataSource source = new DruidDataSource();
       source.setDriverClassName("com.mysql.jdbc.Driver");
       source.setUsername("root");
       source.setPassword("123456");
       source.setUrl("jdbc:mysql://localhost:3306/test");
       source.setInitialSize(1);
       source.setMaxActive(100);
       source.setMinIdle(0);
       source.setMaxWait(60000);
       return source;
   }

}

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

为了内容的完整性,如下的程序清单展现了如何使用Spring XML代替Java配置,实现相同的profile配置。

<?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/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

  <beans profile="dev">
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
  </beans>
  
  <beans profile="prod">
    <jee:jndi-lookup id="dataSource"
      lazy-init="true"
      jndi-name="jdbc/myDatabase"
      resource-ref="true"
      proxy-interface="javax.sql.DataSource" />
  </beans>
</beans>
<beans profile="qa">
    <bean id="dataSource" class="com.springframework.jdbc.datasource.DriverManangerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://localhost:3306/spitter"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>
</beans>

现在我们已经通过数据源建立了与数据库的连接,接下来要实际访问数据库了。Spring 为我们提供了多种使用数据库的方式包括JDBC、Hibernate以及Java持久化API(Java Peristence API, JPA)。

在Spring中使用JDBC

JDBC不要求我们掌握其他框架的查询语言。它是建立在SQL之上的,而SQL本身就是数据访问语言。此外,与其他的技术相比,使用JDBC能够更好地对数据访问的性能进行调优。JDBC允许你使用数据库的所有特性,而这是其他框架不鼓励甚至禁止的。

应对失控的JDBC代码

如果使用JDBC所提供的直接操作数据库的API,你需要负责处理与数据库访问相关的所有事情,其中包含管理数据库资源和处理异常。例如,如下所示

private static final String SQL_INSERT_SPITTER =
            "insert into spitter (username, password, firstname) values (?, ?, ?)";
    private DataSource dataSource;
    public void addSpitter(Spitter spitter) {
        Connection conn = null;
        PreparedStatement 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.getFirstName());
            stmt.execute();   // 执行语句
        }catch (SQLException e){
            // 处理异常
        }
        finally {
            try {
                if (stmt != null) {  // 清理资源
                    stmt.close();
                }

                if (conn != null) {
                    conn.close();
                }
            }
            catch (SQLException e) {

            }
        }

    }

仅仅就是往数据库中插入一条数据,却使用了超过20行的代码。更新的过程与插入类似。下面我们来看看如何从数据库中获取数据。如下所示

    private static final String SQL_SELECT_SPITTER =
            "select id, username, firstname from spitter where id = ?";
    public Spitter findOne(long id) {
        Connection conn = null;
        PreparedStatement 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.setFirstName(rs.getString("firstname"));
            }
            return spitter;

        }
        catch (SQLException e){
            // (以某种方式处理异常)
        }
        finally {
            // 清理资源
            if (rs != null) {
                try {
                    rs.close();
                }
                catch (SQLException e) {}
            }

            if (stmt != null) {
                try {
                    stmt.close();
                }
                catch (SQLException e){ }
            }
            if (conn != null) {
                try {
                    conn.close();
                }
                catch (SQLException e) {}
            }
        }
        return  null;
    }

虽然大量的JDBC代码都是用于创建连接和语句以及异常处理的样板代码,很冗余。但实际上,这些样板代码是非常重要的。资源清理和处理错误确保了数据访问的健壮性。我们不仅需要这些代码,而且还要保证它是正确的。

使用JDBC模板

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

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

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

从Spring 3.1,SimpleJdbcTemplate已经被废弃了,其Java 5的特性被转移到了JdbcTemplate中,并且只有在需要使用命名空间参数的时候,才需要使用NameParameterJdbcTemplate。对于大多数的JDBC任务来说,JdbcTemplate就是最好的可选方案。

为了让JdbcTemplate正常功能,只需要为其设置DataSource就可以了,这使得在Spring中配置JdbcTemplate非常容易,如下所示:

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

在这里DataSource是通过构造器参数注入进来的。这里所引用的dataSource bean可以是javax.sql.DataSource的任意实现,包括了之前文中所创建的。

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

@Repository
public class JdbcSpitterRepository implements SpitterRepository{

    private JdbcOperations jdbcOperations;

    @Autowired
    public JdbcSpitterRepository(JdbcOperations jdbcOperations) {
        this.jdbcOperations = jdbcOperations;
    }
...
}

在这里,JdbcSpitterRepository类上使用了@Repository注解,这表明它将会在组件扫描的时候自动创建。它的构造器上使用了@Autowired(或使用@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() 方法。如下所示:

public void addSpitter(Spitter spitter)  {
     jdbcOperations.update("insert into Spitter (username, password, firstname) values (?, ?, ?)",
            spitter.getUsername(),
            spitter.getPassword(),
            spitter.getFirstName();
}

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

在这里,也看不到对SQLException处理的代码。在内部,JdbcTemplate将会捕获所有可能抛出的SQLException,并将通过的SQLException转换为Spring的哪些更明确的数据访问异常,然后将其重新抛出。因为Spring的数据访问异常都是运行时异常,所以我们不必在addSpring() 方法中进行捕获。

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

public Spitter findOne(long id) {
    return jdbcOperations.queryForObject(
                "select username, password, firstname from Spitter where id=?",
                new SpitterRowMapper(),  // 将查询结果映射到对象
                id);
}

...

public static final class SpitterRowMapper implements RowMapper<Spitter>{
    public Spitter mapRow(ResultSet rs, int rowNum) throw SQLException {
        return new Spitter (
            rs.getLong("id"),
            rs.getString("username"),
            rs.getString("password"),
            rs.getString("firstname"));
    }
}

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

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

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

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

public Spitter findOne(long id) {
    return jdbcOperations.queryForObject(
        "select username, password, firstname from Spitter where id=?",
        (rs, rowNum) -> {
            return new Spitter(
            rs.getLong("id"),
            rs.getString("username"),
            rs.getString("password"),
            rs.getString("firstname"));
        },id);
}

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

public Spitter findOne(long id) {
    return jdbcOperations.queryForObject(
                "select username, password, firstname from Spitter where id=?",
                this::mapSpitter,  
                id);
}

private Spitter mapSpitter(ResultSet rs, int row) throws SQLException {
    return new Spitter (
            rs.getLong("id"),
            rs.getString("username"),
            rs.getString("password"),
            rs.getString("firstname"));
}

不管采用哪种方式,我们都不必显示实现RowMapper接口,但是与实现RowMapper类似,我们所提供的Lambda表达式和方法必须要接口相同的参数,并返回相同的类型。

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

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

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

使用命名参数查询,绑定值的顺序就不重要了,我们可以按照名字来绑定值。如果查询语句发生了变化导致参数的顺序与之前不一致,我们不需要修改绑定的代码。

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

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

addSpitter() 方法如下所示:

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

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("firstname", spitter.getFirstName());

    jdbcOperations.update(SQL_INSERT_SPITTER, paramMap);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容