3.3使用JDBC核心类控制基本JDBC处理和错误处理

本节介绍如何使用JDBC核心类来控制基本的JDBC处理,包括错误处理。它包括以下主题:

  • 使用JdbcTemplate

  • 使用NamedParameterJdbcTemplate

  • 使用SQLExceptionTranslator

  • 运行声明

  • 运行查询

  • 更新数据库

  • 检索自动生成的密钥

3.3.1 使用JdbcTemplate

JdbcTemplate是JDBC核心软件包中的中心类。它处理资源的创建和释放,这有助于你避免常见的错误,例如忘记关闭连接。它执行核心JDBC工作流程的基本任务(例如,语句创建和执行),而使应用程序代码提供SQL并提取结果。 JdbcTemplate类:

  • 运行SQL查询

  • 更新语句和存储过程调用

  • 对ResultSet实例执行迭代并提取返回的参数值。

  • 捕获JDBC异常并将其转换为org.springframework.dao包中定义的通用,信息量更大的异常层次结构。 (请参见一致的异常层次结构。)

当将JdbcTemplate用于代码时,只需实现回调接口,即可为它们提供明确定义的协定。给定JdbcTemplate类提供的Connection,PreparedStatementCreator回调接口创建一个准备好的语句,提供SQL和任何必要的参数。创建可调用语句的CallableStatementCreator接口也是如此。 RowCallbackHandler接口从ResultSet的每一行提取值。

你可以通过直接实例化DataSource引用在DAO实现中使用JdbcTemplate,也可以在Spring IoC容器中对其进行配置,并将其作为Bean引用提供给DAO。

应该始终在Spring IoC容器中将DataSource配置为Bean。在第一种情况下,bean被直接提供给服务。在第二种情况下,将其提供给准备好的模板。

此类发出的所有SQL都在DEBUG级别下记录,该类别对应于模板实例的完全合格的类名称(通常为JdbcTemplate,但是如果使用JdbcTemplate类的自定义子类,则可能有所不同)。

以下各节提供了一些JdbcTemplate用法的示例。这些示例不是JdbcTemplate公开的所有功能的详尽列表。请参阅附带的javadoc。

查询(SELECT)

以下查询获取关系中的行数:

int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

以下查询使用绑定变量:

int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

以下查询查找字符串:

String lastName = this.jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        new Object[]{1212L}, String.class);

以下查询查找并填充单个域对象:

Actor actor = this.jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        new Object[]{1212L},
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

以下查询查找并填充许多域对象:

List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

如果最后两个代码片段实际存在于同一个应用程序中,那么删除两个RowMapper匿名内部类中存在的重复并将它们提取到一个可以引用的单个类(通常是静态嵌套类)中是有意义的。 根据需要通过DAO方法。 例如,最好编写前面的代码片段,如下所示:

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}

private static final class ActorMapper implements RowMapper<Actor> {

    public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
        Actor actor = new Actor();
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }
}

Updating (INSERT, UPDATE, and DELETE) with JdbcTemplate

你可以使用update(..)方法执行插入,更新和删除操作。 参数值通常作为变量参数提供,或者作为对象数组提供。

以下示例插入一个新条目:

this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");

以下示例更新现有条目:

this.jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L);

以下示例删除现有条目:

this.jdbcTemplate.update(
        "delete from actor where id = ?",
        Long.valueOf(actorId));

其他JdbcTemplate操作

你可以使用execute(..)方法来运行任何任意SQL。 因此,该方法通常用于DDL语句。 带有回调接口,绑定变量数组等的变体极大地超载了它。 以下示例创建一个表:

this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

下面调用一个存储过程:

this.jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        Long.valueOf(unionId));

JdbcTemplate最佳实践

一旦配置,JdbcTemplate类的实例是线程安全的。 这很重要,因为这意味着你可以配置JdbcTemplate的单个实例,然后安全地将此共享引用注入到多个DAO(或存储库)中。 JdbcTemplate是有状态的,因为它维护对DataSource的引用,但是此状态不是会话状态。

使用JdbcTemplate类(和关联的NamedParameterJdbcTemplate类)的常见做法是在Spring配置文件中配置DataSource,然后将共享的DataSource bean依赖注入到DAO类中。 在数据源的设置器中创建了JdbcTemplate。 这导致类似于以下内容的DAO:

public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

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

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}

下面是相应的XML文件:

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

显式配置的替代方法是使用组件扫描和注解支持进行依赖项注入。 在这种情况下,可以使用@Repository注解该类(这使其成为组件扫描的候选对象),并使用@Autowired注解DataSource setter方法。 以下示例显示了如何执行此操作:

