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每次读取一个分页大小的数据,在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