第14章 JDBC API的最佳实践

14.1 基于Template的JDBC使用方式

JdbcTemplate和它的兄弟们

之前对JdbcTemplate的实现原理和细节说了那么多,无非是想让读者知道,当需要一个类似的轮子的时候,我们该如何去造一个出来。但是,当有现成的轮子存在的时候,我希望我们去使用这个现成的轮子,而不是耗费人力物力去重造一个。

所以,下面主要是告诉读者有哪些轮子可用,这些轮子又是如何使用的。最初,Spring框架只提供了JdbcTemplate这一个实现。但随着Java版本升级,并且考虑到使用中的便利性等问题,Spring在新发布的版本中又为JdbcTemplate添加了两位兄弟,一个是org.springframework.jdbc.core.simple.SimpleJdbcTemplate,主要面向Java5提供的一些便利;另一个是org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate,可以在SQL中使用名称代替原先使用的?占位符。

下面,让我们先从JdbcTemplate开始,亲身领略一下Spring所提供的数据访问方式的便利和优雅。

1. 使用JdbcTemplate进行数据访问

初始化JdbcTemplate

JdbcTemplate的初始化很容易,只要通过构造方法传入它所使用的DataSource就可以。如果使用Jakarta Commons DBCP,那么,初始化代码如下方代码清单所示。

    BasicDataSource dataSource=new BasicDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost/mysql?useUnicode=true&characterEncoding=utf8&failOverReadOnly=false&roundRobinLoadBalance=true");
    dataSource.setUsername("user");
    dataSource.setPassword("password");
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    //现在可以使用jdbcTemplate进行数据访问了
 

我们也可以通过无参数的构造方法来实例化JdbcTemplate,然后通过setDataSource()来设置所使用的DataSource。当然,这仅限于使用编码的方式来初始化JdbcTemplate。如果应用程序使用Spring的IoC容器,那么JdbcTemplate的初始化工作就可以转移到容器的配置文件中(见下方代码清单)。

 
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="c1ose">
      <property name="url">
        <value>$(jdbc.url)</value>
      </property>
      <property name="driverClassName">
        <value>$fjdbc.driver]</value>
      </property>
      <property name="username">
        <value>$(jdbc.username)</value>
      </property>
      <property name="password">
        <value>$(jdbc.password)</value>
      </property>
    </bean>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JabcTemplate">
    	<property name="dataSource">
    		<ref bean="dataSource"/>
    	</property>
    </bean>
 

之后,想把 JdbcTemplate 注入哪个依赖于它的对象都可以。不过,这里需要注意的不是 JdbcTemplate 的配置,而是 DataSource 的配置,我们使用了自定义的销毁对象的回调方法(destroy-method=“close”),以确保应用退出后,数据库连接可以关闭。

好了,JdbcTemplate初始化完成之后,我们就可以大展拳脚了。

基于JdbcTemplate的数据访问

这里仅从整体上对如何使用JdbcTemplate进行基本的数据访问进行介绍,更多JdbcTemplate使用的细节请参照Spring参考文档以及相应的Javadoc文档。

使用JdbcTemplate查询数据

JdbcTemplate针对数据查询提供了多个重载的模板方法,我们可以根据需要选用不同的模板方法。如果查询很简单,仅是传入相应SQL或者相关参数,然后取得一个结果,那么我们可以选择如下一组便利的模板方法:

 
 
    int queryForInt(String sql)
 
    int queryForInt(String sql, object[] args)
 
    long queryForLong(String sql)
 
    long queryForLong(String sq1, Object[] args)
 
    Object queryForobject(String sql, Class requiredType)
 
    Object queryForObject(String sq1, object[] args, Class requiredType)
 
    Map queryForMap(String sq1)
 
    Map queryForMap(String sql, Object[] args)
 

如果所查询的结果就包含一列数字型的结果,或者使用了SQL函数,或者其他单列的结果,那么就可以直接通过这组便利的模板方法进行查询(如下所示):

 
 
    int age = jdbcTemplate.queryForInt("select age from customer where customerId = ?", new Object[]{new Integer(100)});
    ...
    long interval = jdbcTemplate.queryForLong("select count(customerId) from customer");
    ...
    String customerName = jdbcTemplate.queryForString("select username from customer where customerId=110");
    ...
    Map singleCustomer = jdbcTemplate.queryForMap("select * from customer limit 1");
    ...
 

