使用 SimpleJdbc
类简化 JDBC 操作
SimpleJdbcInsert
和 SimpleJdbcCall
类通过利用可通过 JDBC 驱动程序检索的数据库元数据,提供简化的配置。这意味着您在前期需要配置的内容更少,尽管如果您愿意在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。
使用 SimpleJdbcInsert
插入数据
我们首先使用最少配置选项查看 SimpleJdbcInsert
类。您应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert
。对于此示例,初始化方法是 setDataSource
方法。您不需要对 SimpleJdbcInsert
类进行子类化。相反,您可以创建一个新实例,并使用 withTableName
方法设置表名。此类的配置方法遵循返回 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
是您可以依赖的基本类。如果您有多个自动生成列或生成的值是非数字的,则可以使用从 executeAndReturnKeyHolder
方法返回的 KeyHolder
。
为 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
参数的名称,这样您不必显式声明它们。如果您愿意,或者如果您有无法自动映射到 Java 类的参数(例如 ARRAY
或 STRUCT
),则可以声明参数。第一个示例显示了一个简单的过程,它仅从 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
参数返回从表中读取的数据。
您可以以类似于声明 SimpleJdbcInsert
的方式声明 SimpleJdbcCall
。您应该在数据访问层的初始化方法中实例化并配置该类。与 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
的参数
本章前面,我们描述了如何从元数据中推导出参数,但如果您愿意,可以显式声明它们。您可以通过使用 declareParameters
方法创建和配置 SimpleJdbcCall
来执行此操作,该方法将可变数量的 SqlParameter
对象作为输入。有关如何定义 SqlParameter
的详细信息,请参阅下一节。
如果您使用的数据库不是 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
参数,用于存储过程调用。InOut
参数(为过程提供 IN 值且也返回一个值的参数)也有一个 SqlInOutParameter
。
只有声明为 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
-
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
。
从 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 规则,所以您可以使用 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
,因为此调用不采用任何参数。然后从结果映射中检索演员列表并返回给调用者。