Skip to content

使用 JDBC 核心类控制基本的 JDBC 处理和错误处理 (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 类提供 Connection 的情况下,PreparedStatementCreator 回调接口创建一个预编译语句(prepared statement),提供 SQL 和任何必要的参数。CallableStatementCreator 接口也是如此,它用于创建可调用语句。RowCallbackHandler 接口则从 ResultSet 的每一行中提取值。

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

注意

DataSource 应该始终在 Spring IoC 容器中配置为 Bean。如果是第一种情况,Bean 会直接提供给服务;如果是第二种情况,Bean 也会被提供给已准备好的模板。

该类发出的所有 SQL 都会在对应于模板实例的全限定类名(通常是 JdbcTemplate)的类别下以 DEBUG 级别记录日志。

查询 (SELECT)

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

java
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
kotlin
val rowCount = jdbcTemplate.queryForObject<Int>("select count(*) from t_actor")!!

以下查询使用绑定变量:

java
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
		"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
kotlin
val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
		"select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!

以下查询查找 String

java
String lastName = this.jdbcTemplate.queryForObject(
		"select last_name from t_actor where id = ?",
		String.class, 1212L);
kotlin
val lastName = this.jdbcTemplate.queryForObject<String>(
		"select last_name from t_actor where id = ?",
		arrayOf(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);
kotlin
val actor = jdbcTemplate.queryForObject(
			"select first_name, last_name from t_actor where id = ?",
			arrayOf(1212L)) { rs, _ ->
		Actor(rs.getString("first_name"), rs.getString("last_name"))
	}

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

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;
		});
kotlin
val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ ->
		Actor(rs.getString("first_name"), rs.getString("last_name"))
}

如果上述最后两个代码片段确实存在于同一个应用程序中,那么移除两个 RowMapper lambda 表达式中的重复代码,并将其提取到单个字段中,然后在需要时由 DAO 方法引用,这样做是有意义的。例如,前面的代码片段最好改写如下:

java
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);
}
kotlin
val actorMapper = RowMapper<Actor> { rs: ResultSet, rowNum: Int ->
	Actor(rs.getString("first_name"), rs.getString("last_name"))
}

fun findAllActors(): List<Actor> {
	return jdbcTemplate.query("select first_name, last_name from t_actor", actorMapper)
}

更新 (INSERT, UPDATE, 和 DELETE)

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

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

java
this.jdbcTemplate.update(
		"insert into t_actor (first_name, last_name) values (?, ?)",
		"Leonor", "Watling");
kotlin
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);
kotlin
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));
kotlin
jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong())

其他 JdbcTemplate 操作

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

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

以下示例调用存储过程:

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

JdbcTemplate 最佳实践

一旦配置完成,JdbcTemplate 类的实例就是线程安全的。这非常重要,因为这意味着你可以配置一个单一的 JdbcTemplate 实例,然后安全地将这个共享引用注入到多个 DAO(或 repository)中。JdbcTemplate 是有状态的,因为它维护着对 DataSource 的引用,但这个状态并不是对话式状态(conversational state)。

使用 JdbcTemplate 类(以及相关的 NamedParameterJdbcTemplate 类)时的常见做法是在 Spring 配置文件中配置 DataSource,然后将该共享的 DataSource Bean 依赖注入到你的 DAO 类中。JdbcTemplate 是在 DataSource 的 setter 方法或构造函数中创建的。这导致 DAO 类似于以下形式:

java
public class JdbcCorporateEventDao implements CorporateEventDao {

	private final JdbcTemplate jdbcTemplate;

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

	// 接着是基于 JDBC 实现的 CorporateEventDao 方法...
}
kotlin
class JdbcCorporateEventDao(dataSource: DataSource): CorporateEventDao {

	private val jdbcTemplate = JdbcTemplate(dataSource)

	// 接着是基于 JDBC 实现的 CorporateEventDao 方法...
}

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

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;
}
kotlin
@Bean
fun corporateEventDao(dataSource: DataSource) = JdbcCorporateEventDao(dataSource)

@Bean(destroyMethod = "close")
fun dataSource() = BasicDataSource().apply {
	driverClassName = "org.hsqldb.jdbcDriver"
	url = "jdbc:hsqldb:hsql://localhost:"
	username = "sa"
	password = ""
}
xml
<bean id="corporateEventDao" class="org.example.jdbc.JdbcCorporateEventDao">
	<constructor-arg ref="dataSource"/>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp2.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"/>

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

java
@Repository
public class JdbcCorporateEventRepository implements CorporateEventRepository {

	private JdbcTemplate jdbcTemplate;

