Common Problems with Parameter and Data Value Handling
Spring Framework 的 JDBC 支持提供的不同方法中存在参数和数据值的常见问题。本节介绍如何解决这些问题。
Providing SQL Type Information for Parameters
通常,Spring 根据传入的参数类型确定参数的 SQL 类型。在设置参数值时可以显式地提供要使用的 SQL 类型。有时需要这样做才能正确设置 NULL
值。
你可以通过以下几种方式提供 SQL 类型信息:
-
JdbcTemplate
的许多更新和查询方法以int
数组的形式采用附加参数。该数组用于通过使用java.sql.Types
类中的常量值指示相应参数的 SQL 类型。为每个参数提供一个条目。 -
您可以使用
SqlParameterValue
类来包装需要此附加信息的 parameter 值。请为此为每个值创建一个新实例,并在构造函数中传递 SQL 类型和参数值。您还可以为数字值提供可选的 scale 参数。 -
对于使用命名参数的方法,您可以使用
SqlParameterSource
类、BeanPropertySqlParameterSource
或MapSqlParameterSource
。它们都具有用于为任何命名参数值注册 SQL 类型的函数。
Handling BLOB and CLOB objects
你可以在数据库中存储图像、其他二进制数据和大量文本。这些大对象对于二进制数据称为 BLOB(二进制大对象),对于字符数据称为 CLOB(字符大对象)。在 Spring 中,你可以通过直接使用 JdbcTemplate
,以及使用 RDBMSObjects 和 SimpleJdbc
类提供的更高级别的抽象来处理这些大对象。所有这些方法都使用 LobHandler
接口的实现来实际管理 LOB(大对象)数据。LobHandler
通过 getLobCreator
方法提供对 LobCreator
类的访问,该类用于创建要插入的新 LOB 对象。
LobCreator
和 LobHandler
为 LOB 输入和输出提供以下支持:
-
BLOB
-
byte[]
:getBlobAsBytes
andsetBlobAsBytes
-
InputStream
:getBlobAsBinaryStream
andsetBlobAsBinaryStream
-
-
CLOB
-
String
:getClobAsString
andsetClobAsString
-
InputStream
:getClobAsAsciiStream
andsetClobAsAsciiStream
-
Reader
:getClobAsCharacterStream
andsetClobAsCharacterStream
-
下一个示例演示如何创建和插入 BLOB。稍后我们将演示如何从数据库中回读它。
此示例使用 JdbcTemplate
和 AbstractLobCreatingPreparedStatementCallback
的实现。它实现了一个方法 setValues
。此方法提供一个 LobCreator
,我们使用它为你 SQL insert 语句中的 LOB 列设置值。
对于此示例,我们假设存在一个变量 lobHandler
,该变量已预先设置为 DefaultLobHandler
的实例。你通常通过依赖注入来设置此值。
下面的示例演示如何创建和插入 BLOB:
- Java
-
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 的内容。
|
4 | 传递 lobHandler (在本例中为单纯的 DefaultLobHandler )。 |
5 | 使用 setClobAsCharacterStream 方法传递 CLOB 的内容。 |
6 | 使用 setBlobAsBinaryStream 方法传递 BLOB 的内容。 |
如果你调用 |
现在是时候从数据库中读取 LOB 数据了。同样,你使用具有相同实例变量 lobHandler
的 JdbcTemplate
和对 DefaultLobHandler
的引用。以下示例演示如何执行此操作:
- Java
-
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 方法检索 BLOB 的内容。
|
3 | 使用 getClobAsString 方法检索 CLOB 的内容。 |
4 | 用 getBlobAsBytes 方法检索 BLOB 的内容。 |
Passing in Lists of Values for IN Clause
SQL 标准允许根据包含可变值列表的表达式来选择行。一个典型的示例是 select * from T_ACTOR where id in(1, 2, 3)
。这个可变列表不受 JDBC 标准的 prepared 语句直接支持。你无法声明可变数量的占位符。你需要准备好具有所需数量的占位符的许多变体,或者在知道需要多少个占位符后动态生成 SQL 字符串。NamedParameterJdbcTemplate
中提供的命名参数支持采用后一种方法。你可以将值作为简单值的 java.util.List
(或任何 Iterable
)传递。此列表用于将所需的占位符插入到实际 SQL 语句中并在执行语句期间传递值。
在传递多个值时应小心。JDBC 标准未保证你可以对 |
除了值列表中的原生值之外,你可以创建一个对象数组的 java.util.List
。此列表可以支持为 in
子句定义多个表达式,例如 select * from T_ACTOR where (id, last_name) in 1, 'Johnson'), (2,'Harrop'
。当然,这需要你的数据库支持此语法。
Handling Complex Types for Stored Procedure Calls
当你调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,Spring 提供了一个 SqlReturnType
,用于在存储过程调用返回这些类型时对它们进行处理,以及一个 SqlTypeValue
,用于将其作为参数传递给存储过程。
SqlReturnType
接口有一个必须实现的单一方法(名为 getTypeValue
)。此接口用作 SqlOutParameter
声明的一部分。以下示例显示如何返回用户声明类型 ITEM_TYPE
的 Oracle STRUCT
对象的值:
-
Java
-
Kotlin
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;
}));
// ...
}
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {
init {
// ...
declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName ->
val struct = cs.getObject(colIndx) as STRUCT
val attr = struct.getAttributes()
TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date)
})
// ...
}
}
你可以使用 SqlTypeValue
将 Java 对象(例如 TestItem
)的值传递给存储过程。SqlTypeValue
接口有一个必须实现的单一方法(名为 createTypeValue
)。活动连接将传入,你可以使用它来创建数据库特定对象,例如 StructDescriptor
实例或 ArrayDescriptor
实例。以下示例创建一个 StructDescriptor
实例:
-
Java
-
Kotlin
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;
}
};
val (id, description, expirationDate) = TestItem(123L, "A test item",
SimpleDateFormat("yyyy-M-d").parse("2010-12-31"))
val value = object : AbstractSqlTypeValue() {
override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
val itemDescriptor = StructDescriptor(typeName, conn)
return STRUCT(itemDescriptor, conn,
arrayOf(id, description, java.sql.Date(expirationDate.time)))
}
}
你现在可以将此 SqlTypeValue
添加到包含存储过程 execute
调用的输入参数的 Map
中。
SqlTypeValue
的另一个用法是将值数组传递给 Oracle 存储过程。Oracle 具有自己的内部 ARRAY
类,在这种情况下必须使用它,你可以使用 SqlTypeValue
创建 Oracle ARRAY
的一个实例,并使用来自 Java ARRAY
的值填充它,如下例所示:
-
Java
-
Kotlin
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;
}
};
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {
init {
val ids = arrayOf(1L, 2L)
val value = object : AbstractSqlTypeValue() {
override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
val arrayDescriptor = ArrayDescriptor(typeName, conn)
return ARRAY(arrayDescriptor, conn, ids)
}
}
}
}