跳到主要内容

使用 SimpleJdbc 类简化 JDBC 操作

ChatGPT-4o 中英对照 Simplifying JDBC Operations with the SimpleJdbc Classes Simplifying JDBC Operations with the SimpleJdbc Classes

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

使用 SimpleJdbcInsert 插入数据

我们首先来看一下 SimpleJdbcInsert 类的最小配置选项。你应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。在这个例子中,初始化方法是 setDataSource 方法。你不需要继承 SimpleJdbcInsert 类,而是可以创建一个新的实例,并使用 withTableName 方法设置表名。这个类的配置方法遵循 fluid 风格,返回 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);
}

// ... additional methods
}
java

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

// ... additional methods
}
java

当你使用第二种方法运行插入操作时,主要的区别在于你不需要将 id 添加到 Map 中,并且需要调用 executeAndReturnKey 方法。这个方法会返回一个 java.lang.Number 对象,你可以用它来创建在你的领域类中使用的数值类型的实例。你不能依赖所有数据库在这里返回一个特定的 Java 类。java.lang.Number 是你可以依赖的基础类。如果你有多个自动生成的列或者生成的值是非数值类型的,你可以使用从 executeAndReturnKeyHolder 方法返回的 KeyHolder

SimpleJdbcInsert 指定列

您可以通过使用 usingColumns 方法指定列名称列表来限制插入的列,如以下示例所示:

public class JdbcActorDao implements ActorDao {

private SimpleJdbcInsert insertActor;

public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.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());
}

// ... additional methods
}
java

插入的执行与依赖元数据来确定使用哪些列是相同的。

使用 SqlParameterSource 提供参数值

使用 Map 来提供参数值虽然可以正常工作,但它不是最方便的类。Spring 提供了几个 SqlParameterSource 接口的实现,你可以使用这些实现来代替。第一个是 BeanPropertySqlParameterSource,如果你有一个符合 JavaBean 规范的类来包含你的值,这是一个非常方便的类。它使用相应的 getter 方法来提取参数值。下面的示例展示了如何使用 BeanPropertySqlParameterSource

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) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}

// ... additional methods
}
java

另一种选择是 MapSqlParameterSource,它类似于 Map,但提供了一个更方便的 addValue 方法,可以进行链式调用。以下示例展示了如何使用它:

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) {
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", actor.getFirstName())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}

// ... additional methods
}
java

正如你所看到的,配置是相同的。只有执行代码需要更改以使用这些替代输入类。

使用 SimpleJdbcCall 调用存储过程

SimpleJdbcCall 类使用数据库中的元数据来查找 inout 参数的名称,因此您不必显式声明它们。如果您愿意,也可以声明参数,或者如果您有一些参数无法自动映射到 Java 类。第一个示例展示了一个简单的过程,该过程仅从 MySQL 数据库中以 VARCHARDATE 格式返回标量值。示例过程读取指定的演员条目,并以 out 参数的形式返回 first_namelast_namebirth_date 列。以下列表展示了第一个示例:

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;
sql

in_id 参数包含您正在查找的演员的 idout 参数返回从表中读取的数据。

你可以以类似于声明 SimpleJdbcInsert 的方式来声明 SimpleJdbcCall。你应该在数据访问层的初始化方法中实例化和配置该类。与 StoredProcedure 类相比,你不需要创建一个子类,也不需要声明可以在数据库元数据中查找的参数。下面是一个 SimpleJdbcCall 配置的示例,使用了前面的存储过程(除了 DataSource,唯一的配置选项是存储过程的名称):

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

// ... additional methods
}
java

你为执行调用编写的代码涉及创建一个包含 IN 参数的 SqlParameterSource。你必须将输入值提供的名称与存储过程声明的参数名称匹配。大小写不必匹配,因为你使用元数据来确定在存储过程中应如何引用数据库对象。存储过程源中指定的内容不一定是它在数据库中存储的方式。一些数据库将名称转换为全大写,而其他数据库使用小写或按指定的大小写使用。

execute 方法接受 IN 参数并返回一个 Map,其中包含存储过程指定的以名称为键的任何 out 参数。在这种情况下,它们是 out_first_nameout_last_nameout_birth_date

execute 方法的最后一部分创建了一个 Actor 实例,用于返回检索到的数据。同样,使用存储过程中声明的 out 参数的名称是很重要的。此外,结果映射中存储的 out 参数名称的大小写与数据库中的 out 参数名称匹配,这在不同的数据库之间可能会有所不同。为了使代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用 LinkedCaseInsensitiveMap。要实现后者,你可以创建自己的 JdbcTemplate 并将 setResultsMapCaseInsensitive 属性设置为 true。然后,你可以将这个自定义的 JdbcTemplate 实例传递给 SimpleJdbcCall 的构造函数。以下示例展示了这种配置:

public class JdbcActorDao implements ActorDao {

private SimpleJdbcCall procReadActor;

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

// ... additional methods
}
java

通过执行此操作,可以避免返回的 out 参数名称中的大小写冲突。

显式声明用于 SimpleJdbcCall 的参数

