Skip to content

使用 SimpleJdbc 类简化 JDBC 操作 (Simplifying JDBC Operations with the SimpleJdbc Classes)

SimpleJdbcInsertSimpleJdbcCall 类通过利用可以从 JDBC 驱动程序检索到的数据库元数据,提供了简化的配置。这意味着你预先需要配置的内容更少,尽管如果你愿意在代码中提供所有细节,也可以覆盖或关闭元数据处理。

使用 SimpleJdbcInsert 插入数据

我们首先来看使用最少配置选项的 SimpleJdbcInsert 类。你应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。在这个例子中,初始化方法是 setDataSource 方法。你不需要继承 SimpleJdbcInsert 类;相反,你可以创建一个新实例并使用 withTableName 方法设置表名。该类的配置方法遵循流式(fluent)风格,返回 SimpleJdbcInsert 实例,允许你链式调用所有配置方法。

java
public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(3);
		parameters.put("id", actor.getId());
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		insertActor.execute(parameters);
	}

	// ... 其他方法
}
kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor")

	fun add(actor: Actor) {
		val parameters = mutableMapOf<String, Any>()
		parameters["id"] = actor.id
		parameters["first_name"] = actor.firstName
		parameters["last_name"] = actor.lastName
		insertActor.execute(parameters)
	}

	// ... 其他方法
}

这里使用的 execute 方法接受一个普通的 java.util.Map 作为其唯一参数。需要注意的一点是,用于 Map 的键必须与数据库中定义的表列名相匹配。这是因为我们通过读取元数据来构建实际的插入语句。

使用 SimpleJdbcInsert 获取自动生成的主键

接下来的示例使用了与前一个示例相同的插入方式,但它没有传入 id,而是检索自动生成的主键并将其设置在新的 Actor 对象上。在创建 SimpleJdbcInsert 时,除了指定表名外,还通过 usingGeneratedKeyColumns 方法指定了生成的主键列的名称。

java
public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(2);
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... 其他方法
}
kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor").usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = mapOf(
				"first_name" to actor.firstName,
				"last_name" to actor.lastName)
		val newId = insertActor.executeAndReturnKey(parameters);
		return actor.copy(id = newId.toLong())
	}

	// ... 其他方法
}

使用这种方法进行插入的主要区别在于:你不需要将 id 添加到 Map 中,而是调用 executeAndReturnKey 方法。该方法返回一个 java.lang.Number 对象。如果你有多个自动生成的列,或者生成的值是非数值型的,你可以使用由 executeAndReturnKeyHolder 方法返回的 KeyHolder

SimpleJdbcInsert 指定列

你可以通过 usingColumns 方法指定列名列表来限制插入的列:

java
public void setDataSource(DataSource dataSource) {
    this.insertActor = new SimpleJdbcInsert(dataSource)
            .withTableName("t_actor")
            .usingColumns("first_name", "last_name")
            .usingGeneratedKeyColumns("id");
}
kotlin
private val insertActor = SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingColumns("first_name", "last_name")
        .usingGeneratedKeyColumns("id")

使用 SqlParameterSource 提供参数值

使用 Map 提供参数值固然可行,但并不是最方便的。Spring 提供了 SqlParameterSource 接口的实现供你选择。常用的有 BeanPropertySqlParameterSource(适用于遵循 JavaBean 规范的类)和 MapSqlParameterSource(类似于 Map 但支持链式调用)。

java
public void add(Actor actor) {
    SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
    Number newId = insertActor.executeAndReturnKey(parameters);
    actor.setId(newId.longValue());
}
kotlin
fun add(actor: Actor): Actor {
    val parameters = BeanPropertySqlParameterSource(actor)
    val newId = insertActor.executeAndReturnKey(parameters)
    return actor.copy(id = newId.toLong())
}

使用 SimpleJdbcCall 调用存储过程

SimpleJdbcCall 类利用数据库中的元数据来查找 INOUT 参数的名字,因此你不需要显式声明它们。当然,你也可以根据需要显式声明。

以下示例展示了在 MySQL 中调用存储过程:

sql
CREATE PROCEDURE read_actor (
	IN in_id INTEGER,
	OUT out_first_name VARCHAR(100),
	OUT out_last_name VARCHAR(100),
	OUT out_birth_date DATE)
BEGIN
	SELECT first_name, last_name, birth_date
	INTO out_first_name, out_last_name, out_birth_date
	FROM t_actor where id = in_id;
END;
java
public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		this.procReadActor = new SimpleJdbcCall(dataSource)
				.withProcedureName("read_actor");
	}

	public Actor readActor(Long id) {
		SqlParameterSource in = new MapSqlParameterSource()
				.addValue("in_id", id);
		Map out = procReadActor.execute(in);
		Actor actor = new Actor();
		actor.setId(id);
		actor.setFirstName((String) out.get("out_first_name"));
		actor.setLastName((String) out.get("out_last_name"));
		actor.setBirthDate((Date) out.get("out_birth_date"));
		return actor;
	}
}
kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val procReadActor = SimpleJdbcCall(dataSource)
			.withProcedureName("read_actor")

	fun readActor(id: Long): Actor {
		val source = MapSqlParameterSource().addValue("in_id", id)
		val output = procReadActor.execute(source)
		return Actor(
				id,
				output["out_first_name"] as String,
				output["out_last_name"] as String,
				output["out_birth_date"] as Date)
	}
}

调用存储函数

调用存储函数的方式与存储过程基本相同,只是使用 withFunctionName 而非 withProcedureName,并使用 executeFunction 方法运行:

java
public String getActorName(Long id) {
    SqlParameterSource in = new MapSqlParameterSource()
            .addValue("in_id", id);
    String name = funcGetActorName.executeFunction(String.class, in);
    return name;
}
kotlin
fun getActorName(id: Long): String {
    val source = MapSqlParameterSource().addValue("in_id", id)
    return funcGetActorName.executeFunction(String::class.java, source)
}

补充教学

1. 为什么叫 "Simple"?—— 元数据的魔力

Spring JDBC 中带 Simple 前缀的类(如 SimpleJdbcInsert)都有一个共同的“超能力”:自动检测元数据(Metadata)。 普通的 JdbcTemplate 要求你必须写出完整的 INSERT INTO table (col1, col2) VALUES (?, ?)。而 SimpleJdbcInsert 只需要你告诉它表名。它会在初始化时询问数据库:“这张表有哪些列?主键是谁?”。 当你执行插入时,它会自动帮你拼装 SQL。这种方式极大地减少了代码中的硬编码字符串,降低了出错几率。

2. 警惕“大小写”陷阱

不同的数据库处理方式千差万别:

  • Oracle 默认将所有名称转换为大写。
  • MySQL 通常保留原始大小写。
  • PostgreSQL 默认转换为小写。

如果你在 Map 里的键名是 firstName,但数据库列名是 FIRST_NAMEfirstname,元数据匹配可能会失效。 建议:如果遇到“找不到列”的报错,可以尝试在初始化时配置 JdbcTemplate.setResultsMapCaseInsensitive(true),或者显式地在 usingColumns 中使用与数据库一致的大小写。

3. Java Record 类的完美搭档

在现代 Java 开发中,如果你使用 record 类来定义领域对象(Domain Object),配合 BeanPropertySqlParameterSource 简直是天作之合。

java
public record Actor(Long id, String firstName, String lastName) {}
// ...
insertActor.execute(new BeanPropertySqlParameterSource(new Actor(1L, "Jack", "Chen")));

你会发现,你几乎不需要写任何 SQL,也不需要手动拼装 Map,代码变得非常整洁且类型安全。

Based on Spring Framework.