【问题标题】:Using cursors and getting result in Oracle PL/SQL with Java/JDBC在带有 Java/JDBC 的 Oracle PL/SQL 中使用游标并获取结果
【发布时间】:2013-04-09 10:57:01
【问题描述】:

我有一个这样构造的 PL/SQL 查询:

DECLARE
a NUMBER;
B NUMBER;
CURSOR cursor
IS
 ( SOME SELECT QUERY);
BEGIN
  OPEN cursor;
    LOOP
    SOME STUFF;
    END LOOP;
  CLOSE cursor;
END

如何使用 jdbc 从 java 代码运行此查询并获取结果集?我尝试在不使用游标的情况下运行查询,并且它运行正常。我想不出在java代码中做到这一点的方法。如果我直接在 oracle 客户端上运行查询,它可以正常工作。所以查询没有问题。

附:由于某些限制,我不想将代码存储为存储过程并调用它。

【问题讨论】:

标签: java sql oracle jdbc plsql


【解决方案1】:

@拉贾特,

你可以试试下面的方法吗:

要检索游标,您应该在包规范中将其声明为 REF CURSOR。

  --Creating the REF CURSOR type
  type g_cursor is ref cursor;

在规范和正文中,您需要在过程签名中声明一个 out REF CURSOR 变量,如上文所述。

  procedure PRO_RETURN_CARS(
    i_id     in     tbl_car.car_id%type,
    o_cursor in out g_cursor);

游标必须在程序体中打开才能返回,这样:

open o_cursor for
          select car_id, company, model, color, hp, price
          from tbl_car
          where car_id = i_id;

完整的包:

create or replace package PAC_CURSOR is
  --Creating REF CURSOR type
  type g_cursor is ref cursor;

  --Procedure that return the cursor
  procedure PRO_RETURN_CARS(
    i_id     in     tbl_car.car_id%type,
    o_cursor in out g_cursor); -- Our cursor

end PAC_CURSOR;
/

create or replace package body PAC_CURSOR is
  procedure PRO_RETURN_CARS(
    i_id     in     tbl_car.car_id%type,
    o_cursor in out g_cursor) is

       begin
        --Opening the cursor to return matched rows
        open o_cursor for
          select car_id, company, model, color, hp, price
          from tbl_car
          where car_id = i_id;

  end PRO_RETURN_CARS;

end PAC_CURSOR;

Oracle 端已经准备好了,现在我们需要创建 Java 调用

游标是如何由过程返回的,我们将使用java.sql.CallableStatement 实例:

CallableStatement cs = conn.prepareCall("{call PAC_CURSOR.PRO_RETURN_CARS(?,?)}");

registerOutParameter 将获得oracle.jdbc.OracleTypes.CURSOR 类型并返回一个java.sql.ResultSet 实例。我们可以像普通的Iterator 一样迭代ResultSet

SELECT 返回的每一行列都将使用对应的 getter 表示如何映射。例如,当列的值为varchar时调用getString()方法,当为日期时调用getDate()等。

完整的代码会是这样的:

//Calling Oracle procedure
CallableStatement cs = conn.prepareCall("{call PAC_CURSOR.PRO_RETURN_CARS(?,?)}");

//Defining type of return
cs.registerOutParameter("o_cursor", OracleTypes.CURSOR);
cs.setLong("i_id", id);

cs.execute();//Running the call

//Retrieving the cursor as ResultSet
ResultSet rs = (ResultSet)cs.getObject("o_cursor");

//Iterating the returned rows
while(rs.next()){
    //Getting column values
    System.out.println("ID: " + rs.getLong("car_id"));
    System.out.println("Manufacturer: " + rs.getString("company"));
    System.out.println("Model: " + rs.getString("model"));
    System.out.println("Color: " + rs.getString("color"));
    System.out.println("HP: " + rs.getString("hp"));
    System.out.println("Price: " + rs.getFloat("price"));
}

最后,您将获得 SELECT 子句中返回的任何值。

【讨论】:

  • 我可以将您的解决方案与返回游标一起使用 DBMS_SQL.RETURN_RESULT(cur);以及如何关闭您的案例上的光标。因为当Procedure中带有“Out”参数关键字的游标返回时,PRO_RETURN_CARS Procedure执行完成后会自动关闭吗?
