使用 SimpleJdbc 类简化 JDBC 操作 (Simplifying JDBC Operations with the SimpleJdbc Classes)
SimpleJdbcInsert 和 SimpleJdbcCall 类通过利用可以从 JDBC 驱动程序检索到的数据库元数据,提供了简化的配置。这意味着你预先需要配置的内容更少,尽管如果你愿意在代码中提供所有细节,也可以覆盖或关闭元数据处理。
使用 SimpleJdbcInsert 插入数据
我们首先来看使用最少配置选项的 SimpleJdbcInsert 类。你应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。在这个例子中,初始化方法是 setDataSource 方法。你不需要继承 SimpleJdbcInsert 类;相反,你可以创建一个新实例并使用 withTableName 方法设置表名。该类的配置方法遵循流式(fluent)风格,返回 SimpleJdbcInsert 实例,允许你链式调用所有配置方法。
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);
}
// ... 其他方法
}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 方法指定了生成的主键列的名称。
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());
}
// ... 其他方法
}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 方法指定列名列表来限制插入的列:
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
}private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id")使用 SqlParameterSource 提供参数值
使用 Map 提供参数值固然可行,但并不是最方便的。Spring 提供了 SqlParameterSource 接口的实现供你选择。常用的有 BeanPropertySqlParameterSource(适用于遵循 JavaBean 规范的类)和 MapSqlParameterSource(类似于 Map 但支持链式调用)。
public void add(Actor actor) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}fun add(actor: Actor): Actor {
val parameters = BeanPropertySqlParameterSource(actor)
val newId = insertActor.executeAndReturnKey(parameters)
return actor.copy(id = newId.toLong())
}使用 SimpleJdbcCall 调用存储过程
SimpleJdbcCall 类利用数据库中的元数据来查找 IN 和 OUT 参数的名字,因此你不需要显式声明它们。当然,你也可以根据需要显式声明。
以下示例展示了在 MySQL 中调用存储过程:
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;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;
}
}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 方法运行:
public String getActorName(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
String name = funcGetActorName.executeFunction(String.class, in);
return name;
}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_NAME 或 firstname,元数据匹配可能会失效。 建议:如果遇到“找不到列”的报错,可以尝试在初始化时配置 JdbcTemplate.setResultsMapCaseInsensitive(true),或者显式地在 usingColumns 中使用与数据库一致的大小写。
3. Java Record 类的完美搭档
在现代 Java 开发中,如果你使用 record 类来定义领域对象(Domain Object),配合 BeanPropertySqlParameterSource 简直是天作之合。
public record Actor(Long id, String firstName, String lastName) {}
// ...
insertActor.execute(new BeanPropertySqlParameterSource(new Actor(1L, "Jack", "Chen")));你会发现,你几乎不需要写任何 SQL,也不需要手动拼装 Map,代码变得非常整洁且类型安全。