游标结果处理
预计阅读时间: 约 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=ResultSet | Java 层面的结果类型 | 映射为 JDBC ResultSet |
typeHandler | CursorTypeHandler | MyBatis 内置的游标类型处理器 |
二、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 数据库兼容性
| 数据库 | 游标支持 | 说明 |
|---|---|---|
| MySQL | 5.0+ | 需要 InnoDB 引擎 |
| Oracle | 完善 | REFCURSOR 原生支持 |
| PostgreSQL | 完善 | REFCURSOR + 事务块 |
| SQL Server | 有限 | 使用 ResultSet 替代游标参数 |
四、要点总结
| 要点 | 说明 |
|---|---|
| 游标优势 | 逐行读取,避免大数据量 OOM |
| 核心配置 | jdbcType=CURSOR + javaType=ResultSet + CursorTypeHandler |
| 资源管理 | 必须使用 try-with-resources 确保关闭 |
| 事务保持 | 游标迭代期间事务不能提交 |
| 单向遍历 | 游标不支持回退和随机访问 |
📝 发现内容有误?点击此处直接编辑