Simplifying JDBC Operations with the SimpleJdbc Classes

SimpleJdbcInsert 和 SimpleJdbcCall 类通过利用可以通过 JDBC 驱动检索到的数据库元数据,提供了一个简化配置。这意味着你在前面的配置较少,尽管你可以覆盖或关闭元数据处理,如果你喜欢在你的代码中提供所有细节。

通过使用 SimpleJdbcInsert 插入数据

Inserting Data by Using SimpleJdbcInsert

我们先来看看配置选项最少的 SimpleJdbcInsert 类。你应该在数据访问层的初始化方法中将 SimpleJdbcInsert 实例化。对于这个例子,初始化方法是 setDataSource方法。你不需要对 SimpleJdbcInsert 类进行子类化。相反,你可以创建一个新的实例并通过使用 withTableName 方法来设置表名。这个类的配置方法遵循返回 SimpleJdbcInsert 实例的流体风格,这让你可以将所有的配置方法连锁起来。下面的例子只使用了一个配置方法(后面我们会展示多个方法的例子)。

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcInsert insertActor;
  3. public void setDataSource(DataSource dataSource) {
  4. this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
  5. }
  6. public void add(Actor actor) {
  7. Map<String, Object> parameters = new HashMap<String, Object>(3);
  8. parameters.put("id", actor.getId());
  9. parameters.put("first_name", actor.getFirstName());
  10. parameters.put("last_name", actor.getLastName());
  11. insertActor.execute(parameters);
  12. }
  13. // ... additional methods
  14. }

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

通过使用 SimpleJdbcInsert 检索自动生成的键

Retrieving Auto-generated Keys by Using SimpleJdbcInsert

下一个例子使用了与前面例子相同的插入方法,但是,它没有传入 id,而是检索自动生成的键,并将其设置在新的 Actor 对象上。当它创建SimpleJdbcInsert 时,除了指定表名外,它还用 usingGeneratedKeyColumns方法指定了生成的键列的名称。下面的列表显示了它是如何工作的:

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcInsert insertActor;
  3. public void setDataSource(DataSource dataSource) {
  4. this.insertActor = new SimpleJdbcInsert(dataSource)
  5. .withTableName("t_actor")
  6. .usingGeneratedKeyColumns("id");
  7. }
  8. public void add(Actor actor) {
  9. Map<String, Object> parameters = new HashMap<String, Object>(2);
  10. parameters.put("first_name", actor.getFirstName());
  11. parameters.put("last_name", actor.getLastName());
  12. Number newId = insertActor.executeAndReturnKey(parameters);
  13. actor.setId(newId.longValue());
  14. }
  15. // ... additional methods
  16. }

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

为 SimpleJdbcInsert 指定列

Specifying Columns for a SimpleJdbcInsert

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

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcInsert insertActor;
  3. public void setDataSource(DataSource dataSource) {
  4. this.insertActor = new SimpleJdbcInsert(dataSource)
  5. .withTableName("t_actor")
  6. .usingColumns("first_name", "last_name")
  7. .usingGeneratedKeyColumns("id");
  8. }
  9. public void add(Actor actor) {
  10. Map<String, Object> parameters = new HashMap<String, Object>(2);
  11. parameters.put("first_name", actor.getFirstName());
  12. parameters.put("last_name", actor.getLastName());
  13. Number newId = insertActor.executeAndReturnKey(parameters);
  14. actor.setId(newId.longValue());
  15. }
  16. // ... additional methods
  17. }

插入的执行与你依靠元数据来决定使用哪些列是一样的。

使用 SqlParameterSource 来提供参数值

Using SqlParameterSource to Provide Parameter Values

使用 Map 来提供参数值效果很好,但它不是最方便使用的类。Spring 提供了几个 SqlParameterSource 接口的实现,你可以使用它们来代替。第一个是 BeanPropertySqlParameterSource,如果你有一个符合 JavaBean 标准的类,它是一个非常方便的类,包含你的值。它使用相应的 getter 方法来提取参数值。下面的例子展示了如何使用 BeanPropertySqlParameterSource:

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcInsert insertActor;
  3. public void setDataSource(DataSource dataSource) {
  4. this.insertActor = new SimpleJdbcInsert(dataSource)
  5. .withTableName("t_actor")
  6. .usingGeneratedKeyColumns("id");
  7. }
  8. public void add(Actor actor) {
  9. SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
  10. Number newId = insertActor.executeAndReturnKey(parameters);
  11. actor.setId(newId.longValue());
  12. }
  13. // ... additional methods
  14. }

