|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
使用 SimpleJdbc 类简化 JDBC 操作
SimpleJdbcInsert 和 SimpleJdbcCall 类通过利用 JDBC 驱动程序可检索的数据库元数据,提供了简化的配置。
这意味着您在初始阶段需要进行的配置更少,不过如果您希望在代码中提供所有细节,也可以选择覆盖或关闭元数据处理。
使用以下方式插入数据SimpleJdbcInsert
我们首先查看 SimpleJdbcInsert 类,它具有最少的配置选项。您应在数据访问层的初始化方法中实例化 SimpleJdbcInsert。在本例中,初始化方法是 setDataSource 方法。您无需对 SimpleJdbcInsert 类进行子类化,而是可以创建一个新实例,并使用 withTableName 方法设置表名。该类的配置方法采用fluid风格,会返回 SimpleJdbcInsert 的实例,从而允许您将所有配置方法链式调用。以下示例仅使用了一个配置方法(稍后我们会展示多个方法的示例):
-
Java
-
Kotlin
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
}
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)
}
// ... additional methods
}
此处使用的 execute 方法仅接受一个普通的 java.util.Map 作为参数。
这里需要注意的重要一点是,Map 中使用的键必须与数据库中定义的表列名相匹配。
这是因为我们会读取元数据来构建实际的插入语句。
通过使用检索自动生成的键SimpleJdbcInsert
下一个示例使用与前一个示例相同的插入操作,但不是传入id,而是检索自动生成的键并将其设置到新的Actor对象上。在创建SimpleJdbcInsert时,除了指定表名之外,还通过usingGeneratedKeyColumns方法指定了生成键列的名称。以下代码清单展示了其工作方式:
-
Java
-
Kotlin
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
}
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())
}
// ... additional methods
}
当你使用第二种方法执行插入操作时,主要区别在于:你不需要将 id 添加到 Map 中,而是调用 executeAndReturnKey 方法。该方法会返回一个 java.lang.Number 对象,你可以利用它来创建你的领域类中所使用的数值类型的实例。但你不能依赖所有数据库在此处都返回特定的 Java 类。java.lang.Number 是你可以依赖的基类。如果你有多个自动生成的列,或者生成的值是非数值类型,那么你可以使用通过 KeyHolder 方法返回的 executeAndReturnKeyHolder。
指定列以用于一个SimpleJdbcInsert
你可以通过 usingColumns 方法指定一个列名列表,从而限制插入操作所涉及的列,如下例所示:
-
Java
-
Kotlin
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.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())
}
// ... additional methods
}
执行插入操作的方式与依赖元数据来确定使用哪些列的情况相同。
使用SqlParameterSource提供参数值
使用 Map 来提供参数值是可行的,但它并不是最方便的类。Spring 提供了几个 SqlParameterSource 接口的实现类,可以作为替代方案。第一个是 BeanPropertySqlParameterSource,如果你有一个符合 JavaBean 规范的类来包含你的值,那么这个类就非常方便。它会使用对应的 getter 方法来提取参数值。以下示例展示了如何使用 BeanPropertySqlParameterSource:
-
Java
-
Kotlin
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id")
fun add(actor: Actor): Actor {
val parameters = BeanPropertySqlParameterSource(actor)
val newId = insertActor.executeAndReturnKey(parameters)
return actor.copy(id = newId.toLong())
}
// ... additional methods
}
另一种选择是 MapSqlParameterSource,它类似于 Map,但提供了更便捷的 addValue 方法,该方法支持链式调用。以下示例展示了如何使用它:
-
Java
-
Kotlin
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val insertActor = SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id")
fun add(actor: Actor): Actor {
val parameters = MapSqlParameterSource()
.addValue("first_name", actor.firstName)
.addValue("last_name", actor.lastName)
val newId = insertActor.executeAndReturnKey(parameters)
return actor.copy(id = newId.toLong())
}
// ... additional methods
}
如您所见,配置是相同的。只有执行代码需要更改,以使用这些替代的输入类。
调用存储过程,使用SimpleJdbcCall
SimpleJdbcCall 类使用数据库中的元数据来查找 in 和 out 参数的名称,因此您无需显式声明它们。如果您愿意,或者如果您的参数(例如 ARRAY 或 STRUCT)没有自动映射到 Java 类,则可以声明这些参数。第一个示例展示了一个简单的存储过程,该过程从 MySQL 数据库中仅以 VARCHAR 和 DATE 格式返回标量值。该示例存储过程读取指定的演员条目,并以 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 参数包含您要查找的演员的 id。out
参数返回从表中读取的数据。
你可以像声明 SimpleJdbcCall 一样来声明 SimpleJdbcInsert。你应该在数据访问层的初始化方法中实例化并配置该类。与 StoredProcedure 类相比,你无需创建子类,也无需声明那些可以通过数据库元数据查找到的参数。SimpleJdbcCall 的以下配置示例使用了前面提到的存储过程(除了 DataSource 之外,唯一的配置选项就是存储过程的名称):
-
Java
-
Kotlin
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
}
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)
}
// ... additional methods
}
您为执行调用所编写的代码涉及创建一个包含 IN 参数的 SqlParameterSource。
您必须确保为输入值提供的名称与存储过程中声明的参数名称相匹配。
大小写不必完全一致,因为您可以使用元数据来确定在存储过程中应如何引用数据库对象。
存储过程源代码中指定的名称不一定就是其在数据库中存储的方式。
某些数据库会将名称转换为全大写,而其他数据库则使用小写,或者保留原始指定的大小写形式。
execute 方法接收输入(IN)参数,并返回一个 Map,其中包含存储过程中指定名称的输出(out)参数。在本例中,这些参数是 out_first_name、out_last_name 和 out_birth_date。
execute 方法的最后一部分会创建一个 Actor 实例,用于返回检索到的数据。同样重要的是,必须使用存储过程中声明的 out 参数名称。此外,结果映射中存储的 out 参数名称的大小写需与数据库中 out 参数名称的大小写保持一致,而不同数据库之间可能存在差异。为了提高代码的可移植性,您应当执行不区分大小写的查找,或指示 Spring 使用 LinkedCaseInsensitiveMap。
要实现后者,您可以创建自己的 JdbcTemplate,并将 setResultsMapCaseInsensitive 属性设置为 true。然后,您可以将这个定制的 JdbcTemplate 实例传入您的 SimpleJdbcCall 构造函数中。以下示例展示了此配置:
-
Java
-
Kotlin
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_actor")
// ... additional methods
}
通过执行此操作,您可以避免在返回的 out 参数名称的大小写使用上发生冲突。
显式声明要使用的参数SimpleJdbcCall
在本章前面部分,我们描述了参数如何从元数据中推断得出,但如果你愿意,也可以显式声明它们。
你可以通过使用 SimpleJdbcCall 方法创建并配置 declareParameters 来实现这一点,
该方法接受可变数量的 SqlParameter 对象作为输入。有关如何定义 #jdbc-params 的详细信息,请参见下一节。
| 如果所使用的数据库不是 Spring 支持的数据库,则需要显式声明。目前,Spring 支持以下数据库的存储过程调用元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。 我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数元数据查找。 |
您可以选择显式声明一个、部分或全部参数。对于未显式声明的参数,仍会使用参数元数据。如果您希望跳过所有潜在参数的元数据查找处理,仅使用已声明的参数,可以在声明中调用 withoutProcedureColumnMetaDataAccess 方法。假设您为某个数据库函数声明了两个或更多不同的调用签名,此时应调用 useInParameterNames 来指定在给定签名中包含的 IN 参数名称列表。
以下示例展示了一个完整声明的存储过程调用,并使用了前述示例中的信息:
-
Java
-
Kotlin
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),
SqlOutParameter("out_last_name", Types.VARCHAR),
SqlOutParameter("out_birth_date", Types.DATE)
)
// ... additional methods
}
这两个示例的执行过程和最终结果是相同的。第二个示例显式指定了所有细节,而不是依赖元数据。
如何定义SqlParameters
要为 SimpleJdbc 类以及 RDBMS 操作类(参见将 JDBC 操作建模为 Java 对象一节)定义参数,您可以使用 SqlParameter 或其某个子类。
通常,您需要在构造函数中指定参数名称和 SQL 类型。SQL 类型通过使用 java.sql.Types 中的常量来指定。本章前面我们已经看到过类似如下的声明:
-
Java
-
Kotlin
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),
包含 SqlParameter 的第一行声明了一个 IN 参数。您可以使用 IN 参数来调用存储过程和执行查询,方法是使用 SqlQuery 及其子类(在 理解 SqlQuery 中涵盖)。
第二行(使用 SqlOutParameter)声明了一个在存储过程调用中使用的 out 参数。此外,还有一个 SqlInOutParameter 用于 InOut 参数(即向存储过程提供输入值,同时也返回一个值的参数)。
只有声明为 SqlParameter 和 SqlInOutParameter 的参数才会用于提供输入值。这与 StoredProcedure 类不同,后者(出于向后兼容的原因)允许为声明为 SqlOutParameter 的参数提供输入值。 |
对于 IN 参数,除了指定名称和 SQL 类型外,还可以为数值型数据指定精度(scale),或为自定义数据库类型指定类型名称。对于 out 参数,您可以提供一个 RowMapper 来处理从 REF 游标返回的行数据的映射。另一种选择是指定一个 SqlReturnType,以便自定义返回值的处理方式。
调用存储函数,使用SimpleJdbcCall
你可以几乎以与调用存储过程相同的方式来调用存储函数,唯一的区别是你提供的是函数名而非过程名。你可以在配置中使用 withFunctionName 方法来表明你希望调用一个函数,系统会自动生成对应的函数调用字符串。执行函数时使用专门的方法(executeFunction),该方法会以指定类型的对象形式返回函数的返回值,这意味着你无需从结果映射(results map)中手动获取返回值。对于只有一个 executeObject 参数的存储过程,也有一个类似的便捷方法(名为 out)。以下示例(针对 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
-
Kotlin
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}
private val funcGetActorName = SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name")
fun getActorName(id: Long): String {
val source = MapSqlParameterSource().addValue("in_id", id)
return funcGetActorName.executeFunction(String::class.java, source)
}
// ... additional methods
}
所使用的 executeFunction 方法返回一个包含函数调用返回值的 String。
返回一个ResultSet或来自的 REF 游标SimpleJdbcCall
调用返回结果集的存储过程或函数有点棘手。某些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则要求显式注册一个特定类型的 out 参数。这两种方式都需要额外的处理来遍历结果集并处理返回的行。使用 SimpleJdbcCall 时,你可以使用 returningResultSet 方法,并为特定参数声明一个 RowMapper 实现。如果结果集是在结果处理过程中返回的,则不会定义名称,因此返回的结果必须与你声明 RowMapper 实现的顺序相匹配。不过,所指定的名称仍会被用于将处理后的结果列表存储在 execute 语句返回的结果映射(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 方法中传入需要映射的目标类来创建它。
以下示例展示了如何实现这一点:
-
Java
-
Kotlin
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor::class.java))
fun getActorsList(): List<Actor> {
val m = procReadAllActors.execute(mapOf<String, Any>())
return m["actors"] as List<Actor>
}
// ... additional methods
}
execute 调用传入了一个空的 Map,因为该调用不接受任何参数。
然后从结果映射(map)中获取演员列表,并返回给调用者。