第 10 章 使用JDBC进行数据访问

10.1. 简介

Spring提供的JDBC抽象框架由coredatasourceobject和 support四个不同的包组成。

就和它名字的暗示一样,org.springframework.jdbc.core包里定义了提供核心功能的类。 其中有各种SQLExceptionTranslator和DataFieldMaxValueIncrementer的实现以及一个用于JdbcTemplate的DAO基础类。

org.springframework.jdbc.datasource包里有一个用以简化数据源访问的工具类, 以及各种数据源的简单实现,以被用来在J2EE容器之外不经修改地测试JDBC代码。 这个工具类提供了从JNDI获得连接和可能用到的关闭连接的静态方法。 它支持绑定线程的连接,比如被用于DataSourceTransactionManager。

接着,org.springframework.jdbc.object包里是把关系数据库的查询, 更新和存储过程封装为线程安全并可重用对象的类。 这中方式模拟了JDO,尽管查询返回的对象理所当然的“脱离”了数据库连接。 这个JDBC的高层抽象依赖于org.springframework.jdbc.core包中所实现的底层抽象。

最后在org.springframework.jdbc.support包中你可以找到 SQLException的转换功能和一些工具类。

在JDBC调用中被抛出的异常会被转换成在定义org.springframework.dao包中的异常。 这意味着使用Spring JDBC抽象层的代码不需要实现JDBC或者RDBMS特定的错误处理。 所有的转换后的异常都是unchecked,它允许你捕捉那些你可以恢复的异常, 并将其余的异常传递给调用者。

10.2. 使用JDBC核心类控制基本的JDBC处理和错误处理

10.2.1. JdbcTemplate

这是在JDBC核心包中最重要的类。它简化了JDBC的使用, 因为它处理了资源的建立和释放。 它帮助你避免一些常见的错误,比如忘了总要关闭连接。它运行核心的JDBC工作流, 如Statement的建立和执行,而只需要应用程序代码提供SQL和提取结果。这个类执行SQL查询, 更新或者调用存储过程,模拟结果集的迭代以及提取返回参数值。它还捕捉JDBC的异常并将它们转换成 org.springframework.dao包中定义的通用的,能够提供更多信息的异常体系。

使用这个类的代码只需要根据明确定义的一组契约来实现回调接口。 PreparedStatementCreator回调接口创建一个由该类提供的连接所建立的PreparedStatement, 并提供SQL和任何必要的参数。CallableStatementCreateor实现同样的处理, 只是它创建了CallableStatement。RowCallbackHandler接口从数据集的每一行中提取值。

这个类可以直接通过数据源的引用实例化,然后在服务中使用, 也可以在ApplicationContext中产生并作为bean的引用给服务使用。 注意:数据源应当总是作为一个bean在ApplicationContext中配置, 第一种情况它被直接传递给服务,第二种情况给JdbcTemplate。 因为这个类把回调接口和SQLExceptionTranslator接口作为参数表示,所以没有必要为它定义子类。 这个类执行的所有SQL都会被记入日志。

10.2.2. 数据源

为了从数据库获得数据,我们需要得到数据库的连接。 Spring采用的方法是通过一个数据源数据源是JDBC规范的一部分,它可以被认为是一个通用的连接工厂。 它允许容器或者框架将在应用程序代码中隐藏连接池和事务的管理操作。 开发者将不需要知道连接数据库的任何细节,那是设置数据源的管理员的责任。 虽然你很可能在开发或者测试的时候需要兼任两种角色,但是你并不需要知道实际产品中的数据源是如何配置的。

使用Spring的JDBC层,你可以从JNDI得到一个数据源,也可以通过使用Spring发行版提供的实现自己配置它。 后者对于脱离Web容器的单元测试是十分便利的。 我们将在本节中使用DriverManagerDataSource实现,当然以后还会提到其他的一些的实现。 DriverManagerDataSource和传统的方式一样获取JDBC连接。 为了让DriverManager可以装载驱动类,你必须指定JDBC驱动完整的类名。 然后你必须提供相对于各种JDBC驱动的不同的URL。你必须参考你所用的驱动的文档,以获得需要使用的正确参数。 最后,你还必须提供用来连接数据库的用户名和密码 下面这个例子说明如何配置DriverManagerDataSource:

DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName( "org.hsqldb.jdbcDriver");
dataSource.setUrl( "jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername( "sa");
dataSource.setPassword( ""); 

10.2.3. SQLExceptionTranslator

SQLExceptionTranslator是一个需要实现的接口, 它被用来处理SQLException和我们的数据访问异常org.springframework.dao.DataAccessException之间的转换。

实现可以是通用的(比如使用JDBC的SQLState值),也可以为了更高的精确度特殊化 (比如使用Oracle的ErrorCode)。

SQLErrorCodeSQLExceptionTranslator 是SQLExceptionTranslator的实现,它被默认使用。比供应商指定的SQLState更为精确。 ErrorCode的转换是基于被保存在SQLErrorCodes型的JavaBean中的值。 这个类是由SQLErrorCodesFactory建立并填充的,就像它的名字说明的, SQLErrorCodesFactory是一个基于一个名为"sql-error-codes.xml"的配置文件的内容建立SQLErrorCodes的工厂。 这个文件带有供应商的码一起发布,并且是基于DatabaseMetaData信息中的DatabaseProductName,适合当前数据库的码会被使用。

SQLErrorCodeSQLExceptionTranslator使用以下的匹配规则:

  • 使用子类实现的自定义转换。要注意的是这个类本身就是一个具体类,并可以直接使用, 在这种情况下,将不使用这条规则。

  • 使用ErrorCode的匹配。在默认情况下,ErrorCode是从SQLErrorCodesFactory得到的。 它从classpath中寻找ErrorCode,并把从数据库metadata中得到的数据库名字嵌入它们。

  • 如果以上规则都无法匹配,那么是用SQLStateSQLExceptionTranslator作为默认转换器。

SQLErrorCodeSQLExceptionTranslator可以使用以下的方式继承:

public class MySQLErrorCodesTransalator extends SQLErrorCodeSQLExceptionTranslator {
    protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) {
        if (sqlex.getErrorCode() == -12345)
            return new DeadlockLoserDataAccessException(task, sqlex);
        return null;
    }
}

在这个例子中,只有特定的ErrorCode'-12345'被转换了,其他的错误被简单的返回, 由默认的转换实现来处理。要使用自定义转换器时,需要通过setExceptionTranslator 方法将它传递给JdbcTemplate,并且在所有需要使用自定义转换器的数据访问处理中使用这个JdbcTemplate 下面是一个如何使用自定义转换器的例子:

// create a JdbcTemplate and set data source 
JdbcTemplate jt = new JdbcTemplate(); 
jt.setDataSource(dataSource); 
// create a custom translator and set the datasource for the default translation lookup 
MySQLErrorCodesTransalator tr = new MySQLErrorCodesTransalator(); 
tr.setDataSource(dataSource); 
jt.setExceptionTranslator(tr); 
// use the JdbcTemplate for this SqlUpdate 
SqlUpdate su = new SqlUpdate(); 
su.setJdbcTemplate(jt); 
su.setSql("update orders set shipping_charge = shipping_charge * 1.05"); 
su.compile(); 
su.update();

这个自定义的转换器得到了一个数据源, 因为我们仍然需要默认的转换器在sql-error-codes.xml中查找ErrorCode。

10.2.4. 执行Statement

