跳到主要内容

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

ChatGPT-4o 中英对照 Using the JDBC Core Classes to Control Basic JDBC Processing and Error Handling

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

使用 JdbcTemplate

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

  • 运行 SQL 查询

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

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

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

当你在代码中使用 JdbcTemplate 时,你只需要实现回调接口,并为它们提供一个明确的契约。由 JdbcTemplate 类提供的 ConnectionPreparedStatementCreator 回调接口创建一个预处理语句,提供 SQL 和任何必要的参数。对于创建可调用语句的 CallableStatementCreator 接口也是如此。RowCallbackHandler 接口从 ResultSet 的每一行中提取值。

您可以在 DAO 实现中通过直接实例化并传入 DataSource 引用来使用 JdbcTemplate,或者您可以在 Spring IoC 容器中配置它,并将其作为 bean 引用提供给 DAOs。

备注

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

此类发出的所有 SQL 都会在 DEBUG 级别下记录到与模板实例的完全限定类名相对应的类别中(通常是 JdbcTemplate,但如果您使用 JdbcTemplate 类的自定义子类,则可能会有所不同)。

以下部分提供了一些 JdbcTemplate 使用的示例。这些示例并不是 JdbcTemplate 所提供的所有功能的详尽列表。有关详细信息,请参阅相关的 javadoc

查询(SELECT

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

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

以下查询使用了一个绑定变量:

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

以下查询查找一个 String

String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
java

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

Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
},
1212L);
java

以下查询查找并填充域对象列表:

List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
java

如果最后两个代码片段实际上存在于同一个应用程序中,那么移除两个 RowMapper lambda 表达式中存在的重复部分,并将它们提取到一个可以被 DAO 方法根据需要引用的单一字段中是有意义的。例如,可能更好地将前面的代码片段写成如下形式:

private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};

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

使用 JdbcTemplate 进行更新(INSERTUPDATEDELETE

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

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

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

以下示例更新了一个现有条目:

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

以下示例删除一个条目:

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

其他 JdbcTemplate 操作

您可以使用 execute(..) 方法运行任意的 SQL。因此,该方法通常用于 DDL 语句。它有许多重载变体,这些变体接受回调接口、绑定变量数组等。以下示例创建一个表:

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

下面的示例调用了一个存储过程:

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

更复杂的存储过程支持在后面进行介绍。

JdbcTemplate 最佳实践

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

在使用 JdbcTemplate 类(以及相关的 NamedParameterJdbcTemplate 类)时,一个常见的做法是在 Spring 配置文件中配置一个 DataSource,然后将该共享的 DataSource bean 依赖注入到你的 DAO 类中。JdbcTemplate 是在 DataSource 的 setter 方法或构造函数中创建的。这导致 DAO 类看起来如下所示:

public class JdbcCorporateEventDao implements CorporateEventDao {

private final JdbcTemplate jdbcTemplate;

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

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

以下示例显示了相应的配置:

@Bean
JdbcCorporateEventDao corporateEventDao(DataSource dataSource) {
return new JdbcCorporateEventDao(dataSource);
}

@Bean(destroyMethod = "close")
BasicDataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
java

显式配置的另一种替代方法是使用组件扫描和注解支持进行依赖注入。在这种情况下,你可以使用 @Repository 注解类(这使其成为组件扫描的候选对象)。以下示例展示了如何实现:

@Repository
public class JdbcCorporateEventRepository implements CorporateEventRepository {

private JdbcTemplate jdbcTemplate;

// Implicitly autowire the DataSource constructor parameter
public JdbcCorporateEventRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

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

以下示例显示了相应的配置:

@Configuration
@ComponentScan("org.example.jdbc")
public class JdbcCorporateEventRepositoryConfiguration {

@Bean(destroyMethod = "close")
BasicDataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}

}
java

如果你使用 Spring 的 JdbcDaoSupport 类,并且你的各种基于 JDBC 的 DAO 类继承自它,那么你的子类将从 JdbcDaoSupport 类继承一个 setDataSource(..) 方法。你可以选择是否从这个类继承。JdbcDaoSupport 类仅作为一种便利提供。

无论您选择使用上述哪种模板初始化样式(或不使用),通常都不需要每次运行 SQL 时创建一个新的 JdbcTemplate 类实例。配置完成后,一个 JdbcTemplate 实例是线程安全的。如果您的应用程序访问多个数据库,您可能需要多个 JdbcTemplate 实例,这需要多个 DataSource,进而需要多个不同配置的 JdbcTemplate 实例。

使用 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate 类通过使用命名参数来编写 JDBC 语句,而不是仅使用经典的占位符('?')参数来编写 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);
}
java

注意在赋值给 sql 变量的值中使用了命名参数符号,以及插入到 namedParameters 变量(类型为 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);
}
java

