Parameter 與資料值處理的常見問題

Spring Framework 的 JDBC 支援提供的不同方法中,存在 Parameter 與資料值的常見問題。本節涵蓋如何解決這些問題。

為 Parameter 提供 SQL 類型資訊

通常,Spring 會根據傳入的 Parameter 類型來判斷 Parameter 的 SQL 類型。可以明確提供設定 Parameter 值時要使用的 SQL 類型。這在某些情況下對於正確設定 NULL 值是必要的。

您可以透過幾種方式提供 SQL 類型資訊

  • JdbcTemplate 的許多更新和查詢方法都採用 int 陣列形式的額外 Parameter。此陣列用於透過使用 java.sql.Types 類別中的常數值來指示對應 Parameter 的 SQL 類型。為每個 Parameter 提供一個條目。

  • 您可以使用 SqlParameterValue 類別來包裝需要此額外資訊的 Parameter 值。為此,為每個值建立一個新實例,並在建構子中傳入 SQL 類型和 Parameter 值。您也可以為數值 Parameter 提供可選的 scale Parameter。

  • 對於使用具名 Parameter 的方法,您可以使用 SqlParameterSource 類別、BeanPropertySqlParameterSourceMapSqlParameterSource。它們都具有用於註冊任何具名 Parameter 值的 SQL 類型的方法。

處理 BLOB 和 CLOB 物件

您可以將影像、其他二進位資料以及大量文字儲存在資料庫中。這些大型物件對於二進位資料稱為 BLOB (二進位大型物件),對於字元資料稱為 CLOB (字元大型物件)。在 Spring 中,您可以透過直接使用 JdbcTemplate 以及使用 RDBMS 物件和 SimpleJdbc 類別提供的高階抽象化來處理這些大型物件。所有這些方法都使用 LobHandler 介面的實作來實際管理 LOB (大型物件) 資料。LobHandler 提供對 LobCreator 類別的存取,透過 getLobCreator 方法,該類別用於建立要插入的新 LOB 物件。

LobCreatorLobHandler 為 LOB 輸入和輸出提供以下支援

  • BLOB

    • byte[]: getBlobAsBytessetBlobAsBytes

    • InputStream: getBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

    • String: getClobAsStringsetClobAsString

    • InputStream: getClobAsAsciiStreamsetClobAsAsciiStream

    • Reader: getClobAsCharacterStreamsetClobAsCharacterStream

下一個範例示範如何建立和插入 BLOB。稍後我們將示範如何從資料庫讀取它。

此範例使用 JdbcTemplateAbstractLobCreatingPreparedStatementCallback 的實作。它實作了一個方法 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 的內容。

如果您在從 DefaultLobHandler.getLobCreator() 傳回的 LobCreator 上調用 setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream 方法,您可以選擇性地為 contentLength 引數指定負值。如果指定的內容長度為負數,則 DefaultLobHandler 會使用 JDBC 4.0 變體的 set-stream 方法,而無需長度 Parameter。否則,它會將指定的長度傳遞給驅動程式。

請參閱您使用的 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 標準不直接支援預備語句的此可變列表。您無法宣告可變數量的佔位符。您需要準備具有所需佔位符數量的多種變體,或者在知道需要多少佔位符後,您需要動態產生 SQL 字串。NamedParameterJdbcTemplate 中提供的具名 Parameter 支援採用後一種方法。您可以將值作為簡單值的 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,用於將類型作為 Parameter 傳遞到預存程序。

SqlReturnType 介面有一個必須實作的單一方法 (名為 getTypeValue)。此介面用作 SqlOutParameter 宣告的一部分。以下範例示範傳回使用者宣告類型 ITEM_TYPEjava.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 呼叫的輸入 Parameter 的 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)
	}
}