写点什么

Spring 5 中文解析数据存储篇 -JDBC 数据存储 (中)

用户头像
青年IT男
关注
发布于: 2020 年 09 月 27 日
Spring 5 中文解析数据存储篇-JDBC数据存储(中)
3.5 JDBC 批量操作


如果将多个调用批处理到同一条准备好的语句,则大多数 JDBC 驱动程序都会提高性能。通过将更新分组成批,可以限制到数据库的往返次数。


###### 3.5.3 使用 JdbcTemplate 的基本批处理操作


通过实现特殊接口的两个方法 BatchPreparedStatementSetter 并将该实现作为 batchUpdate 方法调用中的第二个参数传入,可以完成 JdbcTemplate 批处理。你可以使用 getBatchSize 方法提供当前批处理的大小。你可以使用 setValues 方法设置语句的参数值。此方法称为你在 getBatchSize 调用中指定的次数。以下示例根据列表中的条目更新 t_actor 表,并将整个列表用作批处理:


public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); }
public int[] batchUpdate(final List<Actor> actors) { return this.jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", new BatchPreparedStatementSetter() { public void setValues(PreparedStatement ps, int i) throws SQLException { Actor actor = actors.get(i); ps.setString(1, actor.getFirstName()); ps.setString(2, actor.getLastName()); ps.setLong(3, actor.getId().longValue()); } public int getBatchSize() { return actors.size(); } }); }
// ... additional methods}
复制代码


如果处理更新流或从文件读取,则可能具有首选的批处理大小,但最后一批可能没有该数量的条目(译者:意思是最后一批数据可能没有分割数量大)。在这种情况下,可以使用 InterruptibleBatchPreparedStatementSetter 接口,该接口可在输入源耗尽后中断批处理(译者:意思是数据源数据消耗完)。isBatchExhausted 方法使你可以发出批处理结束的信号。


###### 3.5.2 批处理操作的对象列表


JdbcTemplate 和 NamedParameterJdbcTemplate 都提供了另一种提供批处理更新的方式。无需实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值,并使用一个内部语句 setter。API 会有所不同,具体取决于你是否使用命名参数。对于命名参数,你提供一个 SqlParameterSource 数组,该批处理的每个成员都有一个条目。你可以使用 SqlParameterSourceUtils.createBatch 便捷方法创建此数组,传入一个 bean 样式的对象数组(带有与参数相对应的 getter 方法),字符串键 Map 实例(包含对应的参数作为值),或者混合使用。


以下示例显示使用命名参数的批处理更新:


public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); }
public int[] batchUpdate(List<Actor> actors) { return this.namedParameterJdbcTemplate.batchUpdate( "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", SqlParameterSourceUtils.createBatch(actors)); }
// ... additional methods}
复制代码


对于使用经典的 SQL 语句?占位符,则传入包含更新值的对象数组的列表。该对象数组在 SQL 语句中的每个占位符必须具有一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同。


以下示例与前面的示例相同,不同之处在于它使用经典的 JDBC?占位符:


