Common Problems with Parameter and Data Value Handling

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

为参数提供 SQL 类型信息

Providing SQL Type Information for Parameters

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

你可以通过几种方式提供 SQL 类型信息。

  • JdbcTemplate 的许多更新和查询方法需要一个 int 数组形式的额外参数。这个数组通过使用 java.sql.Types类中的常量值来表示相应参数的 SQL 类型。为每个参数提供一个条目。

  • 你可以使用 SqlParameterValue 类来包装需要这些额外信息的参数值。要做到这一点,为每个值创建一个新的实例,并在构造函数中传入 SQL 类型和参数值。你也可以为数字值提供一个可选的比例参数。

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

处理 BLOB 和 CLOB 对象

Handling BLOB and CLOB objects

你可以在数据库中存储图像、其他二进制数据和大块的文本。这些大型对象在二进制数据中被称为 BLOB(Binary Large OBject),在字符数据中被称为 CLOB(Character Large OBject)。在 Spring 中,你可以通过直接使用 JdbcTemplate 来处理这些大对象,也可以在使用 RDBMS Objects 和 SimpleJdbc 类提供的更高的抽象时处理。所有这些方法都使用 LobHandler 接口的实现来实际管理 LOB(Large OBject)数据。 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。后来我们展示了如何从数据库中读回它。

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

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

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

  1. final File blobIn = new File("spring2004.jpg");
  2. final InputStream blobIs = new FileInputStream(blobIn);
  3. final File clobIn = new File("large.txt");
  4. final InputStream clobIs = new FileInputStream(clobIn);
  5. final InputStreamReader clobReader = new InputStreamReader(clobIs);
  6. jdbcTemplate.execute(
  7. "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
  8. // 传入一个 lobHandler,这里使用 DefaultLobHandler
  9. new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
  10. protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
  11. ps.setLong(1, 1L);
  12. // 传入 CLOB 内容
  13. lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());
  14. // 传入 BLOB 内容
  15. lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());
  16. }
  17. }
  18. );
  19. blobIs.close();
  20. clobReader.close();

:::info 如果在从 DefaultLobHandler.getLobCreator() 返回的 LobCreator 上调用 setBlobAsBinaryStream、setClobAsciiStream 或 setClobAsCharacterStream 方法,可以选择为 contentLength 参数指定一个负值。如果指定的内容长度是负值,DefaultLobHandler 就会使用没有长度参数的 JDBC 4.0 变体的 set-stream 方法。否则,它将指定的长度传递给驱动程序。

请看你使用的 JDBC 驱动程序的文档,以验证它是否支持不提供内容长度的 LOB 流。 :::

现在是时候从数据库中读取 LOB 数据了。同样,你使用一个 JdbcTemplate,它有相同的实例变量 lobHandler 和一个对 DefaultLobHandler 的引用。下面的例子展示了如何做到这一点:

  1. List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
  2. new RowMapper<Map<String, Object>>() {
  3. public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
  4. Map<String, Object> results = new HashMap<String, Object>();
  5. String clobText = lobHandler.getClobAsString(rs, "a_clob");
  6. results.put("CLOB", clobText);
  7. byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");
  8. results.put("BLOB", blobBytes);
  9. return results;
  10. }
  11. });

在 IN 子句中传递数值列表

Passing in Lists of Values for IN Clause

SQL 标准允许根据表达式来选择记录,表达式包括一个可变的值列表。一个典型的例子是 select * from T_ACTOR where id in (1, 2, 3). JDBC 标准不直接支持准备好的语句的这种变量列表。你不能声明一个可变数量的占位符。你需要准备一些具有所需占位符数量的变体,或者你需要在知道需要多少占位符后动态地生成 SQL 字符串。NamedParameterJdbcTemplate 和 JdbcTemplate 中提供的命名参数支持采用了后一种方法。你可以将值作为原始对象的 java.util.List传入。这个列表被用来插入所需的占位符,并在语句执行过程中传入值。

:::info 在传入许多值时要小心。JDBC 标准并不保证你可以在表达式列表中使用超过 100 个值。各种数据库都超过了这个数字,但它们通常对允许的数值有一个硬性限制。例如,Oracle 的限制是 1000。 :::

除了值列表中的原始值,你还可以创建一个 java.util.List的对象数组。这个列表可以支持为 in 子句定义多个表达式,例如 select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop')。当然,这需要你的数据库支持这种语法(MySQL 不支持)。

处理存储过程调用的复杂类型

Handling Complex Types for Stored Procedure Calls

当你调用存储过程时,你有时会使用数据库中的特定复杂类型。为了适应这些类型,Spring提供了一个SqlReturnType,用于处理从存储过程调用中返回的类型,以及作为参数传入存储过程的SqlTypeValue。

SqlReturnType 接口有一个必须实现的方法(名为 getTypeValue)。这个接口被用作 SqlOutParameter 的声明的一部分。下面的例子显示了返回用户声明类型 ITEM_TYPE 的 Oracle STRUCT 对象的值。

  1. public class TestItemStoredProcedure extends StoredProcedure {
  2. public TestItemStoredProcedure(DataSource dataSource) {
  3. // ...
  4. declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
  5. (CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
  6. STRUCT struct = (STRUCT) cs.getObject(colIndx);
  7. Object[] attr = struct.getAttributes();
  8. TestItem item = new TestItem();
  9. item.setId(((Number) attr[0]).longValue());
  10. item.setDescription((String) attr[1]);
  11. item.setExpirationDate((java.util.Date) attr[2]);
  12. return item;
  13. }));
  14. // ...
  15. }

你可以使用 SqlTypeValue 将一个 Java 对象(如 TestItem)的值传递给一个存储过程。SqlTypeValue 接口有一个单一的方法(名为createTypeValue),你必须实现。活动连接被传递进来,你可以用它来创建数据库特定的对象,如 StructDescriptor 实例或 ArrayDescriptor 实例。下面的例子创建了一个 StructDescriptor 实例。

  1. final TestItem testItem = new TestItem(123L, "A test item",
  2. new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
  3. SqlTypeValue value = new AbstractSqlTypeValue() {
  4. protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
  5. StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
  6. Struct item = new STRUCT(itemDescriptor, conn,
  7. new Object[] {
  8. testItem.getId(),
  9. testItem.getDescription(),
  10. new java.sql.Date(testItem.getExpirationDate().getTime())
  11. });
  12. return item;
  13. }
  14. };

现在你可以将这个 SqlTypeValue 添加到包含存储过程执行调用的输入参数的地图中。

SqlTypeValue 的另一个用途是将一个数组的值传递给 Oracle 存储过程。Oracle 有自己的内部 ARRAY 类,在这种情况下必须使用,你可以使用 SqlTypeValue 来创建一个 Oracle ARRAY 的实例,并用 Java ARRAY 的值来填充它,正如下面的例子所示。

  1. final Long[] ids = new Long[] {1L, 2L};
  2. SqlTypeValue value = new AbstractSqlTypeValue() {
  3. protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
  4. ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
  5. ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
  6. return idArray;
  7. }
  8. };