参数和数据值处理中的常见问题

Spring Framework 的 JDBC 支持提供了多种处理参数和数据值的方法,其中存在一些常见问题。本节介绍如何解决这些问题。

为参数提供 SQL 类型信息

通常,Spring 根据传入的参数类型确定参数的 SQL 类型。可以显式提供设置参数值时使用的 SQL 类型。这有时对于正确设置 NULL 值是必需的。

您可以通过以下几种方式提供 SQL 类型信息

  • JdbcTemplate 的许多更新和查询方法接受一个额外的参数,形式为 int 数组。该数组用于指示相应参数的 SQL 类型,使用的是 java.sql.Types 类中的常量值。每个参数提供一个条目。

  • 您可以使用 SqlParameterValue 类来封装需要这些附加信息的参数值。为此,为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。您还可以为数值提供一个可选的刻度参数。

  • 对于处理命名参数的方法,您可以使用 SqlParameterSource 类,如 BeanPropertySqlParameterSourceMapSqlParameterSource。它们都提供了注册任何命名参数值的 SQL 类型的方法。

处理 BLOB 和 CLOB 对象

您可以在数据库中存储图像、其他二进制数据以及大块文本。这些大型对象对于二进制数据称为 BLOB(Binary Large OBject),对于字符数据称为 CLOB(Character Large OBject)。在 Spring 中,您可以使用 JdbcTemplate 直接处理这些大型对象,也可以在使用 RDBMS 对象和 SimpleJdbc 类提供的高级抽象时处理它们。所有这些方法都使用 LobHandler 接口的实现来实际管理 LOB(大型对象)数据。LobHandler 通过 getLobCreator 方法提供对 LobCreator 类的访问,该类用于创建要插入的新 LOB 对象。

LobCreatorLobHandler 为 LOB 输入和输出提供以下支持

  • BLOB

    • byte[]getBlobAsBytessetBlobAsBytes

    • InputStreamgetBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

    • StringgetClobAsStringsetClobAsString

    • InputStreamgetClobAsAsciiStreamsetClobAsAsciiStream

    • ReadergetClobAsCharacterStreamsetClobAsCharacterStream

下一个示例展示如何创建和插入一个 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 方法。否则,它会将指定的长度传递给驱动程序。

请查阅您使用的 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_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 调用输入参数的 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)
	}
}