public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); }
public int[] batchUpdate(final List<Actor> actors) { List<Object[]> batch = new ArrayList<Object[]>(); for (Actor actor : actors) { Object[] values = new Object[] { actor.getFirstName(), actor.getLastName(), actor.getId()}; batch.add(values); } return this.jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", batch); }
// ... additional methods}
复制代码


我们前面介绍的所有批处理更新方法都返回一个 int 数组,其中包含每个批处理条目的受影响行数。此计数由 JDBC 驱动程序报告。如果该计数不可用,则 JDBC 驱动程序将返回值-2。


在这种情况下,通过在基础 PreparedStatement 上自动设置值,需要从给定的 Java 类型派生每个值的对应 JDBC 类型。尽管这通常效果很好,但存在潜在的问题(例如,包含 Map 的空值)。在这种情况下,Spring 默认情况下会调用 ParameterMetaData.getParameterType,这对于 JDBC 驱动程序可能会很昂贵。如果遇到性能问题,则应使用最新的驱动程序版本,并考虑将 spring.jdbc.getParameterType.ignore 属性设置为 true(作为 JVM 系统属性或在类路径根目录中的 spring.properties 文件中)。如关于 Oracle 12c(SPR-16139)的报道。

>

或者,你可以考虑通过BatchPreparedStatementSetter(如前所示),通过为基于“List <Object []>的调用提供的显式类型数组,通过在服务器上的“registerSqlType调用来显式指定相应的 JDBC 类型。自定义“MapSqlParameterSource实例,或者通过BeanPropertySqlParameterSource实例从 Java 声明的属性类型中获取 SQL 类型,即使对于null值也是如此。


###### 3.5.3 具有多个批次的批次操作


前面的批处理更新示例处理的批处理太大,以至于你想将它们分解成几个较小的批处理。你可以通过多次调用 batchUpdate 方法来使用前面提到的方法来执行此操作,但是现在有一个更方便的方法。除了 SQL 语句外,此方法还包含一个对象集合,该对象包含参数,每个批处理要进行的更新次数以及一个 ParameterizedPreparedStatementSetter 来设置准备好的语句的参数值。框架遍历提供的值,并将更新调用分成指定大小的批处理。


以下示例显示了使用 100 的批量大小的批量更新:


public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); }
public int[][] batchUpdate(final Collection<Actor> actors) { int[][] updateCounts = jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", actors, 100, (PreparedStatement ps, Actor actor) -> { ps.setString(1, actor.getFirstName()); ps.setString(2, actor.getLastName()); ps.setLong(3, actor.getId().longValue()); }); return updateCounts; }
// ... additional methods}
复制代码


此调用的批处理更新方法返回一个 int 数组,该数组包含每个批处理的数组条目以及每个更新受影响的行数的数组。顶层数组的长度指示运行的批处理数量,第二层树脂的长度指示该批处理中的更新数量。 每个批次中的更新数量应该是为所有批次提供的批次大小(最后一个可能更少),这取决于所提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的更新计数。如果该计数不可用,则 JDBC 驱动程序将返回值-2。


3.6 使用 SimpleJdbc 类简化 JDBC 操作


SimpleJdbcInsert 和 SimpleJdbcCall 类通过利用可通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。这意味着你可以更少地进行前期配置,但是如果你愿意在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。


###### 3.6.1 使用 SimpleJdbcInsert 插入数据


我们首先查看具有最少配置选项的 SimpleJdbcInsert 类。你应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。对于此示例,初始化方法是 setDataSource 方法。你不需要子类化 SimpleJdbcInsert 类。而是可以创建一个新实例,并使用 withTableName 方法设置表名称。此类的配置方法遵循fluid的样式,该样式返回 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<String, Object>(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 的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的 insert 语句。


###### 3.6.2 通过使用 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<String, Object>(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。


###### 3.6.3 为 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<String, Object>(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); }
// ... additional methods}
复制代码


插入的执行与依靠元数据确定要使用的列的执行相同。


###### 3.6.4 使用 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}
复制代码


如你所见,配置是相同的。只有执行代码才能更改为使用这些替代输入类。


###### 3.6.5 使用 SimpleJdbcCall 调用存储过程


SimpleJdbcCall 类使用数据库中的元数据来查找 in 和 out 参数的名称,因此你不必显式声明它们。如果愿意,可以声明参数,也可以声明没有自动映射到 Java 类的参数(例如 ARRAY 或 STRUCT)。第一个示例显示了一个简单的过程,该过程仅从 MySQL 数据库返回 VARCHAR 和 DATE 格式的标量值。这个存储过程示例读取指定的参与者条目,并以 out 参数的形式返回 firstname,lastname 和 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 之外,唯一的配置选项是存储过程的名称):


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,该 Map 包含由存储过程中指定的名称键入的所有 out 参数。在当前实例中,它们是 outfirstname,outlastname 和 outbirthdate。


execute 方法的最后一部分创建一个 Actor 实例,以用于返回检索到的数据。同样,重要的是使用 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}
复制代码