在本章前面,我们描述了如何从元数据中推导参数,但如果你愿意,也可以显式声明它们。你可以通过创建和配置 SimpleJdbcCall 并使用 declareParameters 方法来实现,该方法将多个 SqlParameter 对象作为输入。有关如何定义 SqlParameter 的详细信息,请参见下一节

备注

如果您使用的数据库不是 Spring 支持的数据库,则需要显式声明。目前,Spring 支持以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数的元数据查找。

您可以选择显式声明一个、一些或全部参数。在您没有显式声明参数的地方,参数元数据仍然会被使用。要绕过所有潜在参数的元数据查找处理,仅使用已声明的参数,您可以在声明中调用方法 withoutProcedureColumnMetaDataAccess。假设您为一个数据库函数声明了两个或多个不同的调用签名。在这种情况下,您可以调用 useInParameterNames 来指定要包含在给定签名中的 IN 参数名称列表。

下面的示例展示了一个完全声明的过程调用,并使用了前面示例中的信息:

public class JdbcActorDao implements ActorDao {

private SimpleJdbcCall procReadActor;

public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
new SqlOutParameter("out_last_name", Types.VARCHAR),
new SqlOutParameter("out_birth_date", Types.DATE)
);
}

// ... additional methods
}
java

这两个示例的执行和最终结果是相同的。第二个示例明确指定了所有细节,而不是依赖于元数据。

如何定义 SqlParameters

要为 SimpleJdbc 类以及 RDBMS 操作类(在将 JDBC 操作建模为 Java 对象中介绍)定义参数,可以使用 SqlParameter 或其子类之一。通常,您可以在构造函数中指定参数名称和 SQL 类型。SQL 类型通过使用 java.sql.Types 常量来指定。在本章前面,我们看到了类似于以下的声明:

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
java

第一行使用 SqlParameter 声明了一个 IN 参数。通过使用 SqlQuery 及其子类(在理解 SqlQuery中介绍),你可以在存储过程调用和查询中使用 IN 参数。

第二行(带有 SqlOutParameter)声明了一个 out 参数,用于存储过程调用中。还有一个 SqlInOutParameter 用于 InOut 参数(即向过程提供一个输入值并同时返回一个值的参数)。

备注

只有声明为 SqlParameterSqlInOutParameter 的参数用于提供输入值。这与 StoredProcedure 类不同,后者(出于向后兼容的原因)允许为声明为 SqlOutParameter 的参数提供输入值。

对于 IN 参数,除了名称和 SQL 类型之外,您还可以为数值数据指定一个小数位数,或者为自定义数据库类型指定一个类型名称。对于 out 参数,您可以提供一个 RowMapper 来处理从 REF 游标返回的行的映射。另一种选择是指定一个 SqlReturnType,以便有机会定义返回值的自定义处理方式。

使用 SimpleJdbcCall 调用存储函数

调用存储函数的方式与调用存储过程几乎相同,只是需要提供函数名而不是过程名。你可以使用 withFunctionName 方法作为配置的一部分来表示你想要调用一个函数,并生成相应的函数调用字符串。使用专门的调用方法(executeFunction)来运行函数,该方法返回指定类型的对象作为函数返回值,这意味着你不需要从结果映射中检索返回值。对于只有一个 out 参数的存储过程,也有一个类似的便捷方法(名为 executeObject)。以下示例(针对 MySQL)基于一个名为 get_actor_name 的存储函数,该函数返回演员的全名:

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
DECLARE out_name VARCHAR(200);
SELECT concat(first_name, ' ', last_name)
INTO out_name
FROM t_actor where id = in_id;
RETURN out_name;
END;
sql

要调用此函数,我们再次在初始化方法中创建一个 SimpleJdbcCall,如下例所示:

public class JdbcActorDao implements ActorDao {

private SimpleJdbcCall funcGetActorName;

public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name");
}

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

// ... additional methods
}
java

executeFunction 方法返回一个 String,其中包含函数调用的返回值。

SimpleJdbcCall 返回 ResultSet 或 REF 游标

调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而其他数据库则需要显式注册特定类型的 out 参数。两种方法都需要额外的处理来遍历结果集并处理返回的行。使用 SimpleJdbcCall,你可以使用 returningResultSet 方法并声明一个 RowMapper 实现用于特定参数。如果结果集在结果处理期间返回,则没有定义名称,因此返回的结果必须与声明 RowMapper 实现的顺序匹配。指定的名称仍然用于将处理后的结果列表存储在从 execute 语句返回的结果映射中。

下一个示例(针对 MySQL)使用了一个不带 IN 参数的存储过程,并返回 t_actor 表中的所有行:

CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
sql

要调用此过程,您可以声明 RowMapper。由于您要映射的类遵循 JavaBean 规则,您可以使用 BeanPropertyRowMapper,通过在 newInstance 方法中传入所需映射的类来创建。以下示例展示了如何做到这一点:

public class JdbcActorDao implements ActorDao {

private SimpleJdbcCall procReadAllActors;

public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor.class));
}

public List getActorsList() {
Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
return (List) m.get("actors");
}

// ... additional methods
}
java

execute 调用传入一个空的 Map,因为此调用不需要任何参数。然后从结果映射中检索演员列表并返回给调用者。