【解决方案2】:

这是不可能的。您不能从匿名 PL/SQL 块返回结果集(因此无法从 JDBC 获取)。

您需要直接从 JDBC 运行选择。

唯一非常难看的解决方法是使用dbms_output.put_line(),然后再阅读。但这是一个非常丑陋的 hack,直接在 JDBC 中处理 SELECT 查询的结果要好得多。


编辑 1

这是一个使用 dbms_output 的小例子:

Connection con = ....;

// turn on support for dbms_output
CallableStatement cstmt = con.prepareCall("{call dbms_output.enable(32000) }");
cstmt.execute();

// run your PL/SQL block
Statement stmt = con.createStatement();
String sql =
    "declare  \n" +
    " a number;  \n" +
    " cursor c1 is select id from foo;  \n" +
    "begin  \n" +
    "  open c1; \n" +
    "  loop \n" +
    "    fetch c1 into a;  \n" +
    "    exit when c1%notfound;  \n" +
    "    dbms_output.put_line('ID: '||to_char(a)); \n" +
    "  end loop; \n" +
    "end;";
stmt.execute(sql);

// retrieve the messages written with dbms_output
cstmt = con.prepareCall("{call dbms_output.get_line(?,?)}");
cstmt.registerOutParameter(1,java.sql.Types.VARCHAR);
cstmt.registerOutParameter(2,java.sql.Types.NUMERIC);

int status = 0;
while (status == 0)
{
    cstmt.execute();
    String line = cstmt.getString(1);
    status = cstmt.getInt(2);
    if (line != null && status == 0)
    {
        System.out.println(line);
    }
}

编辑 2(这对于评论来说太长了)

嵌套循环来检索数据几乎总是一个坏主意。如果你发现自己在做这样的事情:

begin
  for data_1 in (select id from foo_1) loop
    dbms_output.put_line(to_char(data_1.id));

    for data_2 in (select f2.col1, f2.col2 from foo_2 f2 where f2.id = data_1.id) loop
        ... do something else
    end loop;

  end loop;
end;
/

这样做会更有效率:

begin
  for data_1 in (select f2.col1, f2.col2 from foo_2 f2
                 where f2.id in (select f1.id from foo_1 f1)) loop

     ... do something

  end loop;
end;
/

这可以在没有过多内存的情况下在 JDBC 中使用这样的东西来处理:

String sql = "select f2.col1, f2.col2 from foo_2 f2 where f2.id in (select f1.id from foo_1 f1)";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next())
{
   String col1_value = rs.getString(1);
   int    col2_value = rs.getInt(2);
   ... do something
}

即使您处理数十亿行,上面的代码也只会在内存中保存一行。准确地说:JDBC 驱动程序实际上会预取多行。默认值为 10,可以更改。但即便如此,您也没有过多的内存使用。

【讨论】:

  • 有没有办法将字符串从 PL/SQL 块返回到 java 程序?或者剩下的唯一选择是在java程序本身中处理相同的?
  • 我已经尝试过使用 dbms_output.put_line()。它不起作用并抛出无效的 sql (同样的异常也没有)。需要使用 PL/SQL,因为我也在该块中运行查询,使用游标中的每一行。加载整个第一个结果并从java代码中多次取消该查询会更慢,不是吗? (我不知道。)
  • @RajatGarg:我添加了一个关于如何使用dbms_output() 的示例。但我非常质疑“需要PL/SQL”这一事实。对于这样一个简单的任务,这是一个非常愚蠢的要求,使处理结果变得相当复杂,并且通常比直接处理查询结果要慢很多。
  • 谢谢哥们,我会试着解释一下这个场景。我正在通过选择查询检索游标。然后我需要使用该结果集(光标)的每一行,使用一些谓词查询另一个表。替代可以是,我在java中得到结果集,对于我查询第二个表的每一行。但这会占用我的 JVM 上的资源,并且会使用大量内存。使用 PLSQL 块,我将此处理委托给数据库服务器,因此我得到处理后的最小结果并使用更少的内存。
  • @RajatGarg:在循环中运行选择几乎总是一个坏主意。在几乎所有情况下,您都可以用一条 SQL 语句替换它。如果您逐行处理 ResultSet,那么您确实将数据完全加载到内存中,因此它不会占用“JVM 中的大量内存”。如果您正在处理这么多行,那么dbms_output 的缓冲区可能太小而无法处理所有这些。您应该发布您的 real 问题(在循环中选择另一个选择)以获得帮助。您正在与症状作斗争,而不是真正的问题。