queryForMap方法与其他方法的不同之处在于,它的查询结果以java.util.Map的形式返回,Map的键对应所查询表的列名,Map的值是对应键所在列的值。当然,我们也看到了,这组模板方法主要用于单一结果的查询,使用的时候也请确保SQL查询所返回的结果是单一的,否则,JabcTemplate将抛出org.springframework.dao.IncorrectResultSizeDataAccessException异常。

如果查询的结果将返回多行,而我们又不在乎它们是否拥有较强的类型约束,那么以下模板方法可以帮助我们:

 
    List queryForList(String sql)
 
    List queryForList(String sql, Object[] args)

queryForList方法根据传入的SQL以及相应的参数执行查询,将查询的结果以java.util.List的形式返回,返回的java.util.List中的每个元素都是java.util.Map类型,分别对应结果集中的一行,Map的键为每列的列名,而Map的值就是当前行列名对应的值。如果这些还不足以满足我们的查询需要,那么就更进一步,使用相应的Callback接口对查询结果的返回进行定制。

更多不再介绍 有兴趣可以看原文

基于JdbcTemplate的数据更新

相对于查询来说,使用JdbcTemplate进行数据更新就没有那么多说道了。不管是要对数据库进行数据插入,还是更新甚至删除,我们都可以通过JdbcTemplate所提供的一组重载的update()模板方法进行,这些update方法包括:

 
 
    int update(String sql)
 
    int update(String sql, Object[] args)
 
    int update(String sql, Object[] args, int[] argTypes)
 
 

下面是一些update()方法的使用样例:

 
 
    // 插入数据
    jdbcTemplate.update("insert into customer(customerName,age,...) values('darren', 28,...)");
    // 更新数据
    int affectedRows = jdbcTemplate.update("update customer set customerName='daniel',age=36 where customerId=101");
    //或者
    int affectedRows = jdbcTemplate.update("update customer set customerName=?,age=? where customerId=?", new Object[]{"Daniel", new Integer(36), new Integer(101)));                 //删除数据
    int deletedRowCount = jdbcTemplate.update("delete from customer where customerId between 1 and 100");
 
 

通常情况下,接受简单的SQL以及相关参数的update方法就能够满足数据更新的需要。不过,如果我们觉得有必要对更新操作有更多的控制权,那么,可以使用与Preparedstatement相关的Callback接口。这包括使用PreparedStatementCreator创建Preparedstatement,使用PreparedstatementSetter对相关占位符进行设置等。同样的对一条记录进行更新,使用Callback接口作为参数的update方法的数据访问代码如下方代码清单所示。

 
 
    // 使用int update(String sql, PreparedStatementSetter pss)方法
    int affectedRows = jdbcTemplate.update(
      "update customer set customerName=?, age=? where customerId=?",
      new PrepareaStatementSetter() {
        public void setValues(PreparedStatement ps) throws SQLException {
          ps.setString(1, "Daniel");
          ps.setInt(2, 36);
          ps.setInt(3, 101);
        }
      }
    );
    // 使用int update(PreparedStatementCreator psc)方法
    int affectedRows = jdbcTemplate.update(new PreparedStatementCreator() {
      public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
        PreparedStatement ps = con.prepareStatement("update customer set customerName=?, age=? where customerId=?");
        ps.setString(1, "Daniel");
        ps.setInt(2, 36);
        ps.setInt(3, 101);
        return ps;
      }
    });
 
 

使用update方法进行数据更新,可以获得最终更新操作所影响的记录数目,而且,如果不单单指定一个SQL作为参数的话,JdbcTemplate内部会构造相应的PreparedStatement进行实际的更新操作。

不过,除了使用update方法,我们还可以通过只接受SQL语句作为参数的execute()方法进行数据更新。该方法没有返回值,更加适合那种不需要返回值的操作,比如删除表、创建表等操作。

如下代码给出了针对类似场景的代码示例:

 
 
    jdbcTemplate.execute("create table customer (...)");
    // 或者
    jdbcTemplate.execute("drop table customer");
 
 

至于其他重载的execute()方法,相对来说过于贴近JDBC API了,通常情况下,我们没有必要使用。某些时候为了集成遗留系统中某些基于JDBC的数据访问代码,倒是有可能需要求助于这些execute方法。

批量更新数据

对于“更新同一数据表”的多笔更新操作来说,我们可以使用JDBC的批量更新(Batch Update)功能对这些更新操作进行统一提交执行,以避免每一笔更新都单独执行,这样可以大大提高更新的执行效率。