另一个选择是 MapSqlParameterSource,它类似于 Map,但提供了一个更方便的 addValue 方法,可以被链起来。下面的例子展示了如何使用它:

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcInsert insertActor;
  3. public void setDataSource(DataSource dataSource) {
  4. this.insertActor = new SimpleJdbcInsert(dataSource)
  5. .withTableName("t_actor")
  6. .usingGeneratedKeyColumns("id");
  7. }
  8. public void add(Actor actor) {
  9. SqlParameterSource parameters = new MapSqlParameterSource()
  10. .addValue("first_name", actor.getFirstName())
  11. .addValue("last_name", actor.getLastName());
  12. Number newId = insertActor.executeAndReturnKey(parameters);
  13. actor.setId(newId.longValue());
  14. }
  15. // ... additional methods
  16. }

正如你所看到的,配置是一样的。只有执行代码必须改变以使用这些替代的输入类。

用 SimpleJdbcCall 调用一个存储过程

Calling a Stored Procedure with SimpleJdbcCall

SimpleJdbcCall 类使用数据库中的元数据来查找输入和输出参数的名称,这样你就不必明确声明它们。如果你喜欢这样做,或者你的参数(如ARRAY 或 STRUCT)没有自动映射到一个 Java 类,你可以声明参数。第一个例子显示了一个简单的过程,它只从 MySQL 数据库中返回VARCHAR 和 DATE 格式的标量值。这个示例存储过程读取了一个指定的 actor 条目,并以输出参数的形式返回 first_name, last_name 和 birth_date 列。下面的列表显示了第一个例子:

  1. CREATE PROCEDURE read_actor (
  2. IN in_id INTEGER,
  3. OUT out_first_name VARCHAR(100),
  4. OUT out_last_name VARCHAR(100),
  5. OUT out_birth_date DATE)
  6. BEGIN
  7. SELECT first_name, last_name, birth_date
  8. INTO out_first_name, out_last_name, out_birth_date
  9. FROM t_actor where id = in_id;
  10. END;

in_id 参数包含你要查询的 actor 的 ID。out 参数返回从表中读取的数据。

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

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcCall procReadActor;
  3. public void setDataSource(DataSource dataSource) {
  4. this.procReadActor = new SimpleJdbcCall(dataSource)
  5. .withProcedureName("read_actor");
  6. }
  7. public Actor readActor(Long id) {
  8. SqlParameterSource in = new MapSqlParameterSource()
  9. .addValue("in_id", id);
  10. Map out = procReadActor.execute(in);
  11. Actor actor = new Actor();
  12. actor.setId(id);
  13. actor.setFirstName((String) out.get("out_first_name"));
  14. actor.setLastName((String) out.get("out_last_name"));
  15. actor.setBirthDate((Date) out.get("out_birth_date"));
  16. return actor;
  17. }
  18. // ... additional methods
  19. }

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

execute 方法接收 IN 参数,并返回一个 Map,该 Map 包含存储过程中指定的以名称为键的任何 out 参数。在本例中,它们是out_first_name, out_last_name, 和 out_birth_date。

执行方法的最后一部分是创建一个 Actor 实例,用来返回检索到的数据。同样,使用存储过程中声明的 out 参数的名称是很重要的。另外,存储在结果图中的输出参数名称的大小写要与数据库中的输出参数名称的大小写一致,这在不同的数据库中可能有所不同。为了使你的代码更具可移植性,你应该做一个不区分大小写的查找,或者指示 Spring 使用 LinkedCaseInsensitiveMap(不区分 key 的大小写的 Map)。要做到后者,你可以创建自己的 JdbcTemplate,并将 setResultsMapCaseInsensitive属性设置为 「true」。然后你可以把这个定制的 JdbcTemplate 实例传递给 SimpleJdbcCall 的构造函数。下面的例子显示了这种配置:

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcCall procReadActor;
  3. public void setDataSource(DataSource dataSource) {
  4. JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  5. jdbcTemplate.setResultsMapCaseInsensitive(true);
  6. this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
  7. .withProcedureName("read_actor");
  8. }
  9. // ... additional methods
  10. }

明确声明用于 SimpleJdbcCall 的参数

Explicitly Declaring Parameters to Use for a SimpleJdbcCall

在本章的前面,我们描述了如何从元数据中推导出参数,但是如果你愿意,你也可以明确地声明它们。你可以通过创建和配置 SimpleJdbcCall的 declarationParameters 方法来做到这一点,该方法需要一个数量可变的 SqlParameter 对象作为输入。关于如何定义 SqlParameter 的细节,见下一节。

