17

How can one get the output from Oracle's dbms_output.get_lines in a Java app using JDBC without creating additional objects in the database?

Axel Fontaine
  • 34,542
  • 16
  • 106
  • 137
  • this may help https://community.oracle.com/thread/104787 – Ravinder Reddy Dec 15 '17 at 10:33
  • @RavinderReddy No it doesn't. That post is ancient and no solution is provided there. – Axel Fontaine Dec 15 '17 at 10:40
  • You could do [this](https://stackoverflow.com/a/19143017/266304) (which requires a new type) or [this](https://stackoverflow.com/a/30406896/266304) (which uses built-in types); and then call one of those over JDBC? Or there's [this post from Tom Kyte](https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:45027262935845)? A logging table might be more appropriate. Depends exactly what you need to achieve. – Alex Poole Dec 15 '17 at 10:43
  • @AlexPoole Thanks. I just clarified the question by adding "without creating additional objects in the database" as those rights will most likely not have been granted. – Axel Fontaine Dec 15 '17 at 10:45
  • 2
    Note that in recent Oracle versions, `dbms_output.get_lines` is overloaded and can return a `dbmsoutput_linesarray`, which should be more JDBC-friendly than the old `dbms_output.chararray`. – William Robertson Dec 15 '17 at 10:46
  • @WilliamRobertson I'll happily accept an answer with code that demonstrates that. – Axel Fontaine Dec 15 '17 at 10:47
  • I don't know Java I'm afraid, but it should just be a case of calling `dbms_output.get_lines` like any other procedure and getting the results back in a `dbmsoutput_linesarray`, which is a predefined public collection type. – William Robertson Dec 15 '17 at 10:52

1 Answers1

31

I've also blogged about this issue here. Here's a snippet that illustrates how this can be done:

try (CallableStatement call = c.prepareCall(
    "declare "
  + "  num integer := 1000;" // Adapt this as needed
  + "begin "

  // You have to enable buffering any server output that you may want to fetch
  + "  dbms_output.enable();"

  // This might as well be a call to third-party stored procedures, etc., whose
  // output you want to capture
  + "  dbms_output.put_line('abc');"
  + "  dbms_output.put_line('hello');"
  + "  dbms_output.put_line('so cool');"

  // This is again your call here to capture the output up until now.
  // The below fetching the PL/SQL TABLE type into a SQL cursor works with Oracle 12c.
  // In an 11g version, you'd need an auxiliary SQL TABLE type
  + "  dbms_output.get_lines(?, num);"

  // Don't forget this or the buffer will overflow eventually
  + "  dbms_output.disable();"
  + "end;"
)) {
    call.registerOutParameter(1, Types.ARRAY, "DBMSOUTPUT_LINESARRAY");
    call.execute();

    Array array = null;
    try {
        array = call.getArray(1);
        System.out.println(Arrays.asList((Object[]) array.getArray()));
    }
    finally {
        if (array != null)
            array.free();
    }
}

The above will print:

[abc, hello, so cool, null]

Note that the ENABLE / DISABLE setting is a connection wide setting, so you can also do this over several JDBC statements:

try (Connection c = DriverManager.getConnection(url, properties);
     Statement s = c.createStatement()) {

    try {
        s.executeUpdate("begin dbms_output.enable(); end;");
        s.executeUpdate("begin dbms_output.put_line('abc'); end;");
        s.executeUpdate("begin dbms_output.put_line('hello'); end;");
        s.executeUpdate("begin dbms_output.put_line('so cool'); end;");

        try (CallableStatement call = c.prepareCall(
            "declare "
          + "  num integer := 1000;"
          + "begin "
          + "  dbms_output.get_lines(?, num);"
          + "end;"
        )) {
            call.registerOutParameter(1, Types.ARRAY, "DBMSOUTPUT_LINESARRAY");
            call.execute();
    
            Array array = null;
            try {
                array = call.getArray(1);
                System.out.println(Arrays.asList((Object[]) array.getArray()));
            }
            finally {
                if (array != null)
                    array.free();
            }
        }
    }
    finally {
        s.executeUpdate("begin dbms_output.disable(); end;");
    }
}

Note also that this will fetch a fixed size of 1000 lines at most. You may need to loop in PL/SQL or poll the database if you want more lines.

Using jOOQ to do this

Note that if you're a jOOQ user, you can auto-fetch server output on your queries:

DSLContext ctx = DSL.using(c, 
    new Settings().withFetchServerOutputSize(10));

And then you should see some DEBUG log output for FetchServerOutputListener, e.g.

DEBUG [LoggerListener           ] Executing query : begin my_procedure(1, 2); end;
DEBUG [FetchServerOutputListener] <output here>       

In addition to the above DEBUG log, you'll get the information in ExecuteContext::serverOutput

This is also mentioned in the linked blog post

A note on calling DBMS_OUTPUT.GET_LINE instead

Previously, there was a now-deleted answer that suggested individual calls to DBMS_OUTPUT.GET_LINE instead, which returns one line at a time. I've benchmarked the approach comparing it with DBMS_OUTPUT.GET_LINES, and the differences are drastic - up to a factor 30x slower when called from JDBC (even if there's not really a big difference when calling the procedures from PL/SQL).