通过执行此操作,可以避免在用于返回参数名称的情况下发生冲突。


###### 3.6.6 明确声明要用于 SimpleJdbcCall 的参数


在本章的前面,我们描述了如何从元数据推导出参数,但是如果需要,可以显式声明它们。你可以通过使用 defineParameters 方法创建和配置 SimpleJdbcCall 来实现,该方法将可变数量的 SqlParameter 对象作为输入。有关如何定义 SqlParameter 的详细信息,请参见下一部分


如果你使用的数据库不是 Spring 支持的数据库,则必须进行显式声明。当前,Spring 支持针对以下数据库的存储过程调用的元数据查找:Apache Derby,DB2,MySQL,Microsoft SQL Server,Oracle 和 Sybase。我们还支持 MySQL,Microsoft SQL Server 和 Oracle 存储方法的元数据查找。


你可以选择显式声明一个、一些或所有参数。在未显式声明参数的地方,仍使用参数元数据。要绕过对潜在参数的元数据查找的所有处理并仅使用已声明的参数,可以将不带 ProcedureColumnMetaDataAccess 的方法作为声明的一部分来调用。假设你为数据库函数声明了两个或多个不同的调用签名。在这种情况下,你调用 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}
复制代码


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


###### 3.6.7 如何定义 SqlParameters


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


new SqlParameter("in_id", Types.NUMERIC),new SqlOutParameter("out_first_name", Types.VARCHAR),
复制代码


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


第二行(带有 SqlOutParameter)声明在存储过程调用中使用的 out 参数。还有一个用于 InOut 参数的 SqlInOutParameter(为过程提供 IN 值并返回值的参数)。


仅声明为 SqlParameter 和 SqlInOutParameter 的参数用于提供输入值。这不同于 StoredProcedure 类,该类(出于向后兼容的原因)允许为声明为 SqlOutParameter 的参数提供输入值。


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


###### 3.6.8 通过使用 SimpleJdbcCall 调用存储函数


可以使用与调用存储过程几乎相同的方式来调用存储函数,除了提供函数名而不是过程名。你将 withFunctionName 方法用作配置的一部分,以指示你要对函数进行调用,并生成函数调用的相应字符串。专门调用(executeFunction)用于运行该函数,它以指定类型的对象的形式返回函数的返回值,这意味着你不必从结果 Map 检索返回值。对于只有一个 out 参数的存储过程,也可以使用类似的便捷方法(名为 executeObject)。以下示例(对于 MySQL)基于一个名为 getactorname 的存储函数,该函数返回参与者的全名:


CREATE FUNCTION get_actor_name (in_id INTEGER)RETURNS VARCHAR(200) READS SQL DATABEGIN    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 JdbcTemplate jdbcTemplate; private SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(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,其中包含函数调用的返回值。


###### 3.6.9 从 SimpleJdbcCall 返回 ResultSet 或 REF 游标


SimpleJdbcInsert 和 SimpleJdbcCall 类通过利用可通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。这意味着你可以更少地进行前期配置,但是如果你愿意在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。


###### 3.6.1 通过使用SimpleJdbcInsert插入数据


我们首先查看具有最少配置选项的 SimpleJdbcInsert 类。你应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert。对于此示例,初始化方法是 setDataSource 方法。你不需要子类化 SimpleJdbcInsert 类。而是可以创建一个新实例,并使用 withTableName 方法设置表名称。此类的配置方法遵循fluid的样式,该样式返回 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<String, Object>(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 的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的 insert 语句。


###### 3.6.2 通过使用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<String, Object>(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。


###### 3.6.3 为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<String, Object>(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); }
// ... additional methods}
复制代码


插入的执行与依靠元数据确定要使用的列的执行相同。


###### 3.6.4 使用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}
复制代码


如你所见,配置是相同的。只有执行代码才能更改为使用这些替代输入类。


###### 3.6.5 通过SimpleJdbcCall调用存储过程


SimpleJdbcCall 类使用数据库中的元数据来查找 in 和 out 参数的名称,因此你不必显式声明它们。如果愿意,可以声明参数,也可以声明没有自动映射到 Java 类的参数(例如 ARRAY 或 STRUCT)。第一个示例显示了一个简单的过程,该过程仅从 MySQL 数据库返回 VARCHAR 和 DATE 格式的标量值。示例存储过程读取指定的 actor 条目,并以 out 参数的形式返回 firstname,lastname 和 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 之外,唯一的配置选项是存储过程的名称):


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,该 Map 包含由存储过程中指定的名称键入的所有 out 参数。在当前实例中,它们是 outfirstname,outlastname 和 outbirthdate。


execute 方法的最后一部分创建一个 Actor 实例,以用于返回检索到的数据。同样,重要的是使用 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}
复制代码


