简体   繁体   English

将数据库API游标与JDBC和SQLServer一起使用以选择批处理结果

[英]Using a database API cursor with JDBC and SQLServer to select batch results

SOLVED (See answer below.) 已解决(请参见下面的答案。)

I did not understand my problem within the proper context. 我没有在适当的背景下理解我的问题。 The real issue was that my query was returning multiple ResultSet objects, and I had never come across that before. 真正的问题是我的查询返回了多个ResultSet对象,而我之前从未遇到过。 I have posted code below that solves the problem. 我在下面发布了解决问题的代码。


PROBLEM 问题

I have an SQL Server database table with many thousand rows. 我有一个包含数千行的SQL Server数据库表。 My goal is to pull the data back from the source database and write it to a second database. 我的目标是从源数据库中拉回数据并将其写入第二个数据库。 Because of application memory constraints, I will not be able to pull the data back all at once. 由于应用程序内存的限制,我将无法一次全部拉回数据。 Also, because of this particular table's schema (over which I have no control) there is no good way for me to tick off the rows using some sort of ID column. 另外,由于该特定表的模式(我无法控制),因此我没有使用某种ID列来剔除行的好方法。

A gentleman over at the Database Administrators StackExchange helped me out by putting together something called a database API cursor, and basically wrote this complicated query that I only need to drop my statement into. 数据库管理员StackExchange的一位绅士通过组合一个称为数据库API游标的东西帮助了我,并基本上编写了这个复杂的查询,我只需要将语句放入其中即可。 When I run the query in SQL Management Studio (SSMS) it works great. 当我在SQL Management Studio(SSMS)中运行查询时,它的效果很好。 I get all the data back, a thousand rows at a time. 我取回所有数据,一次一千行。

Unfortunately, when I try to translate this into JDBC code, I get back the first thousand rows only. 不幸的是,当我尝试将其转换为JDBC代码时,我只获得了前几千行。

QUESTION

Is it possible using JDBC to retrieve a database API cursor, pull the first set of rows from it, allow the cursor to advance, and then pull the subsequent sets one at a time? 是否可以使用JDBC检索数据库API游标,从中拉出第一组行,允许游标前进,然后一次拉出后一组? (In this case, a thousand rows at a time.) (在这种情况下,一次执行一千行。)

SQL CODE SQL代码

This gets complicated, so I'm going to break it up. 这变得很复杂,因此我将对其进行分解。

The actual query can be simple or complicated. 实际查询可以是简单的也可以是复杂的。 It doesn't matter. 没关系 I've tried several different queries during my experimentation and they all work. 我在实验过程中尝试了几种不同的查询,它们都可以工作。 You just basically drop it into the the SQL code in the appropriate place. 您只需将其放入适当位置的SQL代码中即可。 So, let's take this simple statement as our query: 因此,让我们将此简单语句作为查询:

SELECT MyColumn FROM MyTable; 

The actual SQL database API cursor is far more complicated. 实际的SQL数据库API游标要复杂得多。 I will print it out below. 我将在下面打印出来。 You can see the above query buried in it: 您可以在其中看到上面的查询:

-- http://dba.stackexchange.com/a/82806
DECLARE @cur INTEGER
    ,
    -- FAST_FORWARD | AUTO_FETCH | AUTO_CLOSE
    @scrollopt INTEGER = 16 | 8192 | 16384
    ,
    -- READ_ONLY, CHECK_ACCEPTED_OPTS, READ_ONLY_ACCEPTABLE
    @ccopt INTEGER = 1 | 32768 | 65536
    ,@rowcount INTEGER = 1000
    ,@rc INTEGER;

-- Open the cursor and return the first 1,000 rows
EXECUTE @rc = sys.sp_cursoropen @cur OUTPUT
    ,'SELECT MyColumn FROM MyTable'
    ,@scrollopt OUTPUT
    ,@ccopt OUTPUT
    ,@rowcount OUTPUT;

IF @rc <> 16 -- FastForward cursor automatically closed
BEGIN
    -- Name the cursor so we can use CURSOR_STATUS
    EXECUTE sys.sp_cursoroption @cur
        ,2
        ,'MyCursorName';

    -- Until the cursor auto-closes
    WHILE CURSOR_STATUS('global', 'MyCursorName') = 1
    BEGIN
        EXECUTE sys.sp_cursorfetch @cur
            ,2
            ,0
            ,1000;
    END;