:::info 如果你使用的数据库不是 Spring 支持的数据库,则必须进行明确的声明。目前,Spring 支持对以下数据库的存储过程调用进行元数据查询。Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数的元数据查询。 :::

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

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

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcCall procReadActor;
  3. public void setDataSource(DataSource dataSource) {
  4. JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  5. jdbcTemplate.setResultsMapCaseInsensitive(true);
  6. this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
  7. .withProcedureName("read_actor")
  8. .withoutProcedureColumnMetaDataAccess()
  9. .useInParameterNames("in_id")
  10. .declareParameters(
  11. new SqlParameter("in_id", Types.NUMERIC), // 主要就是声明参数的类型
  12. new SqlOutParameter("out_first_name", Types.VARCHAR),
  13. new SqlOutParameter("out_last_name", Types.VARCHAR),
  14. new SqlOutParameter("out_birth_date", Types.DATE)
  15. );
  16. }
  17. // ... additional methods
  18. }

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

如何定义 SqlParameters

How to Define SqlParameters

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

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

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

第二行(带有 SqlOutParameter)声明了一个用于存储过程调用的输出参数。还有一个 SqlInOutParameter 用于 InOut 参数(为存储过程提供一个 IN 值的参数,同时也返回一个值)。

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

对于输入参数,除了名称和 SQL 类型外,你可以为数字数据指定一个刻度,或者为自定义数据库类型指定一个类型名称。对于 out 参数,你可以提供一个 RowMapper 来处理从 REF 游标返回的行的映射。另一个选择是指定一个 SqlReturnType,它提供了一个机会来定义对返回值的自定义处理。

通过使用 SimpleJdbcCall 调用存储函数

Calling a Stored Function by Using SimpleJdbcCall

你可以用与调用存储过程几乎相同的方式调用存储函数,只是你提供了一个函数名而不是存储过程名。你使用 withFunctionName 方法作为配置的一部分来表明你想对一个函数进行调用,并且生成相应的函数调用字符串。一个专门的调用(executeFunction)被用来运行该函数,它将函数的返回值作为一个指定类型的对象返回,这意味着你不必从结果图中检索返回值。一个类似的方便方法(名为 executeObject)也可用于只有一个输出参数的存储过程。下面的例子(针对 MySQL)是基于一个名为 get_actor_name 的存储函数,它返回一个 actor 的全名:

  1. CREATE FUNCTION get_actor_name (in_id INTEGER)
  2. RETURNS VARCHAR(200) READS SQL DATA
  3. BEGIN
  4. DECLARE out_name VARCHAR(200);
  5. SELECT concat(first_name, ' ', last_name)
  6. INTO out_name
  7. FROM t_actor where id = in_id;
  8. RETURN out_name;
  9. END;

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

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcCall funcGetActorName;
  3. public void setDataSource(DataSource dataSource) {
  4. JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  5. jdbcTemplate.setResultsMapCaseInsensitive(true);
  6. this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
  7. .withFunctionName("get_actor_name");
  8. }
  9. public String getActorName(Long id) {
  10. SqlParameterSource in = new MapSqlParameterSource()
  11. .addValue("in_id", id);
  12. String name = funcGetActorName.executeFunction(String.class, in);
  13. return name;
  14. }
  15. // ... additional methods
  16. }

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

从 SimpleJdbcCall 中返回一个结果集或 REF 游标

Returning a ResultSet or REF Cursor from a SimpleJdbcCall

调用一个返回结果集的存储过程或函数是有点麻烦的。一些数据库在 JDBC 结果处理过程中返回结果集,而其他数据库则需要一个明确注册的特定类型的参数。这两种方法都需要额外的处理来循环处理结果集,并处理返回的行。通过 SimpleJdbcCall,你可以使用 returningResultSet 方法,并声明一个 RowMapper 实现,以用于特定参数。如果在结果处理过程中返回结果集,没有定义名称,所以返回的结果必须与你声明 RowMapper 实现的顺序相匹配。指定的名称仍然用于在从执行语句返回的结果图中存储处理后的结果列表。

下一个例子(针对 MySQL)使用了一个存储过程,它不需要 IN 参数,并返回 t_actor 表中的所有记录:

  1. CREATE PROCEDURE read_all_actors()
  2. BEGIN
  3. SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
  4. END;

为了调用这个程序,你可以声明 RowMapper。因为你想映射到的类遵循 JavaBean 规则,你可以使用一个 BeanPropertyRowMapper,它是通过在 newInstance 方法中传入需要映射到的类而创建的。下面的例子显示了如何做到这一点:

  1. public class JdbcActorDao implements ActorDao {
  2. private SimpleJdbcCall procReadAllActors;
  3. public void setDataSource(DataSource dataSource) {
  4. JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  5. jdbcTemplate.setResultsMapCaseInsensitive(true);
  6. this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
  7. .withProcedureName("read_all_actors")
  8. .returningResultSet("actors",
  9. BeanPropertyRowMapper.newInstance(Actor.class));
  10. }
  11. public List getActorsList() {
  12. Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
  13. return (List) m.get("actors");
  14. }
  15. // ... additional methods
  16. }

执行调用传入一个空的 Map,因为这个调用不接受任何参数。然后从结果图中检索出 actor 列表,并返回给调用者。