Common Problems with Parameter and Data Value Handling
在 Spring Framework 的 JDBC 支持所提供的不同方法中,存在参数和数据值的常见问题。本节介绍了如何解决这些问题
为参数提供 SQL 类型信息
Providing SQL Type Information for Parameters
通常情况下,Spring 会根据传入的参数类型来确定参数的 SQL 类型。在设置参数值时,可以明确地提供要使用的 SQL 类型。这对于正确设置 NULL 值有时是必要的。
你可以通过几种方式提供 SQL 类型信息。
JdbcTemplate 的许多更新和查询方法需要一个 int 数组形式的额外参数。这个数组通过使用
java.sql.Types
类中的常量值来表示相应参数的 SQL 类型。为每个参数提供一个条目。你可以使用 SqlParameterValue 类来包装需要这些额外信息的参数值。要做到这一点,为每个值创建一个新的实例,并在构造函数中传入 SQL 类型和参数值。你也可以为数字值提供一个可选的比例参数。
对于使用命名参数的方法,你可以使用 SqlParameterSource 类,BeanPropertySqlParameterSource 或 MapSqlParameterSource。它们都有为任何命名参数值注册 SQL 类型的方法。
处理 BLOB 和 CLOB 对象
Handling BLOB and CLOB objects
你可以在数据库中存储图像、其他二进制数据和大块的文本。这些大型对象在二进制数据中被称为 BLOB(Binary Large OBject),在字符数据中被称为 CLOB(Character Large OBject)。在 Spring 中,你可以通过直接使用 JdbcTemplate 来处理这些大对象,也可以在使用 RDBMS Objects 和 SimpleJdbc 类提供的更高的抽象时处理。所有这些方法都使用 LobHandler 接口的实现来实际管理 LOB(Large OBject)数据。 LobHandler 通过 getLobCreator 方法提供了对 LobCreator 类的访问,该类用于创建要插入的新 LOB 对象。
LobCreator 和 LobHandler 为 LOB 输入和输出提供以下支持:
- BLOB
byte[]
: getBlobAsBytes 和 setBlobAsBytesInputStream
: getBlobAsBinaryStream 和 setBlobAsBinaryStream
- CLOB
String
: getClobAsString 和 setClobAsStringInputStream
: getClobAsAsciiStream 和 setClobAsAsciiStreamReader
: getClobAsCharacterStream 和 setClobAsCharacterStream
接下来的例子显示了如何创建和插入一个 BLOB。后来我们展示了如何从数据库中读回它。
这个例子使用了一个 JdbcTemplat e和 AbstractLobCreatingPreparedStatementCallback 的实现。它实现了一个方法,setValues。这个方法提供了一个 LobCreator,我们用它来设置 SQL 插入语句中 LOB 列的值。
在这个例子中,我们假设有一个变量 lobHandler,它已经被设置为 DefaultLobHandler 的一个实例。你通常通过依赖性注入来设置这个值。
下面的例子显示了如何创建和插入一个 BLOB:
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 (?, ?, ?)",
// 传入一个 lobHandler,这里使用 DefaultLobHandler
new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
// 传入 CLOB 内容
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());
// 传入 BLOB 内容
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());
}
}
);
blobIs.close();
clobReader.close();
:::info
如果在从 DefaultLobHandler.getLobCreator()
返回的 LobCreator 上调用 setBlobAsBinaryStream、setClobAsciiStream 或 setClobAsCharacterStream 方法,可以选择为 contentLength 参数指定一个负值。如果指定的内容长度是负值,DefaultLobHandler 就会使用没有长度参数的 JDBC 4.0 变体的 set-stream 方法。否则,它将指定的长度传递给驱动程序。
请看你使用的 JDBC 驱动程序的文档,以验证它是否支持不提供内容长度的 LOB 流。 :::
现在是时候从数据库中读取 LOB 数据了。同样,你使用一个 JdbcTemplate,它有相同的实例变量 lobHandler 和一个对 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");
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");
results.put("BLOB", blobBytes);
return results;
}
});
在 IN 子句中传递数值列表
Passing in Lists of Values for IN Clause
SQL 标准允许根据表达式来选择记录,表达式包括一个可变的值列表。一个典型的例子是 select * from T_ACTOR where id in (1, 2, 3)
. JDBC 标准不直接支持准备好的语句的这种变量列表。你不能声明一个可变数量的占位符。你需要准备一些具有所需占位符数量的变体,或者你需要在知道需要多少占位符后动态地生成 SQL 字符串。NamedParameterJdbcTemplate 和 JdbcTemplate 中提供的命名参数支持采用了后一种方法。你可以将值作为原始对象的 java.util.List
传入。这个列表被用来插入所需的占位符,并在语句执行过程中传入值。
:::info 在传入许多值时要小心。JDBC 标准并不保证你可以在表达式列表中使用超过 100 个值。各种数据库都超过了这个数字,但它们通常对允许的数值有一个硬性限制。例如,Oracle 的限制是 1000。 :::
除了值列表中的原始值,你还可以创建一个 java.util.List
的对象数组。这个列表可以支持为 in 子句定义多个表达式,例如 select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop')
。当然,这需要你的数据库支持这种语法(MySQL 不支持)。
处理存储过程调用的复杂类型
Handling Complex Types for Stored Procedure Calls
当你调用存储过程时,你有时会使用数据库中的特定复杂类型。为了适应这些类型,Spring提供了一个SqlReturnType,用于处理从存储过程调用中返回的类型,以及作为参数传入存储过程的SqlTypeValue。
SqlReturnType 接口有一个必须实现的方法(名为 getTypeValue)。这个接口被用作 SqlOutParameter 的声明的一部分。下面的例子显示了返回用户声明类型 ITEM_TYPE 的 Oracle STRUCT 对象的值。
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
// ...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
(CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
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 将一个 Java 对象(如 TestItem)的值传递给一个存储过程。SqlTypeValue 接口有一个单一的方法(名为createTypeValue),你必须实现。活动连接被传递进来,你可以用它来创建数据库特定的对象,如 StructDescriptor 实例或 ArrayDescriptor 实例。下面的例子创建了一个 StructDescriptor 实例。
final TestItem testItem = new TestItem(123L, "A test item",
new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
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 添加到包含存储过程执行调用的输入参数的地图中。
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;
}
};