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参数的形式返回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之外,唯一的配置选项是存储过程的名称):

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参数。在当前实例中,它们是out_first_name,out_last_name和out_birth_date。

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)基于一个名为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,如以下示例所示:

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参数的形式返回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之外,唯一的配置选项是存储过程的名称):

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参数。在当前实例中,它们是out_first_name,out_last_name和out_birth_date。

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)基于一个名为get_actor_name的存储函数,该函数返回actor的全名:

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,如以下示例所示:

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

微信公众号:
Spring 5 中文解析数据存储篇-JDBC数据存储(中)

技术交流群:
Spring 5 中文解析数据存储篇-JDBC数据存储(中)

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

上一篇:Spring 5 中文解析核心篇-集成测试之TestContext(中)


下一篇:Spring 5 中文解析核心篇-IoC容器之Spring AOP API