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

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

为参数提供 SQL 类型信息

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

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

  • JdbcTemplate 的许多更新和查询方法采用 int 数组形式的附加参数。此数组用于通过使用 java.sql.Types 类中的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。

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

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

处理 BLOB 和 CLOB 对象

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

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

  • BLOB

    • byte[]: getBlobAsBytessetBlobAsBytes

    • InputStream: getBlobAsBinaryStream and setBlobAsBinaryStream

  • CLOB

    • String: getClobAsString and setClobAsString

    • InputStream: getClobAsAsciiStream and setClobAsAsciiStream

    • Reader: getClobAsCharacterStream and setClobAsCharacterStream

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

此示例使用 JdbcTemplateAbstractLobCreatingPreparedStatementCallback 的实现。 它实现了一个方法 setValues。此方法提供了一个 LobCreator,我们使用它来设置 SQL 插入语句中 LOB 列的值。

对于此示例,我们假设有一个变量 lobHandler,它已经设置为 DefaultLobHandler 的实例。 您通常通过依赖注入设置此值。

以下示例展示了如何创建和插入 BLOB:

  • Java

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 的内容。

如果您在从 DefaultLobHandler.getLobCreator() 返回的 LobCreator 上调用 setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream 方法,您可以选择为 contentLength 参数指定负值。 如果指定的内容长度为负数,则 DefaultLobHandler 使用不带长度参数的 JDBC 4.0 变体的 set-stream 方法。 否则,它会将指定的长度传递给驱动程序。

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

现在是时候从数据库读取 LOB 数据了。再次,您使用具有相同实例变量 lobHandler 和对 DefaultLobHandler 引用的 JdbcTemplate。以下示例展示了如何执行此操作:

  • Java

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 的内容。

为 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'))。 当然,这需要您的数据库支持此语法。

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

调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,Infra 提供了 SqlReturnType 用于处理从存储过程调用返回的它们,以及 SqlTypeValue 用于将它们作为参数传递给存储过程。

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

  • Java

public class TestItemStoredProcedure extends StoredProcedure {

  public TestItemStoredProcedure(DataSource dataSource) {
    // ...
    declareParameter(new SqlOutParameter("item", OracleTypes.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;
      }));
    // ...
  }

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

  • Java

final 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 conn, int sqlType, String typeName) throws SQLException {
    StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
    Struct item = new STRUCT(itemDescriptor, conn,
    new Object[] {
      testItem.getId(),
      testItem.getDescription(),
      new java.sql.Date(testItem.getExpirationDate().getTime())
    });
    return item;
  }
};

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

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

  • Java

final Long[] ids = new Long[] {1L, 2L};

SqlTypeValue value = new AbstractSqlTypeValue() {
  protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
    ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
    ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
    return idArray;
  }
};