使用 SimpleJdbc 类简化 JDBC 操作
SimpleJdbcInsert 和 SimpleJdbcCall 类利用可以通过 JDBC 驱动程序检索的数据库元数据,提供了简化的配置。
这意味着您可以减少前期的配置工作,当然,如果您更喜欢在代码中提供所有细节,也可以覆盖或关闭元数据处理。
使用 SimpleJdbcInsert 插入数据
我们首先看看具有最少配置选项的 SimpleJdbcInsert 类。您应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。
对于此示例,初始化方法是 setDataSource 方法。您不需要子类化 SimpleJdbcInsert 类。
相反,您可以创建一个新实例并使用 withTableName 方法设置表名。
此类的配置方法遵循返回 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);
}
// ... 其他方法
}
此处使用的 execute 方法接受一个 java.util.Map 作为其唯一参数。这里重要的一点是,用于 Map 的键必须与数据库中定义的表的列名匹配。
这是因为我们读取元数据以构建实际的 SQL 语句。
使用 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());
}
// ... 其他方法
}
运行此代码时的主要区别在于,您不将 ID 添加到 Map 中,而是调用 executeAndReturnKey 方法。
这会返回一个 java.lang.Number 对象,您可以使用它来创建域类中使用的数值类型的实例。
您不能依赖所有的数据库都返回特定的 Java 类。java.lang.Number 是您可以依赖的基类。
如果您有多个自动生成的列,或者是生成的值是非数值的,您可以使用从 executeAndReturnKeyHolder 方法返回的 KeyHolder。
指定 SimpleJdbcInsert 的列
您可以通过使用 usingColumns 方法指定列名列表来限制插入的列,如下例所示:
-
Java
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());
}
// ... 其他方法
}
插入的执行与之前相同。这里的区别在于,只有给定的列会在 SQL 插入语句中使用。
使用 SqlParameterSource 提供参数值
使用 Map 提供参数值工作得很好,但这不是最方便的类。
Infra 提供了几个 SqlParameterSource 接口的实现,您可以方便地使用它们。
第一个是 BeanPropertySqlParameterSource,如果你有一个遵循 JavaBean 约定的类,并且它具有与列名匹配的属性,这非常方便。
它使用相应的 getter 方法来提取参数值。以下示例展示了如何使用 BeanPropertySqlParameterSource:
-
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) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... 其他方法
}
另一种选择是 MapSqlParameterSource,它类似于 Map,但提供了一个更方便的 addValue 方法,可以链接起来。
以下示例展示了如何使用它:
-
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) {
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", actor.getFirstName())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... 其他方法
}
正如您所看到的,配置是相同的。只有执行代码需要更改以使用这些替代输入类。
使用 SimpleJdbcCall 调用存储过程
SimpleJdbcCall 类利用数据库中的元数据来查找 in 和 out 参数的名称,这样您就不必显式声明它们。
如果愿意,您可以声明参数,或者如果除此之外您还有参数(如 ARRAY 或 STRUCT)没有自动映射到 Java 类,也可以声明参数。
第一个示例展示了一个简单的过程,它仅返回 VARCHAR 和 DATE 格式的标量值。
该过程示例读取指定的 actor 条目,并以 out 参数的形式返回 first_name、last_name 和 birth_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 参数包含您要查找的 actor 的 id。out 参数返回从表中读取的数据。
您可以以类似于声明 SimpleJdbcInsert 的方式声明 SimpleJdbcCall。
您应该在数据访问层的初始化方法中实例化和配置该类。
与 StoredProcedure 类相比,您不需要创建子类,也不需要声明可以在数据库元数据中查找的参数。
以下 SimpleJdbcCall 配置示例使用前面的存储过程(除了 DataSource 之外,唯一的配置选项是存储过程的名称):
-
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;
}
// ... 其他方法
}
您为执行调用编写的代码涉及创建一个包含 IN 参数的 SqlParameterSource。
您必须将为输入值提供的名称与存储过程中声明的参数名称相匹配。
大小写不必匹配,因为您使用元数据来确定如何在存储过程中引用数据库对象。
在存储过程的源中指定的内容不一定是它在数据库中的存储方式。
有些数据库将名称转换为全大写,而另一些数据库则使用小写或按指定的大小写使用。
execute 方法接受 IN 参数并返回一个 Map,其中包含由存储过程中的名称作为键的任何 out 参数。
在这种情况下,它们是 out_first_name、out_last_name 和 out_birth_date。
execute 方法的最后一部分创建一个 Actor 实例,用于返回检索到的数据。
同样,使用存储过程中声明的 out 参数名称非常重要。
此外,存储在结果映射中的 out 参数名称的大小写与数据库中 out 参数名称的大小写相匹配,这可能因数据库而异。
为了使您的代码更具可移植性,您应该进行不区分大小写的查找,或者指示 Infra 使用 LinkedCaseInsensitiveMap。
要执行后者,您可以创建自己的 JdbcTemplate 并将 setResultsMapCaseInsensitive 属性设置为 true。
然后,您可以将此自定义 JdbcTemplate 实例传递给您的 SimpleJdbcCall 的构造函数。
以下示例展示了此配置:
-
Java
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");
}
// ... 其他方法
}
通过采取此操作,您可以避免用于返回的 out 参数名称的大小写冲突。
为 SimpleJdbcCall 显式声明参数
在本章前面,我们描述了如何从元数据推导参数,但如果愿意,您可以显式声明它们。
为此,您可以使用 declareParameters 方法创建和配置 SimpleJdbcCall,该方法接受可变数量的 SqlParameter 对象作为输入。
有关如何定义 SqlParameter 的详细信息,请参阅 下一节。
| 如果您使用的数据库不是 Infra 支持的数据库,则必须进行显式声明。 目前,Infra 支持对以下数据库的存储过程调用进行元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。 我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数元数据查找。 |
您可以选择显式声明一个、部分或全部参数。在您未显式声明参数的地方,仍会使用参数元数据。
要绕过对潜在参数的所有元数据查找处理并仅使用声明的参数,您可以调用 withoutProcedureColumnMetaDataAccess 方法作为声明的一部分。
假设您为一个数据库函数声明了两个或多个不同的调用签名。在这种情况下,您调用 useInParameterNames 来指定要包含在给定签名中的 IN 参数名称列表。
以下示例显示了一个完全声明的过程调用,并使用了前面示例中的信息:
-
Java
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)
);
}
// ... 其他方法
}
这两个示例的执行和最终结果是相同的。第二个示例显式指定了所有详细信息,而不是依赖于元数据。
如何定义 SqlParameter
要为 SimpleJdbc 类(以及 RDBMS 操作类,涵盖在 将 JDBC 操作建模为 Java 对象 中)定义参数,您可以使用 SqlParameter 或其子类之一。
为此,您通常在构造函数中指定参数名称和 SQL 类型。SQL 类型使用 java.sql.Types 常量指定。
在本章前面,我们看到了类似于以下的声明:
-
Java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
带有 SqlParameter 的第一行声明了一个 IN 参数。IN 参数既可用于存储过程调用,也可用于使用 SqlQuery 及其子类的查询(涵盖在 理解 SqlQuery 中)。
第二行(带有 SqlOutParameter)声明了一个用于存储过程调用的 out 参数。
还有一个用于 InOut 参数的 SqlInOutParameter(即为存储过程提供 IN 值同时也返回值的参数)。
只有声明为 SqlParameter 和 SqlInOutParameter 的参数才用于提供输入值。
这与 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;
要调用此函数,我们再次在初始化方法中创建一个 SimpleJdbcCall,如下例所示:
-
Java
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;
}
// ... 其他方法
}
使用的 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;
要调用此过程,您可以声明 RowMapper。因为您要映射到的类遵循 JavaBean 规则,
所以您可以使用通过传入所需的映射类到 newInstance 方法创建的 BeanPropertyRowMapper。
以下示例展示了如何执行此操作:
-
Java
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");
}
// ... 其他方法
}
execute 调用传入一个空的 Map,因为此调用不带任何参数。
然后从结果映射中检索演员列表并将其返回给调用者。