准备callablestatement时,HSQLDB为过程设置错误的输入参数名称

时间:2016-01-14 00:23:20

标签: java stored-procedures jdbc hsqldb

这个让我发疯。

我正在我的一个junit测试中使用HSQLDB作为内存数据库,这是我为调用存储过程而编写的一组通用类。

初始化db:

@BeforeClass
public static void initializeDB() throws SQLException, ClassNotFoundException {
    dataSource = new JDBCDataSource();
    dataSource.setDatabase("jdbc:hsqldb:mem:testdb;shutdown=true");
    dataSource.setLoginTimeout(10);
    dataSource.setUser("sa");

    Connection connection = dataSource.getConnection();
    Statement statement = connection.createStatement();
    statement.execute("CREATE TABLE users (id INTEGER, name VARCHAR(25), PRIMARY KEY(id))");
    statement.close();
    statement = connection.createStatement();
    statement.executeUpdate("INSERT INTO users VALUES(1, 'Ramiro')");
    statement.executeUpdate("INSERT INTO users VALUES(2, 'Chanukya')");
    statement.close();

    String storedProcedure1 = "CREATE PROCEDURE sp_say_hi(IN greeting_p VARCHAR(10)) "
            + "READS SQL DATA DYNAMIC RESULT SETS 2 "
            + "BEGIN ATOMIC "
            + "DECLARE result CURSOR WITH RETURN FOR SELECT COALESCE(greeting_p, 'Hi')+' '+name as greeting FROM users FOR READ ONLY; "
            + "DECLARE result1 CURSOR WITH RETURN FOR SELECT * FROM users FOR READ ONLY; "
            + "OPEN result; "
            + "OPEN result1; "
            + "END";

    statement = connection.createStatement();
    statement.execute(storedProcedure1);
    statement.close();

    connection.commit();
}

然后在准备语句的类中的某个点我使用setObject方法来注册输入参数:

...
CallableStatement callableStatement = con.prepareCall("{call sp_say_hi(?)}");
callableStatement.setObject("greeting_p", "Hola");
...

setObject方法抛出带有cause:

的JavaSqlException
Caused by: org.hsqldb.HsqlException: Column not found: greeting_p
at org.hsqldb.error.Error.error(Unknown Source)
at org.hsqldb.error.Error.error(Unknown Source)

通过使用调试器,我能够发现 org.hsqldb.jdbc.JDBCCallableStatement 类中有一个包私有方法,它是HSQLDB的CallableStatement实现,方法名是findParameterIndex,它是在setObject方法中调用,该方法检查提供的参数是否存在于过程的参数映射中:

int findParameterIndex(String parameterName) throws SQLException {

    if (isClosed || connection.isClosed) {
        checkClosed();
    }

    int index = parameterNameMap.get(parameterName, -1);

    if (index >= 0) {
        return index + 1;
    }

    throw JDBCUtil.sqlException(ErrorCode.JDBC_COLUMN_NOT_FOUND,
                            parameterName);
}

通过使用调试器探索该地图,我能够看到hsqldb错误地设置了参数名称,至少在此地图中是这样的:

[@p1, null, null, null, null, null, null, null]

我能够通过更改setObject方法调用中的名称来验证:

    callableStatement.setObject("@p1", "Hola");

之后它运作良好。

奇怪的是,如果我使用DatabaseMetaData.getProcedureColumns方法检索该过程的元数据,那么从jdbc元数据的角度来看,param的名称是正确的:

DatabaseMetaData dbMetadata = con.getMetaData();
    ResultSet rs = dbMetadata.getProcedureColumns(con.getCatalog(),
            con.getSchema(),
            "SP_SAY_HI",
            "%_P");
        while(rs.next()) {
          // get stored procedure metadata
          String procedureCatalog     = rs.getString(1);
          String procedureSchema      = rs.getString(2);
          String procedureName        = rs.getString(3);
          String columnName           = rs.getString(4);
          short  columnReturn         = rs.getShort(5);
          int    columnDataType       = rs.getInt(6);
          String columnReturnTypeName = rs.getString(7);
          int    columnPrecision      = rs.getInt(8);
          int    columnByteLength     = rs.getInt(9);
          short  columnScale          = rs.getShort(10);
          short  columnRadix          = rs.getShort(11);
          short  columnNullable       = rs.getShort(12);
          String columnRemarks        = rs.getString(13);

          System.out.println("stored Procedure name="+procedureName);
          System.out.println("procedureCatalog=" + procedureCatalog);
          System.out.println("procedureSchema=" + procedureSchema);
          System.out.println("procedureName=" + procedureName);
          System.out.println("columnName=" + columnName);
          System.out.println("columnReturn=" + columnReturn);
          System.out.println("columnDataType=" + columnDataType);
          System.out.println("columnReturnTypeName=" + columnReturnTypeName);
          System.out.println("columnPrecision=" + columnPrecision);
          System.out.println("columnByteLength=" + columnByteLength);
          System.out.println("columnScale=" + columnScale);
          System.out.println("columnRadix=" + columnRadix);
          System.out.println("columnNullable=" + columnNullable);
          System.out.println("columnRemarks=" + columnRemarks);
        }

打印出来:

stored Procedure name=SP_SAY_HI
procedureCatalog=PUBLIC
procedureSchema=PUBLIC
procedureName=SP_SAY_HI
columnName=GREETING_P
columnReturn=1
columnDataType=12
columnReturnTypeName=CHARACTER VARYING
columnPrecision=10
columnByteLength=0
columnScale=0
columnRadix=0
columnNullable=1
columnRemarks=null

请注意,该名称是大写的,我已经检查过了。我在setObject方法中将参数名称的注册更改为大写,以查看是否有帮助,但没有区别。

更新 我在HSQLDB问题跟踪系统中创建了一个故障单,他们只修复了它并致力于当前的重新发布:https://sourceforge.net/p/hsqldb/bugs/1431/

2 个答案:

答案 0 :(得分:2)

我认为使用从PreparedStatement继承的setX()方法将变量绑定到存储过程调用时始终使用参数索引是最安全的,例如:

callableStatement.setString(1, "Hola");

来自Connection.prepareCall()的JavaDocs:

  

当prepareCall方法完成时,某些驱动程序可能会将call语句发送到数据库;其他人可能要等到执行CallableStatement对象。

如果在调用execute()之前驱动程序没有与数据库通信,那么它也无法知道参数的名称,因为JDBC查询中的绑定变量总是匿名。当绑定发生时,HSQLDB驱动程序似乎确实不知道实际的参数名称,因为它在内部使用占位符名称(@p1等)。

答案 1 :(得分:2)

我在HSQLDB问题跟踪系统中创建了一个故障单,他们只修复了它并提交到下一个版本,以获取更多信息:https://sourceforge.net/p/hsqldb/bugs/1431/