跳到主要内容

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

ChatGPT-4o 中英对照 Common Problems with Parameter and Data Value Handling

Spring Framework 的 JDBC 支持提供的不同方法中,参数和数据值常见问题的存在。 本节介绍如何解决这些问题。

为参数提供 SQL 类型信息

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

您可以通过多种方式提供 SQL 类型信息:

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

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

  • 对于使用命名参数的方法,你可以使用 SqlParameterSource 类,BeanPropertySqlParameterSourceMapSqlParameterSource。它们都有用于注册任何命名参数值的 SQL 类型的方法。

处理 BLOB 和 CLOB 对象

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

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

  • BLOB

    • byte[]: getBlobAsBytessetBlobAsBytes

    • InputStream: getBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

    • String: getClobAsStringsetClobAsString

    • InputStream: getClobAsAsciiStreamsetClobAsAsciiStream

    • Reader: getClobAsCharacterStreamsetClobAsCharacterStream

下一个示例展示了如何创建和插入一个 BLOB。稍后我们将展示如何从数据库中读取它。

这个例子使用了一个 JdbcTemplate 和一个 AbstractLobCreatingPreparedStatementCallback 的实现。它实现了一个方法,setValues。这个方法提供了一个 LobCreator,我们用它来为 SQL 插入语句中的 LOB 列设置值。

在这个例子中,我们假设有一个变量 lobHandler,它已经被设置为一个 DefaultLobHandler 的实例。通常,你会通过依赖注入来设置这个值。

下面的示例展示了如何创建和插入一个 BLOB:

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();
java
  • 传入 lobHandler,在此示例中是一个普通的 DefaultLobHandler

  • 使用方法 setClobAsCharacterStream 传入 CLOB 的内容。

  • 使用方法 setBlobAsBinaryStream 传入 BLOB 的内容。

备注

如果你在从 DefaultLobHandler.getLobCreator() 返回的 LobCreator 上调用 setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream 方法,可以选择为 contentLength 参数指定一个负值。如果指定的内容长度为负值,DefaultLobHandler 将使用 JDBC 4.0 版本的无长度参数的设置流方法。否则,它会将指定的长度传递给驱动程序。

请查阅你使用的 JDBC 驱动程序的文档,以确认它是否支持在不提供内容长度的情况下流式传输 LOB。

现在是时候从数据库中读取 LOB 数据了。同样,您可以使用 JdbcTemplate,并使用相同的实例变量 lobHandler 和对 DefaultLobHandler 的引用。以下示例展示了如何进行操作:

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;
}
});
java
  • 使用方法 getClobAsString 来获取 CLOB 的内容。

  • 使用方法 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 对象的值:

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;
}));
// ...
}

}
java

您可以使用 SqlTypeValue 将 Java 对象(例如 TestItem)的值传递给存储过程。SqlTypeValue 接口有一个方法(名为 createTypeValue)需要您实现。活动连接会被传入,您可以使用它创建特定于数据库的对象,例如 java.sql.Struct 实例或 java.sql.Array 实例。以下示例创建了一个 java.sql.Struct 实例:

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);
}
};
java

现在,您可以将此 SqlTypeValue 添加到包含存储过程 execute 调用输入参数的 Map 中。

SqlTypeValue 的另一个用途是将值数组传递给 Oracle 存储过程。Oracle 在 OracleConnection 上有一个 createOracleArray 方法,你可以通过解包来访问它。你可以使用 SqlTypeValue 创建一个数组,并用 Java java.sql.Array 中的值填充它,如以下示例所示:

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);
}
};
java