引數和資料值處理中的常見問題
Spring Framework 的 JDBC 支援提供了多種處理引數和資料值的方法,其中存在一些常見問題。本節介紹如何解決這些問題。
為引數提供 SQL 型別資訊
通常,Spring 根據傳入的引數型別確定引數的 SQL 型別。可以顯式提供設定引數值時使用的 SQL 型別。這有時對於正確設定 NULL
值是必需的。
您可以透過以下幾種方式提供 SQL 型別資訊
-
JdbcTemplate
的許多更新和查詢方法接受一個額外的引數,形式為int
陣列。該陣列用於指示相應引數的 SQL 型別,使用的是java.sql.Types
類中的常量值。每個引數提供一個條目。 -
您可以使用
SqlParameterValue
類來封裝需要這些附加資訊的引數值。為此,為每個值建立一個新例項,並在建構函式中傳入 SQL 型別和引數值。您還可以為數值提供一個可選的刻度引數。 -
對於處理命名引數的方法,您可以使用
SqlParameterSource
類,如BeanPropertySqlParameterSource
或MapSqlParameterSource
。它們都提供了註冊任何命名引數值的 SQL 型別的方法。
處理 BLOB 和 CLOB 物件
您可以在資料庫中儲存影像、其他二進位制資料以及大塊文字。這些大型物件對於二進位制資料稱為 BLOB(Binary Large OBject),對於字元資料稱為 CLOB(Character Large OBject)。在 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 insert 語句中 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 資料了。同樣,您使用與之前相同的例項變數 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
的 java.sql.Struct
物件的值
-
Java
-
Kotlin
import java.sql.CallableStatement;
import java.sql.Struct;
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
super(dataSource, "get_item");
declareParameter(new SqlOutParameter("item", Types.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;
}));
// ...
}
}
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.`object`.StoredProcedure
import java.sql.CallableStatement
import java.sql.Struct
import java.sql.Types
import java.util.Date
import javax.sql.DataSource
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, "get_item") {
init {
declareParameter(SqlOutParameter("item",Types.STRUCT,"ITEM_TYPE") {
cs: CallableStatement, colIndx: Int, _: Int, _: String? ->
val struct = cs.getObject(colIndx) as Struct
val attr = struct.attributes
val item = TestItem()
item.id = (attr[0] as Number).toLong()
item.description = attr[1] as String
item.expirationDate = attr[2] as Date
item
})
// ...
}
}
您可以使用 SqlTypeValue
將 Java 物件(例如 TestItem
)的值傳遞給儲存過程。SqlTypeValue
介面有一個必須實現的方法(名為 createTypeValue
)。活動連線會被傳入,您可以使用它建立資料庫特定的物件,例如 java.sql.Struct
例項或 java.sql.Array
例項。以下示例建立了一個 java.sql.Struct
例項
-
Java
-
Kotlin
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 connection, int sqlType, String typeName) throws SQLException {
Object[] item = new Object[] { testItem.getId(), testItem.getDescription(),
new java.sql.Date(testItem.getExpirationDate().getTime()) };
return connection.createStruct(typeName, item);
}
};
val testItem = TestItem(123L, "A test item",
SimpleDateFormat("yyyy-M-d").parse("2010-12-31"))
val value = object : AbstractSqlTypeValue() {
override fun createTypeValue(connection: Connection, sqlType: Int, typeName: String?): Any {
val item = arrayOf<Any>(testItem.id, testItem.description,
Date(testItem.expirationDate.time))
return connection.createStruct(typeName, item)
}
}
現在您可以將此 SqlTypeValue
新增到包含儲存過程 execute
呼叫輸入引數的 Map
中。
SqlTypeValue
的另一個用途是向 Oracle 儲存過程傳入值陣列。Oracle 在 OracleConnection
上有一個 createOracleArray
方法,您可以透過解包裝它來訪問。您可以使用 SqlTypeValue
建立一個數組,並用 Java java.sql.Array
中的值填充它,如下面的示例所示
-
Java
-
Kotlin
Long[] ids = new Long[] {1L, 2L};
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
return conn.unwrap(OracleConnection.class).createOracleArray(typeName, ids);
}
};
val ids = arrayOf(1L, 2L)
val value: SqlTypeValue = object : AbstractSqlTypeValue() {
override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
return conn.unwrap(OracleConnection::class.java).createOracleArray(typeName, ids)
}
}