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输入和输出的支持:

  1. BLOB
  2. byte[] — getBlobAsBytessetBlobAsBytes
  3. InputStream - getBlobAsBinaryStreamsetBlobAsBinaryStream
  4. CLOB
  5. String - getClobAsStringsetClobAsString
  6. InputStream - getClobAsAsciiStreamsetClobAsAsciiStream
  7. Reader - getClobAsCharacterStreamsetClobAsCharacterStream

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

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

针对这个例子我们假定有一个变量lobHandler,已经设置了DefaultLobHandler的一个实例。通常你可以使用依赖注入来设置这个值。

  1. final File blobIn = new File("spring2004.jpg");
  2. final InputStream blobIs = new FileInputStream(blobIn);
  3. final File clobIn = new File("large.txt");
  4. final InputStream clobIs = new FileInputStream(clobIn);
  5. final InputStreamReader clobReader = new InputStreamReader(clobIs);
  6. jdbcTemplate.execute(
  7. "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
  8. new AbstractLobCreatingPreparedStatementCallback(lobHandler) { 1
  9. protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
  10. ps.setLong(1, 1L);
  11. lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); 2
  12. lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); 3
  13. }
  14. }
  15. );
  16. blobIs.close();
  17. 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实例。

  1. List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
  2. new RowMapper<Map<String, Object>>() {
  3. public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
  4. Map<String, Object> results = new HashMap<String, Object>();
  5. String clobText = lobHandler.getClobAsString(rs, "a_clob"); 1
  6. results.put("CLOB", clobText); byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); 2
  7. 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声明的一部分。

  1. final TestItem = new TestItem(123L, "A test item",
  2. new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
  3. declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
  4. new SqlReturnType() {
  5. public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String typeName) throws SQLException {
  6. STRUCT struct = (STRUCT) cs.getObject(colIndx);
  7. Object[] attr = struct.getAttributes();
  8. TestItem item = new TestItem();
  9. item.setId(((Number) attr[0]).longValue());
  10. item.setDescription((String) attr[1]);
  11. item.setExpirationDate((java.util.Date) attr[2]);
  12. return item;
  13. }
  14. }));

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

  1. SqlTypeValue value = new AbstractSqlTypeValue() {
  2. protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
  3. StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
  4. Struct item = new STRUCT(itemDescriptor, conn,
  5. new Object[] {
  6. testItem.getId(),
  7. testItem.getDescription(),
  8. new java.sql.Date(testItem.getExpirationDate().getTime())
  9. });
  10. return item;
  11. }
  12. };

SqlTypeValue会加入到包含输入参数的Map中,用于执行存储过程调用。

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

  1. final Long[] ids = new Long[] {1L, 2L};
  2. SqlTypeValue value = new AbstractSqlTypeValue() {
  3. protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
  4. ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
  5. ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
  6. return idArray;
  7. }
  8. };