`

Spring JDBC(二)

    博客分类:
  • jdbc
 
阅读更多
DataSourceTransactionManager类
DataSourceTransactionManager类是 PlatformTransactionManager接口的一个实现,用于处理单JDBC数据源。 它将从指定DataSource取得的JDBC连接绑定到当前线程,因此它也支持了每个数据源对应到一个线程。

我们推荐在应用代码中使用DataSourceUtils.getConnection(DataSource)来获取 JDBC连接,而不是使用J2EE标准的DataSource.getConnection。因为前者将抛出 unchecked的org.springframework.dao异常,而不是checked的 SQLException异常。Spring Framework中所有的类(比如 JdbcTemplate)都采用这种做法。如果不需要和这个 DataSourceTransactionManager类一起使用,DataSourceUtils 提供的功能跟一般的数据库连接策略没有什么两样,因此它可以在任何场景下使用。

DataSourceTransactionManager类支持定制隔离级别,以及对SQL语句查询超时的设定。 为了支持后者,应用代码必须使用JdbcTemplate或者在每次创建SQL语句时调用DataSourceUtils.applyTransactionTimeout(..)方法。

在使用单个数据源的情形下,你可以用DataSourceTransactionManager来替代JtaTransactionManager, 因为DataSourceTransactionManager不需要容器支持JTA。如果你使用DataSourceUtils.getConnection(DataSource)来获取 JDBC连接,二者之间的切换只需要更改一些配置。最后需要注意的一点就是JtaTransactionManager不支持隔离级别的定制!

11.3.8. NativeJdbcExtractor
有时我们需要执行特殊的,由特定厂商提供的与标准JDBC的API不同的JDBC方法。此时,当我们在某个应用服务器上运行包装了这些厂商各自实现的Connection, Statement和ResultSet对象的DataSource 时,可能会遇到一些问题。如果你要访问这些对象,你可以配置一个包含NativeJdbcExtractor的JdbcTemplate或者OracleLobHandler。

NativeJdbcExtractor根据执行环境的不同,会有不同的风格的实现:

SimpleNativeJdbcExtractor

C3P0NativeJdbcExtractor

CommonsDbcpNativeJdbcExtractor

JBossNativeJdbcExtractor

WebLogicNativeJdbcExtractor

WebSphereNativeJdbcExtractor

XAPoolNativeJdbcExtractor

通常来说SimpleNativeJdbcExtractor类对于绝大多数环境,已经足以屏蔽 Connection 对象。可以参见Java Docs获取详细信息。

11.4. JDBC批量操作
绝大多数JDBC驱动针对批量调用相同的prepared statement对象提供了性能提升。通过将这些更新操作封装到一个批量操作中,可以大量减少与数据库的操作频繁度。 本章节将详细描述使用JdbcTemplate或者SimpleJdbcTemplate进行批量操作的流程。

11.4.1. 使用JdbcTemplate进行批量操作
JdbcTemplate的批量操作特性需要实现特定的接口BatchPreparedStatementSetter来进行的, 通过实现这个接口,并将其传入batchUpdate方法进行调用。 这个接口有两个方法需要实现。一个叫做getBatchSize来提供当前需要批量操作的数量。另外一个方法是setValues 允许你为prepared statement设置参数。这个方法将在整个过程中被调用的次数,则取决于你在getBatchSize中所指定的大小。 下面的示例展示了根据传入的list参数更新actor表,而传入的list同时作为批量操作的参数。


public class JdbcActorDao implements ActorDao {
    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int[] batchUpdate(final List actors) {
        int[] updateCounts = jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                new BatchPreparedStatementSetter() {
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, ((Actor)actors.get(i)).getFirstName());
                        ps.setString(2, ((Actor)actors.get(i)).getLastName());
                        ps.setLong(3, ((Actor)actors.get(i)).getId().longValue());
                    }

                    public int getBatchSize() {
                        return actors.size();
                    }
                } );
        return updateCounts;
    }

    //  ... additional methods
}
如果你是通过读取文件进行批量操作,那么你可能需要一个特定的批量操作的数量,不过最后一次的批量操作,你可能没有那么多数量的记录。 在这种情况下,你可以实现InterruptibleBatchPreparedStatementSetter接口,从而允许你在某些情况中断批量操作,isBatchExhausted 方法允许你指定一个终止批量操作的信号量。

11.4.2. 使用SimpleJdbcTemplate进行批量操作
SimpleJdbcTemplate类提供了另外一种批量操作的方式。无需实现一个特定的接口,你只需要提供所有在调用过程中要用到的参数,框架会遍历这些参数值,并使用内置的prepared statement类进行批量操作。API将根据你是否使用命名参数而有所不同。对于使用命名参数的情况,你需要提供一个SqlParameterSource的数组, 其中的每个元素将将作为批量操作的参数。 你可以使用SqlParameterSource.createBatch方法,通过传入一个JavaBean的数组或者一个包含了参数键值对的Map数组来创建这个数组。

下面的示例展示了使用命名参数进行批量更新的方法:


public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }

    public int[] batchUpdate(final List<Actor> actors) {
        SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(actors.toArray());
        int[] updateCounts = simpleJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                batch);
        return updateCounts;
    }

    //  ... additional methods
}
对于使用传统的“?”作为参数占位符的情况,你可以传入一个List,包含了所有需要进行批量更新的对象。这样的对象数组必须与每个SQL Statement的占位符以及他们在SQL Statement中出现的位置一一对应。

下面是同样的例子,使用的传统的“?”作为参数占位符:


public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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);
        }
        int[] updateCounts = simpleJdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                batch);
        return updateCounts;
    }

    //  ... additional methods
}
所有的批量更新的方法都会返回一组int的数组,表示在整个操作过程中有多少记录被批量更新。 这个数量是由JDBC驱动所返回的,有时这个返回并不可靠,尤其对于某些JDBC驱动只是简单的返回-2作为返回值。

11.5. 通过使用SimpleJdbc类简化JDBC操作
SimpleJdbcInsert类和SimpleJdbcCall类主要利用了JDBC驱动所提供的数据库元数据的一些特性来简化数据库操作配置。 这意味着你可以尽可能的简化你的数据库操作配置。当然,你可以可以将元数据处理的特性关闭,从而在你的代码中详细指定这些特性。

11.5.1. 使用SimpleJdbcInsert插入数据
让我们从SimpleJdbcInsert类开始。我们将尽可能使用最少量的配置。SimpleJdbcInsert类必须在数据访问层的初始化方法中被初始化。 在这个例子中,初始化方法为setDataSource方法。你无需继承自SimpleJdbcInsert 类,只需要创建一个新的实例,并通过withTableName方法设置table名称。 这个类使用了“fluid”模式返回SimpleJdbcInsert,从而你可以访问到所有的可以进行配置的方法。在这个例子中,我们只使用了一个方法,稍后我们会看到更多的配置方法。

public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}
这个方法通过接收 java.utils.Map 作为它唯一的参数。 在这里需要重点注意的是,在Map中的所有的key,必须和数据库中定义的列名完全匹配。这是因为我们是通过读取元数据来构造实际的Insert语句的。

11.5.2. 使用SimpleJdbcInsert来获取自动生成的主键
接下来,我们对于同样的插入语句,我们并不传入id,而是通过数据库自动获取主键的方式来创建新的Actor对象并插入数据库。 当我们创建SimpleJdbcInsert实例时, 我们不仅需要指定表名,同时我们通过usingGeneratedKeyColumns方法指定需要数据库自动生成主键的列名。


public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}
这样我们可以看到与之前执行的insert操作的不同之处在于,我们无需添加id到参数的Map中去,只需要调用executeReturningKey方法。 这个方法将返回一个java.lang.Number对象,我们可以使用这个对象来创建一个字符类型的实例作为我们的域模型的属性。 有一点很重要的地方需要注意,我们无法依赖所有的数据库来返回我们指定的Java类型,因为我们只知道这些对象的基类是java.lang.Number。 如果你有联合主键或者那些非数字类型的主键需要生成,那么你可以使用executeReturningKeyHolder方法返回的KeyHolder对象。

11.5.3. 指定SimpleJdbcInsert所使用的字段
通过指定所使用的字段名称,可以使SimpleJdbcInsert仅使用这些字段作为insert语句所使用的字段。这可以通过usingColumns方法进行配置。


public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}
执行这样的insert语句所使用的字段,与之前我们所依赖的数据库元数据是一致的。

11.5.4. 使用SqlParameterSource提供参数值
使用Map来指定参数值有时候工作得非常好,但是这并不是最简单的使用方式。Spring提供了一些其他的SqlParameterSource实现类来指定参数值。 我们首先可以看看BeanPropertySqlParameterSource类,这是一个非常简便的指定参数的实现类,只要你有一个符合JavaBean规范的类就行了。它将使用其中的getter方法来获取参数值。 下面是一个例子:


public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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 SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}
正如你看到的,配置是一样的,区别只是切换了不同的提供参数的实现方式来执行调用。

11.5.5. 使用SimpleJdbcCall调用存储过程
接下来我们把我们的关注点放在使用 SimpleJdbcCall 来进行存储过程的调用上。 设计这个类的目的在于使得调用存储过程尽可能简单。它同样利用的数据库元数据的特性来查找传入的参数和返回值。 这意味着你无需明确声明那些参数。当然,如果你喜欢,可以依然声明这些参数,尤其对于某些参数,你无法直接将他们映射到Java类上,例如ARRAY类型和STRUCT类型的参数。 在我们的第一个示例中,我们可以看到一个简单的存储过程调用,它仅仅返回VARCHAR和DATE类型。 这里,我特地为Actor类增加了一个birthDate的属性,从而可以使得返回值拥有不同的数据类型。 这个存储过程读取actor的主键,并返回first_name,last_name,和birth_date字段作为返回值。 下面是这个存储过程的源码,它可以工作在MySQL数据库上:


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的主键,剩下的参数是作为读取数据库表中的数据所返回的返回值。

SimpleJdbcCall的声明与SimpleJdbcInsert类似,你无需继承这个类,而只需要在初始化方法中进行初始化。 在这里例子中,我们只需要指定存储过程的名称。


public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}
通过SimpleJdbcCall执行存储过程需要创建一个SqlParameterSource的实现类来指定传入的参数。 需要注意的是,传入参数的名称与存储过程中定义的名称必须保持一致。这里,我们无需保持一致,因为我们使用数据库的元数据信息来决定我们需要什么样的数据库对象。 当然,你在源代码中所指定的名称可能和数据库中完全不同,有的数据库会把这些名称全部转化成大写,而有些数据库会把这些名称转化为小写。

execute方法接收传入的参数,并返回一个Map作为返回值,这个Map包含所有在存储过程中指定的参数名称作为key。 在这个例子中,他们分别是out_first_name,out_last_name和 out_birth_date。

execute方法的最后部分是使用存储过程所返回的值创建一个新的Actor实例。 同样的,这里我们将名称与存储过程中定义的名称保持一致非常重要。在这个例子中,在返回的Map中所定义的key值和数据库的存储过程中定义的值一致。 你可能需要在这里指定Spring使用Jakarta Commons提供的CaseInsensitiveMap。这样做,你需要在创建你自己的JdbcTemplate类时,设置setResultsMapCaseInsensitive属性为True。 然后,你将这个自定义的JdbcTemplate传入SimpleJdbcCall的构造函数。当然,你需要把commons-collections.jar加入到classpath中去。 下面是配置示例:


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
}
通过这样的配置,你就可以无需担心返回参数值的大小写问题。

11.5.6. 声明SimpleJdbcCall使用的参数
你已经看到如何通过元数据来简化参数配置,但是你也可以明确地指定这些参数。可以在创建SimpleJdbcCall时,通过使用declareParameters方法来声明参数。 这个方法接收一组SqlParameter对象作为参数。我们可以参照下一个章节,如何创建SqlParameter。

我们可以有选择性的显示声明一个、多个、甚至所有的参数。参数元数据在这里会被同时使用。 通过调用withoutProcedureColumnMetaDataAccess方法,我们可以指定数据库忽略所有的元数据处理并使用显示声明的参数。 另外一种情况是,其中的某些参数值具有默认的返回值,我们需要在返回值中指定这些返回值。为了实现这个特性,我们可以使用useInParameterNames来指定一组需要被包含的参数名称。

这是一个完整的声明存储过程调用的例子:


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
}
执行和最终的返回处理是相同的,我们在这里只是明确声明了参数类型,而不是依赖数据库元数据特性。 这一点很重要,尤其对于那些数据库并不支持元数据的情况。当前,我们支持元数据的特性的数据包含:Apache Derby、DB2、MySQL、 Microsoft SQL Server、Oracle和Sybase。我们同时对某些数据库内置函数支持元数据特性:MySQL、Microsoft SQL Server和Oracle。

11.5.7. 如何定义SqlParameters
为SimpleJdbc类或者后续章节提到的RDBMS操作指定参数,你需要使用SqlParameter或者他的子类。 你可以通过指定参数名称以及对应的SQL类型并传入构造函数作为参数来指定SqlParameter,其中,SQL类型是java.sql.Types中所定义的常量。 我们已经看到过类似的声明:


   new SqlParameter("in_id", Types.NUMERIC),
   new SqlOutParameter("out_first_name", Types.VARCHAR),

第一行的SqlParameter定义了一个传入参数。传入参数可以在所有的存储过程中使用,也可以在稍后章节中提到的SqlQuery类及其子类中使用。

第二行SqlOutParameter定义了一个返回值。它可以被存储过程调用所使用。当然,还有一个SqlInOutParameter类,可以用于输入输出参数。 也就是说,它既是一个传入参数,也是一个返回值。

除了参数名称和SQL类型,你还可以声明一些其他额外的选项。对于传入参数,你可以为numeric数据类型指定精度,或者对于特定的数据库指定特殊类型。 对于返回值,你可以提供一个RowMapper接口来处理所有从REF cursor返回的列。另外一个选项是指定一个SqlReturnType类,从而可以定制返回值的处理方式。

11.5.8. 使用SimpleJdbcCall调用内置函数
内置函数的调用几乎和存储过程的调用是一样的。唯一的不同在于,你需要声明的是一个函数的名称而不是存储过程的名称。 这可以通过withFunctionName方法来完成。使用这个方法,表明你的调用是一个函数。你所指定的这个函数名称将被作为调用对象。 同时有一个叫做executeFunction的方法,将获得特定的Java类型的函数调用的返回值。 此时,你无需通过返回的Map来获取返回值。另外有一个类似的便捷方法executeObject用于存储过程,但是他只能处理单个返回值的情况。 下面的示例展示了一个叫做get_actor_name 的函数调用,返回actor的完整的名称。 这个函数将工作在MySQL数据库上。


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 SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcCall funcGetActorName;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(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
}
被调用的函数返回一个String类型。

11.5.9. 使用SimpleJdbcCall返回的ResultSet/REF Cursor
期望通过调用存储过程或者函数来返回ResultSet一直是一个问题。一些数据库在JDBC结果处理中返回结果集,而另外一些数据库则需要明确指定返回值的类型。 无论哪种方法,都需要在循环遍历结果集时,做出一些额外的工作,从而处理每一条记录。 通过SimpleJdbcCall,你可以使用returningResultSet方法,并定义一个RowMapper的实现类来处理特定的返回值。 当结果集在返回结果处理过程中没有被定义名称时,返回的结果集必须与定义的RowMapper的实现类指定的顺序保持一致。 而指定的名字也会被用作返回结果集中的名称。

在这个例子中,我们将使用一个存储过程,它并不接收任何参数,返回t_actor表中的所有的行,下面是MySQL数据库中的存储过程源码:

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的规范,所以我们可以使用ParameterizedBeanPropertyRowMapper作为实现类。 通过将相应的class类作为参数传入到newInstance方法中,我们可以创建这个实现类。


public class JdbcActorDao implements ActorDao {
    private SimpleJdbcTemplate simpleJdbcTemplate;
    private SimpleJdbcCall procReadAllActors;

    public void setDataSource(DataSource dataSource) {
        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadAllActors =
                new SimpleJdbcCall(jdbcTemplate)
                        .withProcedureName("read_all_actors")
                        .returningResultSet("actors",
                                ParameterizedBeanPropertyRowMapper.newInstance(Actor.class));
    }

    public List getActorsList() {
        Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
        return (List) m.get("actors");
    }

    //  ... additional methods
}
这个函数调用传入一个空的Map进入,因为这里不需要任何的参数传入。而函数调用所返回的结果集将返回的是Actors列表。

11.6. 用Java对象来表达JDBC操作
org.springframework.jdbc.object包下的类允许用户以更加 面向对象的方式去访问数据库。比如说,用户可以执行查询并返回一个list, 该list作为一个结果集将把从数据库中取出的列数据映射到业务对象的属性上。 用户也可以执行存储过程,以及运行更新、删除以及插入SQL语句。

Note
在许多Spring开发人员中间存在有一种观点,那就是下面将要提到的各种RDBMS操作类 (StoredProcedure类除外) 通常也可以直接使用JdbcTemplate相关的方法来替换。 相对于把一个查询操作封装成一个类而言,直接调用JdbcTemplate方法将更简单而且更容易理解。

必须强调的一点是,这仅仅只是一种观点而已, 如果你认为你可以从直接使用RDBMS操作类中获取一些额外的好处,你不妨根据自己的需要和喜好进行不同的选择。


11.6.1. SqlQuery类
SqlQuery是一个可重用、线程安全的类,它封装了一个SQL查询。 其子类必须实现newResultReader()方法,该方法用来在遍历 ResultSet的时候能使用一个类来保存结果。 我们很少需要直接使用SqlQuery,因为其子类 MappingSqlQuery作为一个更加易用的实现能够将结果集中的行映射为Java对象。 SqlQuery还有另外两个扩展分别是 MappingSqlQueryWithParameters和UpdatableSqlQuery。

11.6.2. MappingSqlQuery类
MappingSqlQuery是一个可重用的查询抽象类,其具体类必须实现 mapRow(ResultSet, int)抽象方法来将结果集中的每一行转换成Java对象。 下面这个例子演示了一个定制查询,它将从客户表中取得的数据映射到一个Customer类实例。

private class CustomerMappingQuery extends MappingSqlQuery {

    public CustomerMappingQuery(DataSource ds) {
        super(ds, "SELECT id, name FROM customer WHERE id = ?");
        super.declareParameter(new SqlParameter("id", Types.INTEGER));
        compile();
    }

    public Object mapRow(ResultSet rs, int rowNumber) throws SQLException {
        Customer cust = new Customer();
        cust.setId((Integer) rs.getObject("id"));
        cust.setName(rs.getString("name"));
        return cust;
    }
}
在上面的例子中,我们为用户查询提供了一个构造函数并为构造函数传递了一个 DataSource参数。在构造函数里面我们把 DataSource和一个用来返回查询结果的SQL语句作为参数 调用父类的构造函数。SQL语句将被用于生成一个PreparedStatement对象, 因此它可以包含占位符来传递参数。而每一个SQL语句的参数必须通过调用 declareParameter方法来进行声明,该方法需要一个 SqlParameter(封装了一个字段名字和一个 java.sql.Types中定义的JDBC类型)对象作为参数。 所有参数定义完之后,我们调用compile()方法来对SQL语句进行预编译。

public Customer getCustomer(Integer id) {
    CustomerMappingQuery custQry = new CustomerMappingQuery(dataSource);
    Object[] parms = new Object[1];
    parms[0] = id;
    List customers = custQry.execute(parms);
    if (customers.size() > 0) {
        return (Customer) customers.get(0);
    }
    else {
        return null;
    }
}
在上面的例子中,getCustomer方法通过传递惟一参数id来返回一个客户对象。 该方法内部在创建CustomerMappingQuery实例之后, 我们创建了一个对象数组用来包含要传递的查询参数。这里我们只有唯一的一个 Integer参数。执行CustomerMappingQuery的 execute方法之后,我们得到了一个List,该List中包含一个 Customer对象,如果有对象满足查询条件的话。

11.6.3. SqlUpdate类
SqlUpdate类封装了一个可重复使用的SQL更新操作。 跟所有RdbmsOperation类一样,SqlUpdate可以在SQL中定义参数。 该类提供了一系列update()方法,就像SqlQuery提供的一系列execute()方法一样。 SqlUpdate是一个具体的类。通过在SQL语句中定义参数,这个类可以支持不同的更新方法,我们一般不需要通过继承来实现定制。

import java.sql.Types;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

    public UpdateCreditRating(DataSource ds) {
        setDataSource(ds);
        setSql("update customer set credit_rating = ? where id = ?");
        declareParameter(new SqlParameter(Types.NUMERIC));
        declareParameter(new SqlParameter(Types.NUMERIC));
        compile();
    }

    /**
     * @param id for the Customer to be updated
     * @param rating the new value for credit rating
     * @return number of rows updated
     */
    public int run(int id, int rating) {
        Object[] params =
            new Object[] {
                new Integer(rating),
                new Integer(id)};
        return update(params);
    }
}
11.6.4. StoredProcedure类
StoredProcedure类是一个抽象基类,它是对RDBMS存储过程的一种抽象。 该类提供了多种execute(..)方法,不过这些方法的访问类型都是protected的。

从父类继承的sql属性用来指定RDBMS存储过程的名字。 尽管该类提供了许多必须在JDBC3.0下使用的功能,但是我们更关注的是JDBC 3.0中引入的命名参数特性。

下面的程序演示了如何调用Oracle中的sysdate()函数。 这里我们创建了一个继承StoredProcedure的子类,虽然它没有输入参数, 但是我必须通过使用SqlOutParameter来声明一个日期类型的输出参数。 execute()方法将返回一个map,map中的每个entry是一个用参数名作key,以输出参数为value的名值对。

import java.sql.Types;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.datasource.*;
import org.springframework.jdbc.object.StoredProcedure;

public class TestStoredProcedure {

    public static void main(String[] args)  {
        TestStoredProcedure t = new TestStoredProcedure();
        t.test();
        System.out.println("Done!");
    }
   
    void test() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("oracle.jdbc.OracleDriver");
        ds.setUrl("jdbc:oracle:thin:@localhost:1521:mydb");
        ds.setUsername("scott");
        ds.setPassword("tiger");

        MyStoredProcedure sproc = new MyStoredProcedure(ds);
        Map results = sproc.execute();
        printMap(results);
    }

    private class MyStoredProcedure extends StoredProcedure {
       
        private static final String SQL = "sysdate";

        public MyStoredProcedure(DataSource ds) {
            setDataSource(ds);
            setFunction(true);
            setSql(SQL);
            declareParameter(new SqlOutParameter("date", Types.DATE));
            compile();
        }

        public Map execute() {
            // the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
            return execute(new HashMap());
        }
    }

    private static void printMap(Map results) {
        for (Iterator it = results.entrySet().iterator(); it.hasNext(); ) {
            System.out.println(it.next()); 
        }
    }
}
下面是StoredProcedure的另一个例子,它使用了两个Oracle游标类型的输出参数。

import oracle.jdbc.driver.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "AllTitlesAndGenres";

    public TitlesAndGenresStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
        compile();
    }

    public Map execute() {
        // again, this sproc has no input parameters, so an empty Map is supplied...
        return super.execute(new HashMap());
    }
}
值得注意的是TitlesAndGenresStoredProcedure构造函数中 declareParameter(..)的SqlOutParameter参数, 该参数使用了RowMapper接口的实现。这是一种非常方便而强大的重用方式。 下面我们来看一下RowMapper的两个具体实现。

首先是TitleMapper类,它简单的把ResultSet中的每一行映射为一个TitleDomain Object。

import com.foo.sprocs.domain.Title;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

public final class TitleMapper implements RowMapper {
   
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        Title title = new Title();
        title.setId(rs.getLong("id"));
        title.setName(rs.getString("name"));
        return title;
    }
}
另一个是GenreMapper类,也是非常简单的将ResultSet中的每一行映射为一个GenreDomain Object。

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.foo.domain.Genre;

public final class GenreMapper implements RowMapper {
   
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}
如果你需要给存储过程传输入参数(这些输入参数是在RDBMS存储过程中定义好了的), 则需要提供一个指定类型的execute(..)方法, 该方法将调用基类的protected execute(Map parameters)方法。例如:

import oracle.jdbc.driver.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "TitlesAfterDate";
    private static final String CUTOFF_DATE_PARAM = "cutoffDate";

    public TitlesAfterDateStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        compile();
    }

    public Map execute(Date cutoffDate) {
        Map inputs = new HashMap();
        inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
        return super.execute(inputs);
    }
}
11.6.5. SqlFunction类
SqlFunction RDBMS操作类封装了一个SQL“函数”包装器(wrapper), 该包装器适用于查询并返回一个单行结果集。默认返回的是一个int值, 不过我们可以采用类似JdbcTemplate中的queryForXxx 做法自己实现来返回其它类型。SqlFunction优势在于我们不必创建 JdbcTemplate,这些它都在内部替我们做了。

该类的主要用途是调用SQL函数来返回一个单值的结果集,比如类似“select user()”、 “select sysdate from dual”的查询。如果需要调用更复杂的存储函数, (可以为这种类型的处理使用StoredProcedure或SqlCall)。

SqlFunction是一个具体类,通常我们不需要它的子类。 其用法是创建该类的实例,然后声明SQL语句以及参数就可以调用相关的run方法去多次执行函数。 下面的例子用来返回指定表的记录行数:

public int countRows() {
    SqlFunction sf = new SqlFunction(dataSource, "select count(*) from mytable");
    sf.compile();
    return sf.run();
}
11.7. 参数和数据处理的基本原则
在Spring的JDBC框架的所有工作模式中贯彻了一些与参数和数据处理相关的基本原则。

11.7.1. 为参数设置SQL类型信息
多数情况下,Spring会根据传入的参数值来设定相应的SQL类型。有时,我们有必要明确指定传入参数所代表的SQL类型,这一点对于正确设置NULL值的时候可能比较有用。

另外还有一些其他的不同方面的作用:

多数JdbcTemplate的update或者query方法会接收一个额外的int数组构成的参数。 这个数组需要提供的是使用java.sql.Types中所定义的SQL类型。而这个数组中定义的类型需要与每个传入的参数所对应。

你可以使用SqlParameterValue对参数进行额外的封装从而包装更多的参数信息。通过传入参数值和对应的SQL类型作为构造函数的参数,你可以创建这个类的一个实例。 你也可以为numeric的值提供一些额外的精度要求。

对于那些使用命名参数的情况,你可以使用SqlParameterSource、BeanPropertySqlParameterSource或者MapSqlParameterSource类。 他们都具备了为命名参数注册SQL类型的功能。

11.7.2. 处理BLOB 和 CLOB对象
你可以在数据库中存储图像、二进制对象或者大文本等对象。这些较大的二进制对象被称之为BLOB类型,而对应的大文本对象被称之为CLOB对象。 Spring允许你使用JdbcTemplate、更高层次封装的RDBMS对象和SimpleJdbc类对这些大对象进行操作。 所有的这些操作方式都实现了LobHandler接口来处理LOB类型的数据。 LobHandler接口提供了访问LobCreator的方法,通过调用getLobCreator,你可以创建一个新的LOB类型的数据。

LobCreator/LobHandler接口针对LOB类型的数据操作提供了下列支持:


BLOB

byte[] – getBlobAsBytes and setBlobAsBytes

byte[] – getBlobAsBytes 和 setBlobAsBytes

InputStream – getBlobAsBinaryStream and setBlobAsBinaryStream

InputStream – getBlobAsBinaryStream和setBlobAsBinaryStream

CLOB

String – getClobAsString and setClobAsString

String – getClobAsString和setClobAsString

InputStream – getClobAsAsciiStream and setClobAsAsciiStream

InputStream – getClobAsAsciiStream和setClobAsAsciiStream

Reader – getClobAsCharacterStream and setClobAsCharacterStream

Reader – getClobAsCharacterStream和setClobAsCharacterStream


现在我们通过一个示例来展示如何创建一个BLOB数据并插入数据库。稍后的例子,我们将展示如何从数据库中将BLOB数据读取出来。

这个例子使用JdbcTemplate和一个AbstractLobCreatingPreparedStatementCallback的实现类。 这里唯一需要实现的方法就是"setValues"。在这个方法中,将提供一个LobCreator接口,被用作在你的插入语句中设置LOB字段的值。

我们假设有一个变量叫做“lobHandler”已经被设置到DefaultLobHandler的实例中。当然,这是由注入完成的。

final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
  "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
  new AbstractLobCreatingPreparedStatementCallback(lobhandler) {                        
      protected void setValues(PreparedStatement ps, LobCreator lobCreator)
          throws SQLException {
        ps.setLong(1, 1L);
        lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());   
        lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());          
      }
  }
);
blobIs.close();
clobReader.close();
我们在这里使用的lobHandler实现类是一个普通的DefaultLobHandler

使用setClobAsCharacterStream,我们传入CLOB的内容

使用setBlobAsBinartStream,我们传入BLOB的内容


现在我们来示范从数据库中读取LOB数据。我们这里再次使用JdbcTempate并使用相同的DefaultLobHandler实例。

List l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
    new RowMapper() {
      public Object mapRow(ResultSet rs, int i) throws SQLException {
        Map results = new HashMap();
        String clobText = lobHandler.getClobAsString(rs, "a_clob");                     
        results.put("CLOB", clobText);
        byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");                     
        results.put("BLOB", blobBytes);
        return results;
      }
    });

使用getClobAsString 获取CLOB内容

使用getBlobAsBytes获取BLOB内容


11.7.3. 在IN语句中传入一组参数值
SQL标准允许基于一个带参数列表的表达式进行查询。一个典型的例子可能像这样:"select * from T_ACTOR where id in (1, 2, 3)"。 不过这种参数列表的方式并不能直接被JDBC标准所支持 - 因为并不存在这种声明一个列表参数作为占位符的方式。 你不得不为此写多个占位符来表示多个参数,或者当你知道占位符的数量时,你可以动态构建SQL字符串。 NamedParameterJdbcTemplate和SimpleJdbcTemplate中所提供的命名参数的特性,采用的是后面一种做法。 当你传入参数时,你需要传入一个java.util.List类型,支持基本类型。而这个list将会在SQL执行时替换占位符并传入参数。

Note
在使用IN语句时,当你传入大批量的值时要小心,JDBC标准并不确保超过100个元素在IN语句中。 有不少数据库可以超出这个值的限制,但是不同的数据库会有不同的数量限制,比如Oracle的限制数量是1000个。


除了基本类型之外,你还可以创建一个java.util.List的对象数组,这可以让你支持在IN表达式中编写多重表达式,例如"select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))". 当然,这样做的前提是数据库底层的语法支持。

11.7.4. 处理复杂类型的存储过程调用
当调用存储过程时,有时需要使用数据库特定的复杂类型。为了适应这些类型,Spring提供了SqlReturnType类来处理存储过程的返回值,而使用SqlTypeValue来处理传入的参数。

下面这个例子,将Oracle的STRUCT对象作为返回值,这是一个由用户自定义的“ITEM_TYPE”。 SqlReturnType接口有唯一的方法“getTypeValue”需要被实现。而这个接口的实现将被用作SqlOutParameter声明的一部分。


declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
    new SqlReturnType() {
      public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String typeName)
          throws SQLException {
        STRUCT struct = (STRUCT)cs.getObject(colIndx);
        Object[] attr = struct.getAttributes();
        TestItem item = new TestItem();
        item.setId(((Number) attr[0]).longValue());
        item.setDescription((String)attr[1]);
        item.setExpirationDate((java.util.Date)attr[2]);
        return item;
      }
    }));
通过Java代码调用存储过程使用SqlTypeValue来传入一个TestItem作为参数。 SqlTypeValue接口有一个方法"createTypeValue"需要被实现。 一个活动的数据库连接也同时被传入,它将被用作创建数据库特定的对象,类似StructDescriptor和ArrayDescriptor


SqlTypeValue value = new AbstractSqlTypeValue() {
  protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
    StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
    Struct item = new STRUCT(itemDescriptor, conn,
        new Object[] {
            testItem.getId(),
            testItem.getDescription(),
            new java.sql.Date(testItem.getExpirationDate().getTime())
        });
    return item;
  }
};
这里的SqlTypeValue 现在可以被加入到作为参数的Map中去,从而可以执行相应的存储过程。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics