JDBC setFetchSize原理

JDBC setFetchSize(int)

​ 以下研究基于pgjdbc42.2.5源码进行分析,对于JDBC的基本原理相通。

fetch是什么

​ 对于数据库查询返回的结果集,有两种方式,一种是包含所有行的,另一种是游标(cursor)类型的结果集,用来被检索行,可以一次读取该查询结果的一些行,这样做的原因之一是包含大量行时避免内存不足。

​ 对cursor的使用,可以通过fetch命令从游标中检索下一(N)行到目标中。

​ 例如:

FETCH cursor1 INTO rowvar;
FETCH cursor2 INTO foo, bar, baz;
FETCH LAST FROM cursor3 INTO x, y;
FETCH RELATIVE-2 FROM cursor4 INTO x;

​ 那么,JDBC便可以通过fetchSize的值与定义的cursorName来一次次的获取结果了。

PGJDBC如何通过FetchSize获取数据库结果集的数据


JDBC fetch获取数据流程图

​ 简言之,JDBC每次读取一个分页大小的数据,在next()时,如果JDBC本地的数据遍历结束,那么通过Portal和fetchSize向数据库请求下一个分页的数据。

类:org.postgresql.jdbc.PgStatement

private void executeInternal(CachedQuery cachedQuery, ParameterList queryParameters, int flags) throws SQLException {
    ...
	// 判断是否使用cursor类型的结果集,条件包含为fetchSize > 0且ResultSet.TYPE_FORWARD_ONLY且非自动提交
    if (fetchSize > 0 && !wantsScrollableResultSet() && !connection.getAutoCommit()
        && !wantsHoldableResultSet()) {
      flags |= QueryExecutor.QUERY_FORWARD_CURSOR;
    }
    ...
    connection.getQueryExecutor().execute(queryToExecute, queryParameters, handler2, 0, 0, flags2);
    ...
}

类:org.postgresql.core.v3.QueryExecutorImpl

private void sendOneQuery(SimpleQuery query, SimpleParameterList params, int maxRows,
      int fetchSize, int flags) throws IOException {
    ...
    boolean noResults = (flags & QueryExecutor.QUERY_NO_RESULTS) != 0;
    boolean noMeta = (flags & QueryExecutor.QUERY_NO_METADATA) != 0;
    boolean describeOnly = (flags & QueryExecutor.QUERY_DESCRIBE_ONLY) != 0;
    //根据之前的标记位判断是否创建一个Portal对应数据库结果集的Cursor
    boolean usePortal = (flags & QueryExecutor.QUERY_FORWARD_CURSOR) != 0 && !noResults && !noMeta && fetchSize > 0 && !describeOnly;
    ...
    // 计算本次执行需要fetch多少行数据
    int rows;
    if (noResults) {
      rows = 1; 
    } else if (!usePortal) {
      rows = maxRows; // 一次性全部取出
    } else if (maxRows != 0 && fetchSize > maxRows) {
      // 如果fetchSize大于结果集的最大行数,取maxRows行数据
      rows = maxRows;
    } else {
      rows = fetchSize; // maxRows > fetchSize
    }
    
    sendParse(query, params, oneShot);
    ...
    // 构建一个Portal对应Cursor类型结果集,通过该Portal实现每次获取结果集时可以找到同一个Cursor结果集
    Portal portal = null;
    if (usePortal) {
      String portalName = "C_" + (nextUniqueID++);
      portal = new Portal(query, portalName);
    }

    sendBind(query, params, portal, noBinaryTransfer);
    ...
    sendExecute(query, portal, rows);
}

类:org.postgresql.jdbc.PgResultSet

public boolean next() throws SQLException {
    ...
    // 当一个分页的数据处理结束,需要请求下一个分片的数据
    row_offset += rows.size(); // 偏移量更新

    int fetchRows = fetchSize; // 默认fetchSize为一个分片
    if (maxRows != 0) {
        if (fetchRows == 0 || row_offset + fetchRows > maxRows) {
            // 当最后剩余的行数不足以一个fetchSize分片,取出剩余所有行
            fetchRows = maxRows - row_offset;
        }
    }

    // 重新fetch数据,并且更新ResultSet
    connection.getQueryExecutor().fetch(cursor, new CursorResultHandler(), fetchRows);
    ...
}

​ 关于portal与Cursor的关系,可以查看org.postgresql.core.ResultHandler的接口与其实现类。具体为CursorResultHandler实现类,在processHandler封装结果集时,将ResultSet.cursor = currentPortal。代码如下:

protected void processResults(ResultHandler handler, int flags) throws IOException {
	...
	case 's': { // 执行结束
        // nb: this appears *instead* of CommandStatus.
        // Must be a SELECT if we suspended, so don't worry about it.

        pgStream.receiveInteger4(); // len, discarded
        LOGGER.log(Level.FINEST, " <=BE PortalSuspended");

        ExecuteRequest executeData = pendingExecuteQueue.removeFirst();
        SimpleQuery currentQuery = executeData.query;
        Portal currentPortal = executeData.portal;

        Field[] fields = currentQuery.getFields();
        if (fields != null && tuples == null) {
            // When no results expected, pretend an empty resultset was returned
            // Not sure if new ArrayList can be always replaced with emptyList
            tuples = noResults ? Collections.<byte[][]>emptyList() : new ArrayList<byte[][]>();
        }

        handler.handleResultRows(currentQuery, fields, tuples, currentPortal);
        tuples = null;
        break;
    }
    ...
}

举个例子

// table : tb_test(id int, name text)
	public static void testGetCursorName() throws Exception {
		Connection conn = getConnectionLocation();
        conn.setAutoCommit(false);
		PreparedStatement ps = conn.prepareStatement("select * from tb_test", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
		ps.setFetchSize(5);
		ResultSet rs = ps.executeQuery();
		while(rs.next()) {
			System.out.println(rs.getInt(1) + " " + rs.getString(2));
            // 对当前行数据进行修改并持久化, 对id =2的数据修改name为"x",并持久化,与fetchSize无关
			if (rs.getInt(1) == 2) {
				rs.updateString(2, "x");
				rs.updateRow();
			}
			System.out.println("end:" + rs.getInt(1) + " " + rs.getString(2));
		}
        conn.commit();
	}

必要条件:

1.connection非自动提交

2.ResultSet.TYPE_FORWARD_ONLY

3.fetchSize > 0

延伸:使用ResultSet对当前行的数据进行update

//TODO

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值