	// 隐式自动装配 DataSource 构造函数参数
	public JdbcCorporateEventRepository(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	// 接着是基于 JDBC 实现的 CorporateEventRepository 方法...
}

使用 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate 类增加了对使用命名参数编写 JDBC 语句的支持,而不仅仅是使用传统的占位符('?')参数。NamedParameterJdbcTemplate 类包装了一个 JdbcTemplate,并委托包裹的 JdbcTemplate 完成大部分工作。本节仅描述 NamedParameterJdbcTemplate 类中与 JdbcTemplate 本身不同的部分——即使用命名参数编写 JDBC 语句。以下示例展示了如何使用 NamedParameterJdbcTemplate

java
// 某个基于 JDBC 的 DAO 类...
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);
}
kotlin
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActorsByFirstName(firstName: String): Int {
	val sql = "select count(*) from t_actor where first_name = :first_name"
	val namedParameters = MapSqlParameterSource("first_name", firstName)
	return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

请注意在赋给 sql 变量的值中使用命名参数表示法,以及插入到 namedParameters 变量(类型为 MapSqlParameterSource)中的相应值。

或者,你可以使用基于 Map 的风格,将命名参数及其对应的值传递给 NamedParameterJdbcTemplate 实例。NamedParameterJdbcOperations 暴露并由 NamedParameterJdbcTemplate 类实现的其他方法遵循类似的模式。

以下示例展示了如何使用基于 Map 的风格:

java
// 某个基于 JDBC 的 DAO 类...
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);
}
kotlin
// 某个基于 JDBC 的 DAO 类...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActorsByFirstName(firstName: String): Int {
	val sql = "select count(*) from t_actor where first_name = :first_name"
	val namedParameters = mapOf("first_name" to firstName)
	return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

NamedParameterJdbcTemplate 相关的另一个好特性(且存在于同一个 Java 包中)是 SqlParameterSource 接口。在前一个代码片段中,你已经看到了该接口的一个实现示例(MapSqlParameterSource 类)。SqlParameterSourceNamedParameterJdbcTemplate 命名参数值的来源。MapSqlParameterSource 类是一个简单的实现,它是 java.util.Map 的适配器,其中键是参数名,值是参数值。

另一个 SqlParameterSource 实现是 BeanPropertySqlParameterSource 类。该类包装了一个任意的 JavaBean,并将包装的 JavaBean 的属性用作命名参数值的来源。

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

从 6.1 开始,NamedParameterJdbcTemplate 的命名参数语句和普通 JdbcTemplate 的位置参数语句都可以通过统一的客户端 API 使用,该 API 具有流畅(fluent)的交互模型。

例如,使用位置参数:

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

使用 SQLExceptionTranslator

SQLExceptionTranslator 是一个由能够将 SQLException 转换为 Spring 自己的 org.springframework.dao.DataAccessException 的类实现的接口。实现可以是通用的(例如,为 JDBC 使用 SQLState 代码)或专有的(例如,使用 Oracle 错误代码)以获得更高的精度。这种异常转换机制被通用的 JdbcTemplateJdbcTransactionManager 入口点所使用,它们不会抛出 SQLException,而是抛出 DataAccessException

注意

从 6.0 开始,默认的异常转换器是 SQLExceptionSubclassTranslator,它通过一些额外的检查来探测 JDBC 4 的 SQLException 子类,并回退到通过 SQLStateSQLExceptionTranslator 进行的 SQLState 内省。这通常足以应对常见的数据库访问,不需要特定厂商的检测。

运行语句

运行 SQL 语句只需要很少的代码。你需要一个 DataSource 和一个 JdbcTemplate。以下示例展示了创建一个新表所需的最小化但功能完整的类:

java
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))");
	}
}
kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class ExecuteAStatement(dataSource: DataSource) {

	private val jdbcTemplate = JdbcTemplate(dataSource)

	fun doExecute() {
		jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
	}
}

获取自动生成的主键

update() 便捷方法支持检索数据库生成的主键。这种支持是 JDBC 3.0 标准的一部分。

java
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() 现在包含生成的主键
kotlin
val INSERT_SQL = "insert into my_test (name) values(?)"
val name = "Rob"

val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({
	it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) }
}, keyHolder)

// keyHolder.getKey() 现在包含生成的主键

补充教学

1. 为什么推荐 JdbcClient

如果你正在使用 Spring Framework 6.1 或之后版本,JdbcClient 是绝大多数场景下的最佳选择。 它吸收了 JdbcTemplateNamedParameterJdbcTemplate 的优点,并提供了极致流畅的 API:

  • 链式调用:告别那种嵌套式的、臃肿的构造函数或方法重载。
  • 自动映射:它对 Java Record 类的支持非常友好,能自动将数据库字段映射到 Record 的构造参数上。
  • 统一体验:不管你想用位置参数(?)还是命名参数(:name),它都支持。

2. JdbcTemplate 是线程安全的吗?

是的。一旦 JdbcTemplate 被配置完成,它就是线程安全的。 这意味着你可以放心大胆地在多个 DAO 类中共享同一个 JdbcTemplate 实例。它的状态不涉及“会话级”的信息,所有的 SQL 执行逻辑都是通过局部变量和底层资源(如 DataSource 获取的连接)来实现的。

3. 注意:异常转译不是万能的

虽然 Spring 努力帮你隐藏 SQLException,但你仍然需要了解基本的 SQL 错误。 比如 DataIntegrityViolationException 只是告诉你数据完整性出了问题。到底是因为外键约束,还是非空约束,或者是唯一索引冲突?这通常需要你查看底层的堆栈信息,或者在异常处理时进行更细粒度的判断。 记住:Spring JDBC 只是帮你传递了错误,并进行了初步分类,最终的业务补救逻辑还是得看具体的报错原因。

Based on Spring Framework.