JdbcTemplate提供了如下两个重载的batchUpdate()方法支持批量更新操作。

 
 
    int[] batchUpdate(String[] sql)
    int[] batchUpdate(String sql, BatchPreparedstatementSetter pss)
 
 

这两个方法在执行批量更新之前,会首先检查使用的JDBC驱动程序是否支持批量更新的功能,如果支持,则进行正常的批量更新;如果驱动程序不支持该功能,则会单独执行每一笔更新操作。

假设我们要将传入的新增加顾客列表的信息增加到数据库,那么就可以使用JabcTemplate的批量更新支持来完成这一任务(详情见下方代码清单)。

 
 
    public int[] insertNewCustomers(final List customers) {
      jdbcTemplate.batchUpdate("insert into customer value(?,?,...)", new BatchPreparedStatementSetter() {
        public int getBatchSize() {
          return customers.size();
        }
 
        public void setValues(PreparedStatement ps, int i) throws SQLException {
          Customer customer = (Customer) customers.get(i);
          ps.setString(1, customer.getFirstName());
          ps.setString(2, customer.getLastName());
          ...
        }
      });
    }
 
 

因为更新语句中涉及参数,所以,使用BatchPreparedstatementSetter回调接口来对批量更新中每次更新所需要的参数进行设置。BatchPreparedStatementSetter有如下两个方法需要我们实现。

  • int getBatchsize()。返回批量更新的数目,因为我们要对通过List传入的所有顾客信息进行更新,所以,当前批量更新的数目就是当前List中所有的顾客数目。

  • void setValues(Preparedstatementps,int i)。设置具体的更新数据,其中第二个int型的参数对应的是每笔更新的索引,我们就是根据这个索引从customers列表中取得相应的信息进行设置的。

Spring的批量更新相对于直接使用JDBC会有微小的性能损失。不过,当某些极端情况下(每个事务100万更新),使用Spring的批量更新可以取得很好的性能。

调用存储过程

存储过程是定义于数据库服务器端的计算单元。 对于涉及多表数据而只使用 SQL 无法完成的计算,我们可以通过在数据库服务器端编写并部署存储过程的方式来实现。相对于将这些计算逻辑转移到客户端进行,使用存储过程的好处在于,可以避免像客户端计算那样在网络间来回传送数据导致的性能损失,因为存储过程的所有计算全部在服务器端完成。如果计算涉及多个数据表、大量的数据查询和更新,那么使用存储过程代替客户端计算是比较合适的做法。

存储过程(Stored Procedure)不是核心SQL标准的一部分,所以,并非所有关系数据库都提供对存储过程的支持。但存储过程在许多企业应用中具有重要地位,所以,JDBC标准也通过提供CallableStatement支持对现有存储过程的调用。

更多不再介绍 有兴趣可以看原文

递增主键生成策略的抽象

在为关系数据库增加数据时,新增数据的主键生成一直是一个需要关注的问题。对于数据主键的生成位置,通常有两种选择:第一种选择是在数据库服务器端,使用不同的数据库厂商提供的主键生成策略支持,第二种选择是直接在应用程序的客户端,根据某些算法生成需要的数据主键。虽然第一种选择可以充分利用数据库的特性以及优化措施,但可移植性比较差,而且某些情况下可能造成数据库的过多负担;采用客户端的主键生成策略(即第二种选择),可以分担服务器的负担,而且主键的生成策略可以根据情况进行调整,灵活性很好,性能也可能随着系统架构的不同而有所提升。

在设计某些应用程序的时候, 出于主键生成策略的可移植性或者性能方面的考虑,会在应用程序客户端采用某种主键生成抽象策略,以统一的方式进行主键生成的管理 。大部分情况下,递增的主键生成策略是我们在这种情况下使用最多的主键生成策略。

我想,在此之前,你我或多或少都做过同样的事情。起码,我经历的FX项目就采用这种类似的主键生成策略。现在,Spring对递增的主键生成策略进行了适当的抽象,针对不同的关系数据库给出了相应的主键生成实现类,帮助我们统一基于递增策略的主键生成。Spring的org.springframework.jdbc.support.incrementer包下是针对递增主键生成策略的相关接口定义和实现类,org.springframework.jdabc.support.incrementer.DataFieldMaxValue-Incrementer是这个体系的项层接口定义,其定义如下:

    public interface DataFieldMaxValueIncrementer {
    	int nextIntValue() throws DataAccessException;
    	long nextLongValue() throws DataAccessException;
    	String nextStringValue() throws DataAccessException;
    }
 

