参数和数据值处理的常见问题
Spring 框架的 JDBC 支持提供了多种方法,但参数和数据值方面存在一些常见问题。本节介绍如何解决这些问题。
为参数提供 SQL 类型信息
通常,Spring 会根据传入参数的类型来确定参数的 SQL 类型。但是,在设置参数值时,可以显式提供要使用的 SQL 类型。这在某些情况下是必要的,以便正确设置 NULL
值。
可以通过几种方式提供 SQL 类型信息
-
JdbcTemplate
的许多更新和查询方法都接受一个额外的参数,该参数以int
数组的形式提供。此数组用于使用java.sql.Types
类中的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。 -
可以使用
SqlParameterValue
类来包装需要此额外信息的 parameter value。为此,为每个值创建一个新实例,并在构造函数中传入 SQL 类型和 parameter value。还可以为数值提供可选的 scale 参数。 -
对于使用命名参数的方法,可以使用
SqlParameterSource
类,例如BeanPropertySqlParameterSource
或MapSqlParameterSource
。它们都具有用于注册任何命名 parameter value 的 SQL 类型的方法。
处理 BLOB 和 CLOB 对象
您可以在数据库中存储图像、其他二进制数据和大量文本。这些大型对象对于二进制数据称为 BLOB(二进制大型对象),对于字符数据称为 CLOB(字符大型对象)。在 Spring 中,您可以使用 `JdbcTemplate` 直接处理这些大型对象,也可以在使用 RDBMS 对象和 `SimpleJdbc` 类提供的更高抽象时处理这些大型对象。所有这些方法都使用 `LobHandler` 接口的实现来实际管理 LOB(大型对象)数据。`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。稍后我们将展示如何从数据库中读取它。
此示例使用 `JdbcTemplate` 和 `AbstractLobCreatingPreparedStatementCallback` 的实现。它实现了一个方法,`setValues`。此方法提供一个 `LobCreator`,我们使用它来设置 SQL 插入语句中 LOB 列的值。
在此示例中,我们假设存在一个变量 `lobHandler`,它已设置为 `DefaultLobHandler` 实例。您通常通过依赖注入设置此值。
以下示例展示了如何创建和插入 BLOB
-
Java
-
Kotlin
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 的内容。 |
val blobIn = File("spring2004.jpg")
val blobIs = FileInputStream(blobIn)
val clobIn = File("large.txt")
val clobIs = FileInputStream(clobIn)
val clobReader = InputStreamReader(clobIs)
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
object: AbstractLobCreatingPreparedStatementCallback(lobHandler) { (1)
override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) {
ps.setLong(1, 1L)
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt()) (2)
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt()) (3)
}
}
)
blobIs.close()
clobReader.close()
1 | 传入 `lobHandler`(在此示例中)是一个简单的 `DefaultLobHandler`。 |
2 | 使用 `setClobAsCharacterStream` 方法传入 CLOB 的内容。 |
3 | 使用 `setBlobAsBinaryStream` 方法传入 BLOB 的内容。 |
如果您在 `DefaultLobHandler.getLobCreator()` 返回的 `LobCreator` 上调用 `setBlobAsBinaryStream`、`setClobAsAsciiStream` 或 `setClobAsCharacterStream` 方法,则可以选择为 `contentLength` 参数指定负值。如果指定的长度为负值,则 `DefaultLobHandler` 使用没有长度参数的 JDBC 4.0 版本的 set-stream 方法。否则,它将指定的长度传递给驱动程序。 请查看您使用的 JDBC 驱动程序的文档,以验证它是否支持在不提供内容长度的情况下流式传输 LOB。 |
现在是时候从数据库中读取 LOB 数据了。同样,您使用一个具有相同实例变量 lobHandler
和对 DefaultLobHandler
的引用的 JdbcTemplate
。以下示例展示了如何做到这一点
-
Java
-
Kotlin
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 的内容。 |
val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ ->
val clobText = lobHandler.getClobAsString(rs, "a_clob") (1)
val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob") (2)
mapOf("CLOB" to clobText, "BLOB" to blobBytes)
}
1 | 使用 getClobAsString 方法检索 CLOB 的内容。 |
2 | 使用 getBlobAsBytes 方法检索 BLOB 的内容。 |
为 IN 子句传递值列表
SQL 标准允许根据包含可变值列表的表达式选择行。一个典型的例子是 select * from T_ACTOR where id in (1, 2, 3)
。JDBC 标准不支持为准备好的语句直接使用此可变列表。您无法声明可变数量的占位符。您需要准备具有所需数量占位符的多个变体,或者您需要在知道需要多少占位符后动态生成 SQL 字符串。NamedParameterJdbcTemplate
中提供的命名参数支持采用后一种方法。您可以将值作为 java.util.List
(或任何 Iterable
)的简单值传递。此列表用于将所需的占位符插入实际的 SQL 语句中,并在语句执行期间传递值。
在传递许多值时要小心。JDBC 标准不保证您可以为 IN 表达式列表使用超过 100 个值。各种数据库超过了这个数字,但它们通常对允许的值数量有硬性限制。例如,Oracle 的限制是 1000。
|
除了值列表中的原始值之外,您还可以创建一个 java.util.List
的对象数组。此列表可以支持为 in
子句定义多个表达式,例如 select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))
。当然,这要求您的数据库支持此语法。
处理存储过程调用中的复杂类型
当您调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,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)
}
}
}
}