So, the bulk data transferring approach using DBMS_OUTPUT.GET_LINES is definitely worth it. Here's a link to the benchmark:

https://blog.jooq.org/the-cost-of-jdbc-server-roundtrips/

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • Is this the standard way to call a procedure that returns a public SQL collection? Seems like that would be a routine thing to do in Java applications that interact with Oracle. – William Robertson Dec 15 '17 at 11:52
  • 1
    @WilliamRobertson: You wouldn't believe how long Oracle has ignored that feature request! Apparently, Oracle 12cR2's JDBC driver can transmit PL/SQL TABLE types. I haven't played around with it yet, so I omitted it from the answer. Likewise, it took them *forever* to support the PL/SQL BOOLEAN type from JDBC (and you still can't use these types from ordinary SQL statements). Crazy! – Lukas Eder Dec 15 '17 at 12:08
  • But you don't need a PL/SQL table type. Surely JDBC can handle schema-level SQL collection types? – William Robertson Dec 15 '17 at 12:30
  • @WilliamRobertson: You probably mean `dbmsoutput_linesarray`. You're right. I missed the fact that this is a schema level type. Will fix my answer. – Lukas Eder Dec 15 '17 at 13:35
  • @StewAshton: See my updated answer. The type is not anonymous, we know it in this case, so the approach is very simple. Besides, Jpublisher is deprecated, which is why I came up with jOOQ some time ago ;-) (in fact, no, but it's a nice coincidence) – Lukas Eder Dec 15 '17 at 13:41
  • Updated answer looks good :) Yes I meant `dbmsoutput_linesarray` which is a `varray(2147483647) of varchar2(32767)` for some reason, but a convenient schema-level type nonetheless. It has been around for several releases but most Google searches find ancient implementations from before it existed. – William Robertson Dec 15 '17 at 14:05
  • 1
    Updated answer looks great and confirmed to work with Oracle 10, 11 and 12. – Axel Fontaine Dec 15 '17 at 16:04
  • May be its obvious, but possible could save so troubles to others. If the database character set is not equal to the charset of Java (UTF-8) the jar `orai18n.jar` must be on the classpath, otherwise `???` ist shown instead of the strings. – Marmite Bomber Apr 09 '18 at 09:28
  • 1
    where did you get the Types.ARRAY? and what ARRAY should I import? – chlara Apr 01 '19 at 09:17
  • @chlara: `java.sql.Types.ARRAY` – Lukas Eder Apr 01 '19 at 10:25
  • what if another process is using dbms_output? How to get only the output of my procedure? How **dbms_output.get_lines** filters output lines of my procedure? Thanks – JRichardsz Apr 29 '21 at 16:52
  • @JRichardsz: I suggest asking a new question – Lukas Eder Apr 29 '21 at 22:02