END;

As I've said, the above creates a cursor in the database and asks the database to execute the statement, keep track (internally) of the data it's returning, and return the data a thousand rows at a time. 就像我说过的那样,上面的代码在数据库中创建了一个游标,并要求数据库执行该语句,(内部)跟踪返回的数据,并一次返回一千行。 It works great. 效果很好。

JDBC CODE JDBC代码

Here's where I'm having the problem. 这就是我遇到的问题。 I have no compilation problems or run-time problems with my Java code. 我的Java代码没有编译问题或运行时问题。 The problem I am having is that it returns only the first thousand rows. 我遇到的问题是它仅返回前一千行。 I don't understand how to utilize the database cursor properly. 我不明白如何正确利用数据库游标。 I have tried variations on the Java basics: 我已经尝试了Java基础的各种变化:

// Hoping to get all of the data, but I only get the first thousand.
ResultSet rs = stmt.executeQuery(fq.getQuery());
while (rs.next()) {
    System.out.println(rs.getString("MyColumn"));
}

I'm not surprised by the results, but all of the variations I've tried produce the same results. 我对结果并不感到惊讶,但是我尝试过的所有变化都能产生相同的结果。

From my research it seems like the JDBC does something with database cursors when the database is Oracle, but you have to set the data type returned in the result set as an Oracle cursor object. 从我的研究看来,当数据库为Oracle时,JDBC似乎对数据库游标做了一些操作,但是您必须将结果集中返回的数据类型设置为Oracle游标对象。 I'm guessing there is something similar with SQL Server, but I have been unable to find anything yet. 我猜想SQL Server有一些类似的东西,但是我还找不到任何东西。

Does anyone know of a way? 有人知道吗?

I'm including example Java code in full (as ugly as that gets). 我完整地包含了示例Java代码(这很丑陋)。

// FancyQuery.java

import java.sql.*;

public class FancyQuery {

    // Adapted from http://dba.stackexchange.com/a/82806
    String query = "DECLARE @cur INTEGER\n"
                 + "    ,\n"
                 + "    -- FAST_FORWARD | AUTO_FETCH | AUTO_CLOSE\n"
                 + "    @scrollopt INTEGER = 16 | 8192 | 16384\n"
                 + "    ,\n"
                 + "    -- READ_ONLY, CHECK_ACCEPTED_OPTS, READ_ONLY_ACCEPTABLE\n"
                 + "    @ccopt INTEGER = 1 | 32768 | 65536\n"
                 + "    ,@rowcount INTEGER = 1000\n"
                 + "    ,@rc INTEGER;\n"
                 + "\n"
                 + "-- Open the cursor and return the first 1,000 rows\n"
                 + "EXECUTE @rc = sys.sp_cursoropen @cur OUTPUT\n"
                 + "    ,'SELECT MyColumn FROM MyTable;'\n"
                 + "    ,@scrollopt OUTPUT\n"
                 + "    ,@ccopt OUTPUT\n"
                 + "    ,@rowcount OUTPUT;\n"
                 + "    \n"
                 + "IF @rc <> 16 -- FastForward cursor automatically closed\n"
                 + "BEGIN\n"
                 + "    -- Name the cursor so we can use CURSOR_STATUS\n"
                 + "    EXECUTE sys.sp_cursoroption @cur\n"
                 + "        ,2\n"
                 + "        ,'MyCursorName';\n"
                 + "\n"
                 + "    -- Until the cursor auto-closes\n"
                 + "    WHILE CURSOR_STATUS('global', 'MyCursorName') = 1\n"
                 + "    BEGIN\n"
                 + "        EXECUTE sys.sp_cursorfetch @cur\n"
                 + "            ,2\n"
                 + "            ,0\n"
                 + "            ,1000;\n"
                 + "    END;\n"
                 + "END;\n";

    public String getQuery() {
        return this.query;
    }

    public static void main(String[ ] args) throws Exception {

        String dbUrl = "jdbc:sqlserver://tc-sqlserver:1433;database=MyBigDatabase";
        String user = "mario";
        String password = "p@ssw0rd";
        String driver = "com.microsoft.sqlserver.jdbc.SQLServerDriver";

        FancyQuery fq = new FancyQuery();

        Class.forName(driver);

        Connection conn = DriverManager.getConnection(dbUrl, user, password);
        Statement stmt = conn.createStatement();

        // We expect to get 1,000 rows at a time.
        ResultSet rs = stmt.executeQuery(fq.getQuery());
        while (rs.next()) {
            System.out.println(rs.getString("MyColumn"));
        }

        // Alas, we've only gotten 1,000 rows, total.

        rs.close();
        stmt.close();
        conn.close();
    }
}