更多不再介绍 有兴趣可以看原文

Spring中的LOB类型处理

LOB(Large OBject),指的是数据库中能够存取大量数据的数据类型。按照存放的具体数据形式,LOB类型通常分为BLOB和CLOB两种类型。

  • BLOB(Binary Large OBject),主要用于存放数据量比较大的二进制类型数据,如比较大的图像、Word文档之类的二进制文件。

  • CLOB(Character Large Object),主要用于存放数据量比较大的文本类型数据。

因为 LOB 类型针对的是大数据量数据,所以在处理数据的方式上与处理其他类型数据不同。 虽然,JDBC 标准对 LOB 类型数据操作已经进行了标准化的规定,但是,或许是历史性原因,某些数据库在处理 LOB 数据的时候,需要使用数据库特定的 API 才能处理,比如 Oracle。通常情况下,我们的应用程序数据访问逻辑需要根据后台的数据库类型进行相应调整。

除了Oracle,其他数据库在进行LOB字段的更新和读取的时候,与通常的字段类型没有太多差别,大都是通过Preparedstatement的相应方法设置LOB的值,然后通过ResultSet的相应方法获取结果集中的LOB数据。假设,我们有如下的表定义用于存放图像文件的相关数据(以MySQL方式定义):

 
    CREATE TABLE images(
      id int(11) NOT NULL,
      filename varchar(200) NOT NULL,
      entity b1ob NOT NULL,
    	description text NULIL,
    	PRIMARY KEY(id)
    )
 
 

只要不是Oracle数据库,我们就可以如下方代码清单所示,按照通常的JDBC操作方式对BLOB类型进行更新和读取(忽略异常处理)。

 
 
    //---将文件数据以二进制流的形式存入BLOB---
    File imageFile = new File("snow__image.jpg");
    InputStream ins = new FileInputStream(imageFile);
    Connection con = dataSource.getConnection();
    PreparedStatement ps = con.prepareStatement("insert into images(id,filename,entity,description) values(?,?,?,?)");
    ps.setInt(1, 1);
    ps.setString(2, "snow_image.jpg");
    pв.setBinaryStream(3, ins, (int) imageFile.length());
    ps.setString(4, "nothing to say");
    ps.executeUpdate();
    ps.close();
    con.close();
    IOUtils.closeQuietly(ins);
    ...
 
    //---以二进制流的形式读取数据---
    File imageFile = new File("snow_image_copy.jpg");
    InputStream ins = null;
    Connection con = dataSource.getConnection();
    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery("select entity from images where id=1");
    while (rs.next()) {
      ins = rs.getBinaryStream(1);
    }
    rs.close();
    stmt.close();
    con.close();
 
    OutputStream ous = new FileOutputStream(imageFile);
    IOUtils.write(IOUtils.toByteArray(ins), ous);
    IOUtils.closeQuietly(ins);
    IOUtils.closeQuietly(ous);
 
 

在这里,我们直接使用Preparedstatement的setBinarystream()方法,对BLOB类型数据进行存储,使用ResultSet的getBinaryStream()方法对BLOB数据进行读取(也可以使用针对Object和byte[]类型的Preparedstatement的setXXX()方法或者ResultSet的getXXX()方法对BLOB数据进行存储,更多信息请参考JDBC文档)。

可是一旦使用Oracle数据库,麻烦就来了,我们只能通过Oracle驱动程序提供的oracle.sql.BLOB或者oracle.sql.CLOB实现类对LOB数据进行操作,而无法通过标准的JDBCAPI进行。在Oracle 9i中,如果我们要同样地插入一笔数据,如下所示。

 
    Connection con = ...;
    Statement stmt = con.createStatement();
    // 1.要插入一笔BLOB数据,需要先插入empty blob以占位
    stmt.executeUpdate("insert into images(id,filename,entity,description) values(1,'snow_image.jpg',empty_blob(),'no desc')");
    // 2.取回对应记录的BLOB的locator,然后通过locator写入数据
    ResultSet rs = stmt.executeQuery("select entity from images where id=1");
    rs.next();
    BLOB blob = ((OracleResultSet) rs).getBLOB(1);
 
    File imageFile = new File("snow_image.jpg");
    InputStream ins = new FileInputStream(imageFile);
    OutputStream ous = blob.getBinaryOutputStream();
    IOUtils.write(IOUtils.toByteArray(ins), ous);
    IOUtils.closeQuietly(ins);
    IOUtils.closeQuietly(ous);
 
    rs.close();
    stmt.close();
    con.close();
 
 