NamedParameterJdbcTemplate 相关的一个不错的功能(并且存在于同一个 Java 包中)是 SqlParameterSource 接口。你已经在之前的代码片段中看到过这个接口的一个实现示例(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...
}
java

下面的示例使用 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);
}
java

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

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

统一 JDBC 查询/更新操作:JdbcClient

从 6.1 版本开始,NamedParameterJdbcTemplate 的命名参数语句和常规 JdbcTemplate 的位置参数语句可以通过一个具有流畅交互模型的统一客户端 API 使用。

例如,使用位置参数:

private JdbcClient jdbcClient = JdbcClient.create(dataSource);

public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?")
.param(firstName)
.query(Integer.class).single();
}
java

例如,使用命名参数:

private JdbcClient jdbcClient = JdbcClient.create(dataSource);

public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName")
.param("firstName", firstName)
.query(Integer.class).single();
}
java

RowMapper 功能同样可用,具有灵活的结果解析:

List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name")))
.list();
java

除了自定义 RowMapper 之外,您还可以指定一个类进行映射。例如,假设 Actor 作为一个记录类具有 firstNamelastName 属性、自定义构造函数、bean 属性或普通字段:

List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query(Actor.class)
.list();
java

需要一个单一对象结果:

Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.single();
java

对于 java.util.Optional 结果:

Optional<Actor> actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.optional();
java

对于更新语句:

this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)")
.param("Leonor").param("Watling")
.update();
java

或者一个带有命名参数的更新语句:

this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.param("firstName", "Leonor").param("lastName", "Watling")
.update();
java

除了单独命名的参数之外,您还可以指定一个参数源对象——例如,记录类、具有 bean 属性的类,或提供 firstNamelastName 属性的普通字段持有者,例如上面的 Actor 类:

this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.paramSource(new Actor("Leonor", "Watling")
.update();
java

自动 Actor 类的参数映射以及上面的查询结果是通过隐式的 SimplePropertySqlParameterSourceSimplePropertyRowMapper 策略提供的,这些策略也可以直接使用。它们可以作为 BeanPropertySqlParameterSourceBeanPropertyRowMapper/DataClassRowMapper 的通用替代方案,也可以与 JdbcTemplateNamedParameterJdbcTemplate 本身一起使用。

备注

JdbcClient 是一个灵活但简化的 JDBC 查询/更新语句的外观。批量插入和存储过程调用等高级功能通常需要额外的自定义:对于 JdbcClient 中不可用的任何此类功能,请考虑使用 Spring 的 SimpleJdbcInsertSimpleJdbcCall 类或直接使用 JdbcTemplate

使用 SQLExceptionTranslator

SQLExceptionTranslator 是一个接口,由可以在 SQLException 和 Spring 自己的 org.springframework.dao.DataAccessException 之间进行转换的类实现,该接口与数据访问策略无关。实现可以是通用的(例如,使用 JDBC 的 SQLState 代码)或专有的(例如,使用 Oracle 错误代码)以获得更高的精确度。这个异常转换机制在常用的 JdbcTemplateJdbcTransactionManager 入口点后面使用,它们不会传播 SQLException,而是传播 DataAccessException

备注

从 6.0 版本开始,默认的异常翻译器是 SQLExceptionSubclassTranslator,它通过一些额外的检查检测 JDBC 4 SQLException 子类,并通过 SQLStateSQLExceptionTranslator 回退到 SQLState 内省。这通常足以满足常见的数据库访问需求,并且不需要特定于供应商的检测。为了向后兼容,可以考虑使用下面描述的 SQLErrorCodeSQLExceptionTranslator,可能需要自定义错误代码映射。

SQLErrorCodeSQLExceptionTranslatorSQLExceptionTranslator 的实现,当类路径的根目录中存在名为 sql-error-codes.xml 的文件时,默认使用该实现。此实现使用特定的供应商代码,比 SQLStateSQLException 子类翻译更为精确。错误代码翻译基于一个名为 SQLErrorCodes 的 JavaBean 类型类中的代码。这个类由 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;
}
}
java

在前面的示例中,特定的错误代码(-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);
}
java

自定义翻译器被传递一个数据源,以便在 sql-error-codes.xml 中查找错误代码。

执行语句

运行 SQL 语句所需的代码非常少。你需要一个 DataSource 和一个 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))");
}
}
java

执行查询

一些查询方法返回单个值。要从一行中检索计数或特定值,请使用 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);
}
}
java

除了单结果查询方法之外,还有几种方法返回一个列表,其中包含查询返回的每一行的条目。最通用的方法是 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");
}
java

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

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

更新数据库

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

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

在上面的例子中,SQL 语句包含行参数的占位符。你可以将参数值作为可变参数传入,或者作为对象数组传入。因此,你应该显式地将原始类型包装在原始类型包装类中,或者使用自动装箱。

检索自动生成的键

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(connection -> {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
ps.setString(1, name);
return ps;
}, keyHolder);

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