【解决方案3】:

这里的其他答案似乎超级复杂。

使用SYS_REFCURSOR

从此以后,您可以非常轻松地从 JDBC 检索 SYS_REFCURSOR 类型:

DECLARE
  cur SYS_REFCURSOR;
BEGIN
  OPEN cur FOR SELECT ...;
  ? := cur;
END;

现在像这样从 Java 运行上面的代码:

try (CallableStatement c = con.prepareCall(sql)) {
    c.registerOutParameter(1, OracleTypes.CURSOR); // -10
    c.execute();

    try (ResultSet rs = (ResultSet) c.getObject(1)) {
        ...
    }
}

当然,您也可以按照pmr's answer 的建议在包中声明自己的游标,但是如果您从 JDBC 运行匿名块,为什么要这样做呢?

使用 Oracle 12c 隐式结果集

Oracle 12c 为这些情况添加了一个方便的新功能,类似于 SQL Server/Sybase 和 MySQL 考虑返回结果的过程/批处理的方式。您现在可以在任何游标上使用DBMS_SQL.RETURN_RESULT 过程,它会“神奇地”返回它:

DECLARE
  cur SYS_REFCURSOR;
BEGIN
  OPEN cur FOR SELECT ...;
  DBMS_SQL.RETURN_RESULT(cur);
END;

Due to a bug (or "feature") in the Oracle JDBC driver,从 JDBC but it can certainly be done as I've shown in this article here 正确获取该游标有点棘手。这就是您可以从任何匿名 PL/SQL 块和/或过程、触发器等中发现任意数量的隐式游标的方法...:

try (PreparedStatement s = cn.prepareStatement(sql)) {
    // Use good old three-valued boolean logic
    Boolean result = s.execute();

    fetchLoop:
    for (int i = 0;; i++) {

        // Check for more results if not already done in this iteration
        if (i > 0 && result == null)
            result = s.getMoreResults();
        System.out.println(result);

        if (result) {
            result = null;

            try (ResultSet rs = s.getResultSet()) {
                System.out.println("Fetching result " + i);
            }
            catch (SQLException e) {
                // Ignore ORA-17283: No resultset available
                if (e.getErrorCode() == 17283)
                    continue fetchLoop;
                else
                    throw e;
            }
        }
        else if (s.getUpdateCount() == -1)
            // Ignore -1 value if there is one more result!
            if (result = s.getMoreResults())
                continue fetchLoop;
            else
                break fetchLoop;
    }
}

【讨论】:

  • 您解释了使用 Oracle 12c 隐式结果集和一个方便的新功能 OPEN cur FOR SELECT ...; DBMS_SQL.RETURN_RESULT(cur);我可以使用此功能并在 java 中读取数据吗? try (CallableStatement c = con.prepareCall(sql)) { c.registerOutParameter(1, OracleTypes.CURSOR); // -10 c.execute();以这种方式尝试 (ResultSet rs = (ResultSet) c.getObject(1)) { ... } }。我为什么要问,因为我无法使用 fetchLoop 理解您的代码的第二部分:
  • 第二部分是为了调试吗?
  • @RahimjonRustamov:我的示例代码开箱即用,您可以根据需要进行调整来使用它。只有 println 命令用于调试。您将通过实际使用您的结果集来替换它们。正如我所描述的,其余的都是必需的。我认为您不能像以前那样使用CallableStatement。但无论如何,理想情况下,你会问一个新的、具体的问题。
【解决方案4】:

因为 java.sql.PreparedStatement.execute() 的签名是“boolean execute()”而不是“Boolean execute()”,所以“result”变量永远不会为空,因为将 s 的返回值装箱.execute(),所以测试“i>0 && result==null”可能是“result==null”

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-08-12
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 2018-10-30
    • 1970-01-01
    • 1970-01-01
    • 2011-07-03
    相关资源
    最近更新 更多