通过执行此操作,可以避免在用于返回参数名称的情况下发生冲突。


###### 3.6.6 明确声明要用于 SimpleJdbcCall 的参数


在本章的前面,我们描述了如何从元数据推导出参数,但是如果需要,可以显式声明它们。你可以通过使用 defineParameters 方法创建和配置 SimpleJdbcCall 来实现,该方法将可变数量的 SqlParameter 对象作为输入。有关如何定义 SqlParameter 的详细信息,请参见下一部分


如果你使用的数据库不是 Spring 支持的数据库,则必须进行显式声明。当前,Spring 支持针对以下数据库的存储过程调用的元数据查找:Apache Derby,DB2,MySQL,Microsoft SQL Server,Oracle 和 Sybase。我们还支持 MySQL,Microsoft SQL Server 和 Oracle 存储功能的元数据查找。


你可以选择显式声明一、一些或所有参数。在未显式声明参数的地方,仍使用参数元数据。要绕过对潜在参数的元数据查找的所有处理并仅使用已声明的参数,可以将不带 ProcedureColumnMetaDataAccess 的方法作为声明的一部分来调用。假设你为数据库函数声明了两个或多个不同的调用签名。在这种情况下,你调用 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}
复制代码


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


###### 3.6.7 怎样定义SqlParameters


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


new SqlParameter("in_id", Types.NUMERIC),new SqlOutParameter("out_first_name", Types.VARCHAR),
复制代码


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


第二行(带有 SqlOutParameter)声明在存储过程调用中使用的 out 参数。还有一个用于 InOut 参数的 SqlInOutParameter(为过程提供 IN 值并返回值的参数)。


仅声明为 SqlParameter 和 SqlInOutParameter 的参数用于提供输入值。这不同于 StoredProcedure 类,该类(出于向后兼容的原因)允许为声明为 SqlOutParameter 的参数提供输入值。


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


###### 3.6.8 通过使用 SimpleJdbcCall 调用存储的函数


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


CREATE FUNCTION get_actor_name (in_id INTEGER)RETURNS VARCHAR(200) READS SQL DATABEGIN    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 JdbcTemplate jdbcTemplate; private SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(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,其中包含函数调用的返回值。


###### 3.6.9 从 SimpleJdbcCall 返回 ResultSet 或 REF 游标


调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则需要显式注册的特定类型的参数。两种方法都需要进行额外的处理才能遍历结果集并处理返回的行。通过 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 方法中传入要映射的必需类而创建的。以下示例显示了如何执行此操作:


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 中检索 actor 列表,并将其返回给调用者。


作者


个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年 IT 男 获取最新技术文章推送!


博客地址: http://youngitman.tech


CSDN: https://blog.csdn.net/liyong1028826685


微信公众号:

技术交流群:


本文由博客群发一文多发等运营工具平台 OpenWrite 发布


发布于: 2020 年 09 月 27 日阅读数: 942
用户头像

青年IT男

关注

站在巨人肩上看得更远! 2018.04.25 加入

从事金融行业,就职过易极付、思建科技、网约车平台等一流技术团队,目前就职于银行负责支付系统建设。对金融行业有强烈的爱好。实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域

评论

发布
暂无评论
Spring 5 中文解析数据存储篇-JDBC数据存储(中)