跳到主要内容
版本:7.0.3

使用SimpleJdbc类简化JDBC操作

Hunyuan 7b 中英对照 Simplifying JDBC Operations with the SimpleJdbc Classes Simplifying JDBC Operations with the SimpleJdbc Classes

SimpleJdbcInsertSimpleJdbcCall 类通过利用可以通过 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);
}

// ... additional methods
}

这里使用的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
}

当你使用第二种方法进行插入操作时,主要的不同之处在于:你不会将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
}

插入操作的执行方式,与依赖元数据来决定使用哪些列的方式是相同的。

使用 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
}

另一个选择是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
}

如你所见,配置是相同的。只需要修改执行代码,以便使用这些替代的输入类即可。

使用 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;

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
}

你为执行调用而编写的代码需要创建一个包含IN参数的SqlParameterSource对象。你必须将提供的输入值名称与存储过程中声明的参数名称相匹配。大小写并不需要完全一致,因为你可以使用元数据来决定在存储过程中如何引用数据库对象。存储过程中源代码中指定的名称并不一定就是数据库中存储的名称。有些数据库会将名称转换为全部大写形式,而有些数据库则使用小写形式,或者按照指定的大小写规则进行存储。

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

execute 方法的最后一部分会创建一个 Actor 实例,用于返回检索到的数据。同样重要的是,要使用存储过程中声明的 out 参数名称。此外,结果映射中存储的 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
}

通过采取这一行动,您可以避免在用于返回的out参数名称的情况中产生冲突。

明确声明用于 SimpleJdbcCall 的参数

在本章的早些部分,我们描述了如何从元数据中推断参数,但如果你愿意,也可以显式地声明它们。你可以通过使用declareParameters方法来创建和配置SimpleJdbcCall来实现这一点,该方法接受可变数量的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
}

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

如何定义 SqlParameters

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

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

第一行中,通过SqlParameter声明了一个IN参数。你可以使用IN参数来调用存储过程,也可以在查询中使用它们,方法是利用SqlQuery及其子类(详见Understanding SqlQuery)。

第二行(带有SqlOutParameter)声明了一个用于存储过程调用的out参数。还有一个SqlInOutParameter用于处理InOut参数(这类参数既向过程提供IN值,又会返回一个值)。

备注

只有被声明为 SqlParameterSqlInOutParameter 的参数才用于提供输入值。这与 StoredProcedure 类不同,出于向后兼容性的考虑,StoredProcedure 允许为被声明为 SqlOutParameter 的参数提供输入值。

对于IN参数,除了名称和SQL类型之外,你还可以为数值数据指定一个比例(scale),或者为自定义数据库类型指定一个类型名称。对于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;

要调用此函数,我们需要在初始化方法中再次创建一个 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
}

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

SimpleJdbcCall 返回 ResultSet 或 REF Cursor

调用返回结果集的存储过程或函数有点棘手。有些数据库在JDBC结果处理过程中会直接返回结果集,而另一些数据库则需要显式注册一个特定类型的out参数。这两种方法都需要额外的处理步骤来遍历结果集并处理返回的行。使用SimpleJdbcCall时,你可以使用returningResultSet方法,并声明一个RowMapper实现来处理特定的参数。如果结果集在结果处理过程中直接返回,那么不会定义任何名称,因此返回的结果必须与你声明的RowMapper实现的顺序一致。指定的名称仍然用于将处理后的结果列表存储在从execute语句返回的结果映射(results map)中。

下一个示例(针对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;

要调用此程序,您可以声明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
}

execute 调用传递了一个空的 Map,因为该调用不接受任何参数。然后从结果 Map 中检索出参与者列表,并将其返回给调用者。