15.7 参数和数据处理的常见问题

Spring JDBC框架提供了多个方法来处理常见的参数和数据问题

15.7.1 为参数设置SQL的类型信息

通常Sping通过传入的参数类型决定SQL的参数类型。可以在设定参数值的时候显式提供SQL类型。有些场景下设置NULL值是有必要的。

你可以通过以下方式来设置SQL类型信息:

许多JdbcTemplate的更新和查询方法需要传入额外的int数组类型的参数。这个数组使用java.sql.Types类的常量值来确定相关参数的SQL类型。每个参数会有对应的类型项。

你可以使用SqlParameterValue类来包装需要额外信息的参数值。针对每个值创建一个新的实例,并且在构造函数中传入SQL类型和参数值。你还可以传入数值类型的可选区间参数

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

15.7.2 处理BLOB和CLOB对象

你可以存储图片,其他类型的二进制数据,和数据库里面的大块文本。这些大的对象叫做BLOBS(全称:Binary Large OBject;用于二进制数据)和CLOBS(全称:Character Large OBject;用于字符数据)。在Spring中你可以使用JdbcTemplate直接处理这些大对象,并且也可以使用RDBMS对象提供的上层抽象类,或者使用SimpleJdbc类。所有这些方法使用LobHandler接口的实现类来处理LOB数据的管理(全称:Large Object)。LobHandler通过getLobCreator方法提供了对LobCreator 类的访问,用于创建新的LOB插入对象。

LobCreator/LobHandler提供了LOB输入和输出的支持:

BLOB: 
  byte[] — getBlobAsBytes和setBlobAsBytes 
  InputStream - getBlobAsBinaryStream和setBlobAsBinaryStream 

CLOB: 
  String - getClobAsString和setClobAsString 
  InputStream - getClobAsAsciiStream和setClobAsAsciiStream 
  Reader - getClobAsCharacterStream和setClobAsCharacterStream 

下面的例子展示了如何创建和插入一个BLOB。后面的例子我们将举例如何从数据库中将BLOB数据读取出来

这个例子使用了JdbcTemplate和AbstractLobCreatingPreparedStatementCallback的实现类。它主要实现了一个方法,setValues.这个方法提供了用于在你的SQL插入语句中设置LOB列的LobCreator。

针对这个例子我们假定有一个变量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) { (1) 
        protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { 
            ps.setLong(1, 1L); 
            lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); (2) 
            lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); (3) 
        } 
    } 
); 
blobIs.close(); 
clobReader.close(); 

(1)、这里例子中传入的lobHandler 使用了默认实现类DefaultLobHandler

(2)、使用setClobAsCharacterStream传入CLOB的内容

(3)、使用setBlobAsBinaryStream传入BLOB的内容

备注:如果你调用从DefaultLobHandler.getLobCreator()返回的LobCreator的setBlobAsBinaryStream, setClobAsAsciiStream, 或者setClobAsCharacterStream方法,其中contentLength参数允许传入一个负值。如果指定的内容长度是负值,DefaultLobHandler会使用JDBC4.0不带长度参数的set-stream方法,或者直接传入驱动指定的长度;JDBC驱动对未指定长度的LOB流的支持请参见相关文档

下面是从数据库读取LOB数据的例子。我们这里再次使用JdbcTempate并使用相同的DefaultLobHandler实例。

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

1、使用getClobAsString获取CLOB的内容

2、使用getBlobAsBytes获取BLOC的内容

15.7.3 传入IN语句的列表值

SQL标准允许基于一个带参数列表的表达式进行查询,一个典型的例子是select * from T_ACTOR where id in (1, 2, 3). 这样的可变参数列表没有被JDBC标准直接支持;你不能定义可变数量的占位符(placeholder),只能定义固定变量的占位符,或者你在动态生成SQL字符串的时候需要提前知晓所需占位符的数量。NamedParameterJdbcTemplate 和 JdbcTemplate 都使用了后面那种方式。当你传入参数时,你需要传入一个java.util.List类型,支持基本类型。而这个list将会在SQL执行时替换占位符并传入参数。

备注:在传入多个值的时候需要注意。JDBC标准不保证你能在一个in表达式列表中传入超过100个值,不少数据库会超过这个值,但是一般都会有个上限。比如Oracle的上限是1000.

除了值列表的元数据值,你可以创建java.util.List的对象数组。这个列表支持多个在in语句内定义的表达式例如
select * from T_ACTOR where (id, last_name) in ((1, ‘Johnson’), (2, ‘Harrop’\))。当然有个前提你的数据库需要支持这个语法。

15.7.4 处理存储过程调用的复杂类型

当你调用存储过程时有时需要使用数据库特定的复杂类型。为了兼容这些类型,当存储过程调用返回时Spring提供了一个SqlReturnType 来处理这些类型,SqlTypeValue用于存储过程的传入参数。

下面是一个用户自定义类型ITEM_TYPE的Oracle STRUCT对象的返回值例子。SqlReturnType有一个方法getTypeValue必须被实现。而这个接口的实现将被用作SqlOutParameter声明的一部分。

final TestItem = new TestItem(123L, "A test item", 
        new SimpleDateFormat("yyyy-M-d").parse("2010-12-31")); 

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; 
        } 
    })); 

你可以使用SqlTypeValue 类往存储过程传入像TestItem那样的Java对象。你必须实现SqlTypeValue接口的createTypeValue方法。你可以使用传入的连接来创建像StructDescriptors这样的数据库指定对象。下面是相关的例子。

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中,用于执行存储过程调用。

SqlTypeValue 的另外一个用法是给Oracle的存储过程传入一个数组。Oracle内部有它自己的ARRAY类,在这些例子中一定会被使用,你可以使用SqlTypeValue来创建Oracle ARRAY的实例,并且设置到Java ARRAY类的值中。

final Long[] ids = new Long[] {1L, 2L}; 

SqlTypeValue value = new AbstractSqlTypeValue() { 
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { 
        ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn); 
        ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids); 
        return idArray; 
    } 
};