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

书籍推荐