@Repository 
public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

    @Autowired 
    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
    }

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}

下面是相应的XML文件:

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- Scans within the base package of the application for @Component classes to configure as beans -->
    <context:component-scan base-package="org.springframework.docs.test" />

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

如果你使用Spring的JdbcDaoSupport类,并且从中扩展了各种JDBC支持的DAO类,则你的子类将从JdbcDaoSupport类继承一个setDataSource(..)方法。 你可以选择是否从此类继承。 提供JdbcDaoSupport类只是为了方便。

不管你选择使用(或不使用)以上哪种模板初始化样式,都不需要在每次要运行SQL时都创建JdbcTemplate类的新实例。 配置完成后,JdbcTemplate实例是线程安全的。 如果你的应用程序访问多个数据库,则可能需要多个JdbcTemplate实例,这需要多个DataSources,以及随后的多个不同配置的JdbcTemplate实例。

3.3.2 使用NamedParameterJdbcTemplate

与仅使用经典占位符('?')编程的JDBC语句相反,NamedParameterJdbcTemplate类增加了使用命名参数对JDBC语句进行编程的支持。 NamedParameterJdbcTemplate类包装JdbcTemplate并委托给包装的JdbcTemplate完成其许多工作。 本节仅描述NamedParameterJdbcTemplate类的与JdbcTemplate本身不同的那些区域,即使用命名参数对JDBC语句进行编程。 下面的示例演示如何使用NamedParameterJdbcTemplate:

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

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

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

请注意,在分配给sql变量的值和插入namednames变量(MapSqlParameterSource类型)的相应值中使用了命名参数符号。

另外,你也可以使用基于Map的样式将命名参数及其对应的值传递给NamedParameterJdbcTemplate实例.NamedParameterJdbcOperations公开和NamedParameterJdbcTemplate类实现的其余方法遵循类似的模式,此处不再赘述。

以下示例说明了基于Map的样式的使用:

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

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

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
}

SqlParameterSource接口是与NamedParameterJdbcTemplate相关的一个不错的功能(并且存在于同一Java包中)。 你已经在前面的代码片段之一(MapSqlParameterSource类)中看到了此接口的实现示例。 SqlParameterSource是NamedParameterJdbcTemplate的命名参数值的源。 MapSqlParameterSource类是一个简单的实现,它是围绕java.util.Map的适配器,其中键是参数名称,值是参数值。

另一个SqlParameterSource实现是BeanPropertySqlParameterSource类。 此类包装任意JavaBean(即,遵循JavaBean约定的类的实例),并将包装的JavaBean的属性用作命名参数值的源。

以下示例显示了典型的JavaBean:

public class Actor {

    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public Long getId() {
        return this.id;
    }

    // setters omitted...

}

以下示例使用NamedParameterJdbcTemplate返回上一示例中显示的类的成员数:

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

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