对于查询来说,也要通过oracle.sql.BLOB或者oracle.sql.CLOB实现类进行,如下方代码清单所示。

 
    Connection con = ...;
    Statement stmt = con.createStatement();
 
    ResultSet rs = stmt.executeQuery("select entity from images where id=1");
    rs.next();
    BLOB blob = ((OracleResultSet) rs).getBLOB(1);
    // 使用blob.getBinaryStream()或者getBytes()方法处理结果即可
 
    rs.close();
    stmt.close();
    con.close();
 
 

鉴于对LOB数据处理方式的不一致性,Spring在org.springframework.jdbc.support.lob包下面,提出了一套LOB数据处理类,用于屏蔽各数据库驱动在处理LOB数据方式上的差异性。

org.springframework.jdbc.support.lob.LobHandler接口是Spring框架得以屏蔽LOB数据处理差异性的核心,它只定义了对BLOB和CLOB数据的操作接口,而具体的实现则留给具体的实现类来做。

我们可以通过LobHandler提供的各种BLOB和CLOB数据访问方法,以需要的方式对LOB进行读取。LobHandler定义见下方代码清单。

 
    public interface LobHandler {
        byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException;
 
        byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException;
 
        InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException;
 
        InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException;
 
        String getClobAsString(ResultSet rs, String columnName) throws SQLException;
 
        String getClobAsString(ResultSet rs, int columnIndex) throws SQLException;
 
        InputStream getClobAsAsciiStream(ResultSet rs, Str ing columnName) throws SQLException;
 
        InputStream getClobAsAsciiStream(ResultSet rs, int co1umnIndex) throws SQLException;
 
        Reader getClobAsCharacterStream(ResultSet rs, String columnName) throws SQLException;
 
        Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException;
 
        LobCreator getLobCreator();
    }
 
 

LobHandler除了作为LOB数据的访问接口,它还有一个角色,那就是它还是org.springframework.jdbc.support.lob.LobCreator的生产工厂,从LobHandler定义的最后一行应该看得出来。LobCreator的职责主要在于LOB数据的创建,它让我们能够以统一的方式创建LOB数据。我们将在插入或者更新LOB数据的时候使用它。不过在此之前,我们先把LobCreator放一边,继续关注LobHandler。

LobHandler的继承关系如图144所示。

image-20220502142127949

整个层次很简单,在LobHandler下实现了一个AbstractLobHandler抽象类以简化子类的实现。这个抽象类的逻辑很简单,就是将LobHandler中位于同一组的重载方法其中一个的逻辑委托给另一个,比如:

 
    public abstract class AbstractLobHandler implements LobHandler {
    	public byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException {
    		return getBlobAsBytes(rs, rs.findColumn(co1umnName));
      }
      ...
    }
 
 

所以,我们应该更多的关注OracleLobHandler和DefaultLobHandler这两个具体实现类。

这里不再介绍 有兴趣可以看原文

2. NamedParameterJdbcTemplate

对于每次调用都需要动态指定查询或者更新参数的SQL来说,通常或者说自始至终,我们都是通过?作为SQL参数的占位符的。有了NamedParameterJdbcTemplate的支持之后,我们就可以通过容易记忆或者更加有语义的符号来作为SQL中的参数占位符。

如果觉得使用语义符号作为占位符更好,那么我们就把?形式的占位符抛入到历史的尘埃中去。如果觉得两种占位符方式都还可以,那么我们也算有了更多的选择。

这里不再介绍 有兴趣可以看原文

3. SimpleJdbcTemplate

SimpleJdbcTemplate是Spring为构建在Java5或者更高版本Java.上的应用程序提供的更加便利的JdbcTemplate实现。

SimpleJdbcTemplate集JdbcTemplate和NamedParameterJdbcTemplate的功能于一身,并且在这二者上添加了Java5之后引入的动态参数(varargs)、自动拆箱解箱(autoboxing)和范型(generic)的支持。

现在,我们可以使用动态参数的形式取代object[]参数的形式,也可以利用自动拆箱解箱功能避免原始类型到相应封装类型的转换,又可以声明强类型的返回值类型,而不是只有Object。

这里不再介绍 有兴趣可以看原文