|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
参数与数据值处理中的常见问题
Spring Framework 的 JDBC 支持所提供的不同方法中,参数和数据值存在一些常见问题。本节将介绍如何解决这些问题。
为参数提供 SQL 类型信息
通常,Spring 会根据传入参数的类型来确定参数的 SQL 类型。在设置参数值时,也可以显式指定要使用的 SQL 类型。这在某些情况下是必要的,以正确设置 NULL 值。
你可以通过多种方式提供 SQL 类型信息:
-
JdbcTemplate的许多更新和查询方法都接受一个额外的参数,该参数是一个int数组。此数组用于通过使用java.sql.Types类中的常量值来指示相应参数的 SQL 类型。每个参数都应提供一个对应的条目。 -
您可以使用
SqlParameterValue类来包装需要此类附加信息的参数值。为此,请为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。您还可以为数值类型提供一个可选的精度(scale)参数。 -
对于使用命名参数的方法,您可以使用
SqlParameterSource类,例如BeanPropertySqlParameterSource或MapSqlParameterSource。它们都提供了用于为任意命名参数值注册 SQL 类型的方法。
处理 BLOB 和 CLOB 对象
你可以在数据库中存储图像、其他二进制数据以及大段文本。这些大型对象在二进制数据的情况下被称为 BLOB(Binary Large OBject,二进制大对象),在字符数据的情况下被称为 CLOB(Character Large OBject,字符大对象)。在 Spring 中,你可以直接使用 JdbcTemplate 来处理这些大型对象,也可以通过 RDBMS 对象和 SimpleJdbc 类所提供的更高层次的抽象来处理。所有这些方法都使用 LobHandler 接口的实现类来实际管理 LOB(Large OBject,大对象)数据。LobHandler 通过 LobCreator 方法提供对 getLobCreator 类的访问,该类用于创建新的 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 的内容。 |
|
如果你在通过 请查阅您所使用的 JDBC 驱动程序的文档,以确认它是否支持在不提供内容长度的情况下流式传输 LOB。 |
现在是时候从数据库中读取 LOB 数据了。同样,您使用一个 JdbcTemplate,配合相同的实例变量 lobHandler 和一个指向 DefaultLobHandler 的引用。
以下示例展示了如何实现这一点:
-
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 标准并未直接支持在预编译语句(prepared statements)中使用这种可变值列表。你无法声明可变数量的占位符。你需要预先准备多个具有所需占位符数量的不同 SQL 语句,或者在知道所需占位符数量后动态生成 SQL 字符串。NamedParameterJdbcTemplate 中提供的命名参数支持采用了后一种方法。你可以将值作为简单值的 java.util.List(或任意 Iterable)传入。该列表用于在实际 SQL 语句中插入所需的占位符,并在执行语句时传入这些值。
在传入大量值时要小心。IN 表达式列表的值数量在 JDBC 标准中并未保证可以超过 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 的一部分。
以下示例展示了如何返回用户自定义类型 STRUCT 的 Oracle ITEM_TYPE 对象的值:
-
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 添加到包含存储过程 Map 调用输入参数的 execute 中。
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)
}
}
}
}