public int countOfActors(Actor exampleActor) {

    // notice how the named parameters match the properties of the above 'Actor' class
    String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

    SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

请记住,NamedParameterJdbcTemplate类包装了经典的JdbcTemplate模板。 如果需要访问包装的JdbcTemplate实例以访问仅在JdbcTemplate类中提供的功能,则可以使用getJdbcOperations()方法通过JdbcOperations接口访问包装的JdbcTemplate。

另请参见JdbcTemplate最佳实践,以获取有关在应用程序上下文中使用NamedParameterJdbcTemplate类的指导。

3.3.3 使用SQLExceptionTranslator

SQLExceptionTranslator是要由可在SQLExceptions和Spring自己的org.springframework.dao.DataAccessException之间进行转换的类实现的接口,该类与数据访问策略无关。为了提高精度,实现可以是通用的(例如,使用SQLState代码用于JDBC)或专有的(例如,使用Oracle错误代码)。

SQLErrorCodeSQLExceptionTranslator是默认使用的SQLExceptionTranslator的实现。此实现使用特定的供应商代码。它比SQLState实现更精确。错误代码转换基于JavaBean类型类(称为SQLErrorCodes)中保存的代码。此类是由SQLErrorCodesFactory创建和填充的,顾名思义,该类是一个工厂,用于根据名为sql-error-codes.xml的配置文件的内容创建SQLErrorCodes。此文件使用供应商代码填充,并基于从DatabaseMetaData中获取的DatabaseProductName填充。使用你正在使用的实际数据库的代码。

SQLErrorCodeSQLExceptionTranslator按以下顺序应用匹配规则:

  1. 子类实现的任何自定义转换。通常,将使用提供的具体SQLErrorCodeSQLExceptionTranslator,因此此规则不适用。仅当你确实提供了子类实现时,它才适用。

  2. 作为SQLErrorCodes类的customSqlExceptionTranslator属性提供的SQLExceptionTranslator接口的任何自定义实现。

  3. 搜索CustomSQLErrorCodesTranslation类的实例列表(为SQLErrorCodes类的customTranslations属性提供),以查找匹配项。

  4. 错误代码匹配被应用。

  5. 使用后备翻译器。 SQLExceptionSubclassTranslator是默认的后备转换器。如果此翻译不可用,则下一个后备翻译器是SQLStateSQLExceptionTranslator。

默认情况下,使用SQLErrorCodesFactory定义错误代码和自定义异常转换。 从类路径的名为sql-error-codes.xml的文件中查找它们,并根据使用中数据库的数据库元数据中的数据库名称找到匹配的SQLErrorCodes实例。

你可以扩展SQLErrorCodeSQLExceptionTranslator,如以下示例所示:

public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {

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

在前面的示例中,特定的错误代码(-12345)被转换,而其他错误则由默认转换器实现转换。 要使用此自定义转换器,必须通过setExceptionTranslator方法将其传递给JdbcTemplate,并且必须在需要此转换器的所有数据访问处理中使用此JdbcTemplate。 以下示例显示了如何使用此自定义转换器:

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

    // create a JdbcTemplate and set data source
    this.jdbcTemplate = new JdbcTemplate();
    this.jdbcTemplate.setDataSource(dataSource);

    // create a custom translator and set the DataSource for the default translation lookup
    CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
    tr.setDataSource(dataSource);
    this.jdbcTemplate.setExceptionTranslator(tr);

}

public void updateShippingCharge(long orderId, long pct) {
    // use the prepared JdbcTemplate for this update
    this.jdbcTemplate.update("update orders" +
        " set shipping_charge = shipping_charge * ? / 100" +
        " where id = ?", pct, orderId);
}

定制转换器会传递一个数据源,以便在sql-error-codes.xml中查找错误代码。

3.3.4 运行声明

运行SQL语句需要很少的代码。 你需要一个数据源和一个JdbcTemplate,包括JdbcTemplate随附的便捷方法。 下面的示例显示了创建一个新表的最小但功能齐全的类需要包含的内容:

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

public class ExecuteAStatement {

    private JdbcTemplate jdbcTemplate;

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

    public void doExecute() {
        this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
    }
}

3.3.5 运行查询

一些查询方法返回单个值。 要从一行中检索计数或特定值,请使用queryForObject(..)。 后者将返回的JDBC Type转换为作为参数传递的Java类。 如果类型转换无效,则抛出InvalidDataAccessApiUsageException。 以下示例包含两种查询方法,一种用于int,另一种用于查询String:

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

public class RunAQuery {

    private JdbcTemplate jdbcTemplate;

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

    public int getCount() {
        return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
    }

    public String getName() {
        return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
    }
}

除了单个结果查询方法外,还有几种方法返回一个列表,其中包含查询返回的每一行的条目。 最通用的方法是queryForList(..),该方法返回一个List,其中每个元素都是一个Map,使用列名作为键,每个列包含一个条目。 如果在前面的示例中添加一种方法来检索所有行的列表,则可能如下所示:

private JdbcTemplate jdbcTemplate;

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

public List<Map<String, Object>> getList() {
    return this.jdbcTemplate.queryForList("select * from mytable");
}

返回的列表类似于以下内容:

[{name=Bob, id=1}, {name=Mary, id=2}]

3.3.6 更新数据库

下面的示例更新某个主键的列:

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

public class ExecuteAnUpdate {

    private JdbcTemplate jdbcTemplate;

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

    public void setName(int id, String name) {
        this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
    }
}

在前面的示例中,SQL语句具有用于行参数的占位符。 你可以将参数值作为varargs或作为对象数组传递。 因此,你应该在原语包装器类中显式包装原语,或者应该使用自动装箱。

3.3.7 检索自动生成的密钥

update()便捷方法支持检索由数据库生成的主键。 此支持是JDBC 3.0标准的一部分。 有关详细信息,请参见规范的第13.6章。 该方法将PreparedStatementCreator作为其第一个参数,这是指定所需插入语句的方式。 另一个参数是KeyHolder,它包含从更新成功返回时生成的密钥。 没有标准的单一方法来创建适当的PreparedStatement(这说明了为什么方法签名就是这样)。 以下示例在Oracle上有效,但在其他平台上可能不适用:

final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";

KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
    new PreparedStatementCreator() {
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
            PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"});
            ps.setString(1, name);
            return ps;
        }
    },
    keyHolder);

// keyHolder.getKey() now contains the generated key

最后更新于