要执行一个SQL,几乎不需要代码。你所需要的全部仅仅是一个数据源和一个JdbcTemplate。 一旦你得到了它们,你将可以使用JdbcTemplate提供的大量方便的方法。 下面是一个例子,它显示了建立一张表的最小的但有完整功能的类。

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAStatement {
    private JdbcTemplate jt;
    private DataSource dataSource;

    public void doExecute() {
        jt = new JdbcTemplate(dataSource);
        jt.execute("create table mytable (id integer, name varchar(100))"); 
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

10.2.5. 执行查询

除了execute方法,还有大量的查询方法。其中的一些被用来执行那些只返回单个值的查询。 也许你需要得到合计或者某一行中的一个特定的值。如果是这种情况,你可以使用queryForIntqueryForLong或者queryForObject。 后者将会把返回的JDBC类型转换成参数中指定的Java类。如果类型转换无效,那么将会抛出一个InvalidDataAccessApiUsageException。 下面的例子有两个查询方法,一个查询得到int,另一个查询得到String

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class RunAQuery {
    private JdbcTemplate jt;
    private DataSource dataSource;
  
    public int getCount() {
        jt = new JdbcTemplate(dataSource);
        int count = jt.queryForInt("select count(*) from mytable");
        return count;
    }

    public String getName() {
        jt = new JdbcTemplate(dataSource);
        String name = (String) jt.queryForObject("select name from mytable", java.lang.String.class);
        return name;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

除了得到单一结果的查询方法之外,还有一些方法,可以得到一个包含查询返回的数据的List。 其中最通用的一个方法是queryForList,它返回一个List, 其中每一项都是一个表示字段值的Map。 你用下面的代码在上面的例子中增加一个方法来得到一个所有记录行的List:

    public List getList() {
        jt = new JdbcTemplate(dataSource);
        List rows = jt.queryForList("select * from mytable");
        return rows;
    }

返回的List会以下面的形式: [{name=Bob, id=1}, {name=Mary, id=2}].

10.2.6. 更新数据库

还有很多更新的方法可以供你使用。我将展示一个例子,说明通过某一个主键更新一个字段。 在这个例子里,我用了一个使用榜定参数的SQL Statement。大多数查询和更新的方法都有这个功能。 参数值通过对象数组传递。

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAnUpdate {
    private JdbcTemplate jt;
    private DataSource dataSource;

    public void setName(int id, String name) {
        jt = new JdbcTemplate(dataSource);
        jt.update("update mytable set name = ? where id = ?", new Object[] {name, new Integer(id)});
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

10.3. 控制如何连接数据库

10.3.1. DataSourceUtils

这个辅助类提供从JNDI获取连接和在必要时关闭连接的方法。它支持线程绑定的连接, 比如被用于DataSourceTransactionManager。

注意:方法getDataSourceFromJndi用以针对那些不使用BeanFactory或者ApplicationContext 的应用。对于后者,建议在factory中配置你的bean甚至JdbcTemplate的引用: 使用JndiObjectFactoryBean可以从JNDI中得到一个DataSource 并将DataSource的引用给别的bean。要切换到另一个DataSource 就仅仅是一个配置的问题:你甚至可以用一个非JNDI的DataSource来替换 FactoryBean的定义!

10.3.2. SmartDataSource

实现这个接口的类可以提供一个关系数据库的连接。 它继承javax.sql.DataSource接口,使用它可以知道在一次数据库操作后, 是否需要关闭连接。如果我们需要重用一个连接,那么它对于效率是很有用的。

10.3.3. AbstractDataSource

这个实现Spring的DataSource的抽象类,关注一些"无趣"的东西。 如果你要写自己的DataSource实现,你可以继承这个类。

10.3.4. SingleConnectionDataSource

这个SmartDataSource的实现封装了单个在使用后不会关闭的连接。 所以很明显,它没有多线程的能力。

如果客户端代码想关闭这个认为是池管理的连接,比如使用持久化工具的时候, 需要将suppressClose设置成true。这样会返回一个禁止关闭的代理来接管物理连接。 需要注意的是,你将无法将不再能将这个连接转换成本地Oracle连接或者类似的连接。

它的主要作用是用来测试。例如,它可以很容易的让测试代码脱离应用服务器测试,而只需要一个简易的JNDI环境。 和DriverManagerDataSource相反,它在所有的时候都重用一个连接, 以此来避免建立物理连接过多的消耗。

10.3.5. DriverManagerDataSource

这个SmartDataSource的实现通过bean的属性配置JDBC驱动, 并每次都返回一个新的连接。

它对于测试或者脱离J2EE容器的独立环境都是有用的, 它可以作为不同的ApplicationContext中的数据源bean, 也可以和简易的JNDI环境一起工作。被认为是池管理的Connection.close()操作 的调用只会简单的关闭连接,所以任何使用数据源的持久化代码都可以工作。

10.3.6. DataSourceTransactionManager

这个PlatformTransactionManager的实现是对于单个JDBC数据源的。 从某个数据源绑定一个JDBC连接到一个线程,可能允许每个数据源一个线程连接。

应用程序的代码需要通过DataSourceUtils.getConnection(DataSource)取得JDBC连接代替 J2EE标准的方法DataSource.getConnection。这是推荐的方法, 因为它会抛出org.springframework.dao中的unchecked的异常代替SQLException。 Framework中所有的类都使用这种方法,比如JdbcTemplate。 如果不使用事务管理,那么就会使用标准的方法,这样他就可以在任何情况下使用。

支持自定义的隔离级,以及应用于适当的JDBC statement查询的超时。 要支持后者,应用程序代码必须使用JdbcTemplate或者对每一个创建的statement 都调用DataSourceUtils.applyTransactionTimeout

因为它不需要容器支持JTA,在只有单个资源的情况下, 这个实现可以代替JtaTransactionManager。 如果你坚持需要的连接的查找模式,两者间的切换只需要更换配置。 不过需要注意JTA不支持隔离级。

10.4. JDBC操作的Java对象化

org.springframework.jdbc.object包由一些允许你 以更面向对象的方式访问数据库的类组成。你可以执行查询并获得一个包含业务对象的List, 这些业务对象关系数据的字段值映射成它们的属性。你也可以执行存储过程,更新,删除和插入操作。

10.4.1. SqlQuery

这是一个表示SQL查询的可重用的而且线程安全的对象。 子类必须实现newResultReader()方法来提供一个对象,它能在循环处理ResultSet的时候保存结果。 这个类很少被直接使用,而使用它的子类MappingSqlQuery,它提供多得多的方法 将数据行映射到Java类。MappingSqlQueryWithParametersUpdatableSqlQuery是继承SqlQuery的另外两个实现。

10.4.2. MappingSqlQuery

MappingSqlQuery是一个可以重用的查询对象, 它的子类必须实现抽象方法mapRow(ResultSet, int)来把JDBC ResultSet的每一行转换成对象。

在所有的SqlQuery实现中,这个类是最常使用并且也是最容易使用的。

下面是一个自定义查询的简单例子,它把customer表中的数据映射成叫做Customer的Java类。

  private class CustomerMappingQuery extends MappingSqlQuery {
    public CustomerMappingQuery(DataSource ds) {
      super(ds, "SELECT id, name FROM customer WHERE id = ?");
      super.declareParameter(new SqlParameter("id", Types.INTEGER));
      compile();
    }
    public Object mapRow(ResultSet rs, int rowNumber) throws SQLException {
      Customer cust = new Customer();
      cust.setId((Integer) rs.getObject("id"));
      cust.setName(rs.getString("name"));
      return cust;
    } 
  }

我们为customer查询提供一个构建方法,它只有数据源这一个参数。 在构建方法中,我们调用超类的构建方法,并将数据源和将要用来查询取得数据的SQL作为参数。 因为这个SQL将被用来建立PreparedStatement,所以它可以包含?来绑定执行时会得到的参数。 每一个参数必须通过declareParameter方法并传递给它一个SqlParameter来声明。 SqlParameter有一个名字和一个在java.sql.Types定义的JDBC类型。 在所有的参数都定义完后,我们调用compile方法建立随后会执行的PreparedStatement

我们来看一段代码,来实例化这个自定义查询对象并执行:

    public Customer getCustomer(Integer id) {
        CustomerMappingQuery custQry = new CustomerMappingQuery(dataSource); 
        Object[] parms = new Object[1];
        parms[0] = id;
        List customers = custQry.execute(parms);
        if (customers.size() > 0)
            return (Customer) customers.get(0);
        else
            return null;
    }

在例子中的这个方法通过一个参数id得到customer。在建立了CustomerMappingQuery 类的一个实例后,我们再创建一个数组,用来放置所有需要传递的参数。 在这个例子中只有一个Integer的参数需要传递。 现在我们使用这个数组执行查询,我们会得到一个List包含着Customer对象, 它对应查询返回结果的每一行。在这个例子中如果有匹配的话,只会有一个实体。

10.4.3. SqlUpdate

这个RdbmsOperation子类表示一个SQL更新操作。就像查询一样, 更新对象是可重用的。和所有的RdbmsOperation对象一样,更新可以有参数并定义在SQL中。

类似于查询对象中的execute()方法,这个类提供很多update()的方法。

这个类是具体的。通过SQL设定和参数声明,它可以很容易的参数化,虽然他也可以子例化 (例如增加自定义方法)。

import java.sql.Types;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating  extends SqlUpdate {
    public UpdateCreditRating(DataSource ds) {
        setDataSource(ds);
        setSql("update customer set credit_rating = ? where id = ?");
        declareParameter(new SqlParameter(Types.NUMERIC));
        declareParameter(new SqlParameter(Types.NUMERIC));
        compile();
    }

    /**
     * @param id for the Customer to be updated
     * @param rating the new value for credit rating
     * @return number of rows updated
     */
    public int run(int id, int rating) {
        Object[] params =
            new Object[] {
                new Integer(rating),
                new Integer(id)};
        return update(params);
    }
}

10.4.4. StoredProcedure

这是RDBMS存储过程的对象抽象的超类。它是一个抽象类,它的执行方法都是protected的, 以避免被直接调用,而只能通过提供更严格形式的子类调用。

继承的sql属性是RDBMS中存储过程的名字。虽然这个类中提供的其他功能在JDBC3.0中也十分的重要, 但最值得注意的是JDBC3.0中的使用命名的参数。

下面是一段例子程序,它调用Oracle数据库提供的函数sysdate()。 要使用存储过程的功能,你必须创建一个继承StoredProcedure的类. 这里没有任何输入参数,但需要使用SqlOutParameter类声明一个date型的输出参数。 execute()方法会返回一个使用名字作为key来映射每一个被声明的输出参数的实体的Map。

import java.sql.Types;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.datasource.*;
import org.springframework.jdbc.object.StoredProcedure;

public class TestSP {

    public static void main(String[] args)  {
        
        System.out.println("DB TestSP!");
        TestSP t = new TestSP();
        t.test();
        System.out.println("Done!");
        
    }
    
    void test() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        ds.setUrl("jdbc:oracle:thin:@localhost:1521:mydb");
        ds.setUsername("scott");
        ds.setPassword("tiger");

        MyStoredProcedure sproc = new MyStoredProcedure(ds);
        Map res = sproc.execute();
        printMap(res);
                
    }

    private class MyStoredProcedure extends StoredProcedure {
        public static final String SQL = "sysdate";

        public MyStoredProcedure(DataSource ds) {
            setDataSource(ds);
            setFunction(true);
            setSql(SQL);
            declareParameter(new SqlOutParameter("date", Types.DATE));
            compile();
        }

        public Map execute() {
            Map out = execute(new HashMap());
            return out;
        }

    }

    private static void printMap(Map r) {
        Iterator i = r.entrySet().iterator();
        while (i.hasNext()) {
            System.out.println((String) i.next().toString());  
        }
    }
}

10.4.5. SqlFunction

SQL "function"封装返回单一行结果的查询。默认的情况返回一个int,当然我们可以重载它, 通过额外返回参数得到其他类型。这和使用JdbcTemplatequeryForXxx方法很相似。使用SqlFunction的好处是 不用必须在后台建立JdbcTemplate

这个类的目的是调用SQL function,使用像"select user()"或者"select sysdate from dual" 得到单一的结果。它不是用来调用复杂的存储功能也不是用来使用CallableStatement 来调用存储过程或者存储功能。对于这类的处理应当使用StoredProcedure或者SqlCall

这是一个具体的类,它通常不需要子类化。使用这个包的代码可以通过声明SQL和参数创建这个类型的对象, 然后能重复的使用run方法执行这个function。下面是一个得到一张表的行数的例子:

    public int countRows() {
        SqlFunction sf = new SqlFunction(dataSource, "select count(*) from mytable");
        sf.compile();
        return sf.run();
    }