I figured it out. 我想到了。

stmt.execute(fq.getQuery());

ResultSet rs = null;

for (;;) {
    rs = stmt.getResultSet();
    while (rs.next()) {
        System.out.println(rs.getString("MyColumn"));
    }
    if ((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1)) {
        break;
    }
}

if (rs != null) {
    rs.close();
}

After some additional googling, I found a bit of code posted back in 2004: 经过一些额外的谷歌搜索之后,我发现了一些早在2004年发布的代码:

http://www.coderanch.com/t/300865/JDBC/databases/SQL-Server-JDBC-Registering-cursor http://www.coderanch.com/t/300865/JDBC/databases/SQL-Server-JDBC-Registering-cursor

The gentleman who posted the snippet that I found helpful (Julian Kennedy) suggested: "Read the Javadoc for getUpdateCount() and getMoreResults() for a clear understanding." 张贴了我觉得有用的代码片段的绅士(朱利安·肯尼迪)建议:“阅读Javadoc的getUpdateCount()和getMoreResults()可以清楚地理解。” I was able to piece it together from that. 这样我就可以将其拼凑起来。

Basically, I don't think I understood my problem well enough at the outset in order to phrase it correctly. 基本上,我认为我一开始对问题的理解不够,无法正确地表达它。 What it comes down to is that my query will be returning the data in multiple ResultSet instances. 归结为,我的查询将在多个ResultSet实例中返回数据。 What I needed was a way to not merely iterate through each row in a ResultSet but, rather, iterate through the entire set of ResultSets. 我需要的是一种不仅遍历ResultSet中的每一行,而且遍历整个ResultSet集的方法。 That's what the code above does. 上面的代码就是这样做的。

If you want all records from the table, just do "Select * from table". 如果要从表中获取所有记录,只需执行“从表中选择*”。

The only reason to retrieve in chunks is if there is some intermediate place for the data: eg if you are showing it on the screen, or storing it in memory. 分块检索的唯一原因是数据是否存在中间位置:例如,如果您正在屏幕上显示数据或将其存储在内存中。

If you are simply reading from one and inserting to another, just read everything from the first.You will not get any better performance by trying to retrieve in batches. 如果您只是从一个中读取并插入到另一个中,则只需从头开始读取所有内容。尝试分批检索将不会获得任何更好的性能。 If there is a difference, it will be negative. 如果存在差异,则为负。 Frame your query in a way that brings back everything. 以带回所有内容的方式来构建查询。 The JDBC software will handle all the other breaking-up and reconstituting that you need. JDBC软件将处理您需要的所有其他分解和重构。

However, you should batch the update/insert side of things. 但是,您应该对事物进行更新/插入。

The set-up would create two statements on the two connections: 该设置将在两个连接上创建两个语句:

Statement stmt = null;
ResultSet rs = null;
PreparedStatement insStmt = null;

stmt = conDb1.createStatement();
insStmt = conDb2.prepareStament("insert into tgt_db2_table (?,?,?,?,?......etc. ?,?) ");
rs = stmt.executeQuery("select * from src_db1_table");

Then, loop over the select as normal, but use batching on the target. 然后,像往常一样循环选择,但对目标使用批处理。

    int batchedRecordCount = 0;
    while (rs.next()) {
        System.out.println(rs.getString("MyColumn"));

        //Here you read values from the cursor and set them to the insStmt ...
        String field1 = rs.getString(1);
        String field2 = rs.getString(2);
        int field3 = rs.getInt(3);
        //--- etc. 

        insStmt.setString(1, field1);
        insStmt.setString(2, field2);
        insStmt.setInt(3, field3);

        //----- etc. for all the fields

        batchedRecordCount++;
        insStmt.addBatch();
        if (batchRecordCount > 1000) {
          insStmt.executeBatch();
        }
    }
    if (batchRecordCount > 0) {
       //Finish of the final (partial) set of records
       insStmt.executeBatch();
    }

    //Close resources...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM