全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 分钟 ✍️ juanwangdev

游标结果处理

预计阅读时间: 约 8 分钟

当存储过程需要返回大规模数据集时,使用游标(Cursor)可以在不一次性加载全部数据到内存的情况下逐行处理结果,有效降低内存消耗。MyBatis 提供了专门的 Cursor<T> 类型来处理游标结果集。

一、存储过程返回游标

1.1 数据库端游标定义

MySQL 存储过程示例:

SQL
DELIMITER //
CREATE PROCEDURE get_large_user_list(
    IN p_dept_id INT,
    OUT p_cursor CURSOR
)
BEGIN
    OPEN p_cursor FOR 
        SELECT u.id, u.name, u.email, u.create_time
        FROM users u
        WHERE u.dept_id = p_dept_id
        ORDER BY u.create_time DESC;
END //
DELIMITER ;

PostgreSQL 风格:

SQL
CREATE OR REPLACE FUNCTION get_user_cursor(p_dept_id INTEGER)
RETURNS REFCURSOR AS $$
DECLARE
    user_cursor REFCURSOR;
BEGIN
    OPEN user_cursor FOR
        SELECT id, name, email, create_time
        FROM users
        WHERE dept_id = p_dept_id
        ORDER BY create_time DESC;
    RETURN user_cursor;
END;
$$ LANGUAGE plpgsql;

1.2 MyBatis 映射配置

XML
<select id="getUserCursor" statementType="CALLABLE" parameterType="map">
    {call get_large_user_list(
        #{deptId, mode=IN, jdbcType=INTEGER},
        #{userCursor, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, 
         typeHandler=org.apache.ibatis.type.CursorTypeHandler}
    )}
</select>

关键属性说明:

属性作用
jdbcType=CURSOR声明数据库游标类型告知驱动层按游标处理
javaType=ResultSetJava 层面的结果类型映射为 JDBC ResultSet
typeHandlerCursorTypeHandlerMyBatis 内置的游标类型处理器

二、ResultSetTypeHandler

2.1 CursorTypeHandler 工作原理

MyBatis 内置的 CursorTypeHandler 负责将 JDBC ResultSet 包装为 MyBatis 的 Cursor<T> 对象:

Java
// org.apache.ibatis.type.CursorTypeHandler 简化理解
public class CursorTypeHandler extends BaseTypeHandler<Cursor<?>> {
    @Override
    public Cursor getNullableResult(ResultSet rs, String columnName) {
        return (Cursor<?>) rs.getObject(columnName);
    }
}

该处理器在获取结果时不会立即遍历 ResultSet,而是保持游标打开状态,由调用方控制迭代节奏。

2.2 自定义 TypeHandler 处理复杂游标

当游标返回的数据需要特殊处理时(如 JSON 解析、加密解密),可编写自定义 TypeHandler:

Java
@MappedTypes(Cursor.class)
@MappedJdbcTypes(JdbcType.CURSOR)
public class JsonCursorTypeHandler extends BaseTypeHandler<Cursor<User>> {

    @Override
    public Cursor<User> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        ResultSet cursorRs = (ResultSet) rs.getObject(columnName);
        return new Cursor<User>() {
            private Iterator<User> iterator = new Iterator<User>() {
                public boolean hasNext() {
                    try {
                        return cursorRs.next();
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                }
                public User next() {
                    try {
                        String json = cursorRs.getString(1);
                        return parseJsonToUser(json);
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                }
            };
            // 其他方法...
        };
    }
}

三、游标迭代与使用

3.1 Mapper 接口定义

Java
public interface UserMapper {
    /**
     * 返回游标类型的结果,按需迭代
     */
    Cursor<User> getUserCursor(@Param("deptId") Integer deptId);
}

XML 配置使用 resultType 指定游标内每行的类型:

XML
<select id="getUserCursor" statementType="CALLABLE" resultType="User">
    {call get_large_user_list(
        #{deptId, mode=IN, jdbcType=INTEGER},
        #{userCursor, mode=OUT, jdbcType=CURSOR, 
         javaType=java.sql.ResultSet, 
         typeHandler=org.apache.ibatis.type.CursorTypeHandler}
    )}
</select>

3.2 游标迭代方式

方式一:增强 for 循环(推荐)

Java
try (Cursor<User> cursor = userMapper.getUserCursor(10)) {
    for (User user : cursor) {
        processUser(user);  // 逐行处理,内存中仅保留当前行
    }
}
// try-with-resources 自动关闭游标

方式二:迭代器模式

Java
try (Cursor<User> cursor = userMapper.getUserCursor(10)) {
    Iterator<User> it = cursor.iterator();
    while (it.hasNext()) {
        User user = it.next();
        if (user.getStatus() == 0) {
            continue;  // 跳过无效用户
        }
        exportUser(user);
    }
}

方式三:配合 Stream API(需自定义转换)

Java
try (Cursor<User> cursor = userMapper.getUserCursor(10)) {
    // 注意:Cursor 不直接支持 stream(),需手动转换
    Spliterator<User> spliterator = Spliterators.spliteratorUnknownSize(
        cursor.iterator(), Spliterator.ORDERED);
    Stream<User> stream = StreamSupport.stream(spliterator, false);
    
    long activeCount = stream.filter(u -> u.getStatus() == 1).count();
}

3.3 游标与 List 对比

特性List<T>Cursor<T>
内存占用全部加载到内存仅持有当前行
适用场景数据量较小大数据量导出
关闭方式无需手动关闭必须关闭(try-with-resources)
重复遍历支持多次仅支持单次向前遍历
性能首次访问快首行快,整体持平

四、注意事项与最佳实践

4.1 游标必须关闭

游标持有数据库连接资源,使用后必须关闭:

Java
// 错误:游标未关闭,导致连接泄漏
Cursor<User> cursor = userMapper.getUserCursor(10);
for (User user : cursor) {
    processUser(user);
}

// 正确:使用 try-with-resources
try (Cursor<User> cursor = userMapper.getUserCursor(10)) {
    for (User user : cursor) {
        processUser(user);
    }
}

4.2 事务管理

游标迭代期间需保持事务打开状态:

Java
// 建议开启事务,确保游标读取期间数据一致性
SqlSession session = sqlSessionFactory.openSession();
try {
    try (Cursor<User> cursor = session.getMapper(UserMapper.class)
            .getUserCursor(10)) {
        for (User user : cursor) {
            processUser(user);
        }
    }
    session.commit();
} finally {
    session.close();
}

4.3 数据库兼容性

数据库游标支持说明
MySQL5.0+需要 InnoDB 引擎
Oracle完善REFCURSOR 原生支持
PostgreSQL完善REFCURSOR + 事务块
SQL Server有限使用 ResultSet 替代游标参数

四、要点总结

要点说明
游标优势逐行读取,避免大数据量 OOM
核心配置jdbcType=CURSOR + javaType=ResultSet + CursorTypeHandler
资源管理必须使用 try-with-resources 确保关闭
事务保持游标迭代期间事务不能提交
单向遍历游标不支持回退和随机访问

📝 发现内容有误?点击此处直接编辑

← 上一篇 存储过程调用
下一篇 → 输入输出参数
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库