MyBatis 分页查询 + Feign 数据补充实战指南

MyBatis 分页查询 + Feign 数据补充实战指南

一、概述

在微服务架构中,一个常见的列表查询模式是:先从本地数据库分页查询基础数据,再通过远程服务(Feign)补充关联信息。这种模式避免了跨库 JOIN,保持了服务间的解耦,但也带来了性能和一致性方面的挑战。

本文以一个"员工考勤记录查询"场景为例,系统介绍 MyBatis 动态 SQL 分页查询、Feign 远程数据补充、枚举翻译、N+1 问题优化等核心技术点。


二、示例场景

一个考勤管理系统,需要实现考勤记录分页查询:

  • 本地表存储:员工ID、打卡时间、打卡状态
  • 远程服务提供:员工姓名、部门名称、职位信息
  • 需要支持:多条件筛选、分页、枚举翻译、操作人信息补充

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

三、分层架构

复制代码
Controller(入参校验/默认值)
    |
Service(业务编排:查询 + 数据补充 + 枚举翻译)
    |
Mapper/DAO(MyBatis 动态 SQL 分页查询)
    |
Feign(远程调用补充关联数据)

四、MyBatis 动态 SQL 分页查询

4.1 Mapper 接口

java 复制代码
public interface AttendanceMapper {

    List<AttendanceResultDto> listAttendancePager(
        @Param("param") AttendanceQueryParamsDto param,
        @Param("pageSize") Integer pageSize,
        @Param("pageNum") Integer pageNum);
}

4.2 XML 动态 SQL

xml 复制代码
<select id="listAttendancePager"
    resultType="com.example.dto.AttendanceResultDto">
    SELECT
        a.id,
        a.employee_id AS employeeId,
        a.check_time AS checkTime,
        a.check_type AS checkType,
        a.status AS status,
        a.create_user_id AS createUserId,
        d.dept_code AS deptCode,
        d.dept_name AS deptName
    FROM attendance_record a
    LEFT JOIN department d ON a.dept_id = d.id
    <where>
        <!-- 单字段模糊搜索 -->
        <if test="param.employeeName != null and param.employeeName != ''">
            AND a.employee_name LIKE CONCAT('%', #{param.employeeName}, '%')
        </if>

        <!-- 精确匹配 -->
        <if test="param.status != null">
            AND a.status = #{param.status}
        </if>

        <!-- 多选 IN 查询 -->
        <if test="param.deptCodeList != null and !param.deptCodeList.isEmpty()">
            AND d.dept_code IN
            <foreach collection="param.deptCodeList" item="deptCode"
                     open="(" separator="," close=")">
                #{deptCode}
            </foreach>
        </if>

        <!-- 时间范围查询 -->
        <if test="param.checkTimeStart != null and param.checkTimeStart != ''">
            AND a.check_time &gt;= #{param.checkTimeStart}
        </if>
        <if test="param.checkTimeEnd != null and param.checkTimeEnd != ''">
            AND a.check_time &lt;= #{param.checkTimeEnd}
        </if>
    </where>
    ORDER BY a.id DESC
</select>

4.3 动态 SQL 核心标签

标签 用途 示例
<where> 自动处理 WHERE 和多余的 AND/OR 条件为空时不生成 WHERE
<if> 条件判断,参数不为空时拼接 SQL 模糊搜索、精确匹配
<foreach> 遍历集合,生成 IN 子句 多选筛选
<choose>/<when>/<otherwise> 类似 switch-case 互斥条件
<trim> 自定义前缀/后缀处理 复杂条件拼接
<set> UPDATE 语句动态字段 部分字段更新

4.4 模糊搜索的安全写法

xml 复制代码
<!-- 正确:使用 CONCAT + #{} 防止 SQL 注入 -->
AND a.employee_name LIKE CONCAT('%', #{param.employeeName}, '%')

<!-- 错误:使用 ${} 存在 SQL 注入风险 -->
AND a.employee_name LIKE '%${param.employeeName}%'

4.5 时间范围查询注意事项

xml 复制代码
<!-- XML 中 > 和 < 需要转义 -->
AND a.check_time &gt;= #{param.checkTimeStart}
AND a.check_time &lt;= #{param.checkTimeEnd}

<!-- 或者使用 CDATA 包裹 -->
<![CDATA[ AND a.check_time >= #{param.checkTimeStart} ]]>

五、分页机制

5.1 PageHelper 插件方式

java 复制代码
PageHelper.startPage(pageNum, pageSize);
List<AttendanceResultDto> list = attendanceMapper.listAttendance(param);
PageInfo<AttendanceResultDto> pageInfo = new PageInfo<>(list);

5.2 参数传递方式

java 复制代码
List<AttendanceResultDto> list = attendanceMapper.listAttendancePager(
    param, param.getPageSize(), param.getPageNum());
YlhPageInfo<AttendanceResultDto> pageInfo = new YlhPageInfo<>(list);

5.3 两种方式对比

维度 PageHelper 参数传递
侵入性 低(调用前设置) 低(参数传入)
灵活性 高(支持多种数据库) 中(依赖拦截器实现)
线程安全 ThreadLocal 存储 无状态
适用场景 通用 项目有自定义分页拦截器

六、Feign 远程数据补充

6.1 基本模式

java 复制代码
@Service
public class AttendanceServiceImpl implements AttendanceService {

    @Resource
    private AttendanceMapper attendanceMapper;
    @Resource
    private EmployeeFeign employeeFeign;

    @Override
    public YlhPageInfo<AttendanceResultDto> listAttendancePager(
            AttendanceQueryParamsDto paramsDto) {
        // 1. 分页查询本地表
        List<AttendanceResultDto> resultList = attendanceMapper.listAttendancePager(
            paramsDto, paramsDto.getPageSize(), paramsDto.getPageNum());

        if (CheckEmptyUtil.isNotEmpty(resultList)) {
            // 2. 遍历补充远程数据
            resultList.forEach(dto -> {
                enrichEmployeeInfo(dto);
                translateEnums(dto);
            });
        }

        return new YlhPageInfo<>(resultList);
    }
}

6.2 N+1 问题与优化

问题:每条记录都单独调用一次 Feign,一页 20 条就要调用 20 次。

java 复制代码
// 反例:N+1 调用
resultList.forEach(dto -> {
    RestControllerResult<EmployeeDto> result =
        employeeFeign.getEmployeeById(dto.getEmployeeId());
});

优化:批量查询 + Map 映射

java 复制代码
// 正例:一次批量查询
List<Integer> employeeIds = resultList.stream()
    .map(AttendanceResultDto::getEmployeeId)
    .distinct()
    .collect(Collectors.toList());

RestControllerResult<List<EmployeeDto>> batchResult =
    employeeFeign.listEmployeeByIds(employeeIds);

Map<Integer, EmployeeDto> employeeMap = batchResult.getData().stream()
    .collect(Collectors.toMap(EmployeeDto::getId, e -> e, (k1, k2) -> k1));

resultList.forEach(dto -> {
    EmployeeDto employee = employeeMap.get(dto.getEmployeeId());
    if (employee != null) {
        dto.setEmployeeName(employee.getName());
        dto.setPosition(employee.getPosition());
    }
});

6.3 Feign 调用的防御性编程

java 复制代码
private void enrichEmployeeInfo(AttendanceResultDto dto) {
    if (CheckEmptyUtil.isEmpty(dto.getEmployeeId())) {
        return;
    }
    try {
        RestControllerResult<EmployeeDto> result =
            employeeFeign.getEmployeeById(dto.getEmployeeId());

        // 三重判空
        if (CheckEmptyUtil.isNotEmpty(result)
                && Objects.equals(result.getSuccess(), Boolean.TRUE)
                && CheckEmptyUtil.isNotEmpty(result.getData())) {
            EmployeeDto employee = result.getData();
            dto.setEmployeeName(employee.getName());
            dto.setDeptName(employee.getDeptName());
        }
    } catch (Exception e) {
        log.warn("Feign调用失败, employeeId={}, error={}",
            dto.getEmployeeId(), e.getMessage());
    }
}

七、枚举翻译

7.1 枚举定义

java 复制代码
public enum AttendanceStatusEnum {
    NORMAL(1, "正常"),
    LATE(2, "迟到"),
    EARLY_LEAVE(3, "早退"),
    ABSENT(4, "缺勤");

    private Integer code;
    private String name;

    AttendanceStatusEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    public static String getNameByCode(Integer code) {
        if (code == null) return null;
        for (AttendanceStatusEnum e : values()) {
            if (e.code.equals(code)) return e.name;
        }
        return null;
    }
}

7.2 设计原则

  • 数据库存编码,不存中文名称
  • Service 层负责翻译
  • 枚举类提供静态方法 getNameByCode

八、操作人信息格式化

java 复制代码
// 格式:工号 姓名(中间空格分隔)
private String formatOperatorName(String staffNo, String staffName) {
    StringBuilder sb = new StringBuilder();
    if (CheckEmptyUtil.isNotEmpty(staffNo)) {
        sb.append(staffNo);
    }
    if (CheckEmptyUtil.isNotEmpty(staffName)) {
        if (sb.length() > 0) {
            sb.append(" ");
        }
        sb.append(staffName);
    }
    return sb.toString().trim();
}

九、性能优化建议

优化点 说明
SQL 层尽量过滤 能在 SQL 中过滤的条件不要放到 Java 层
避免 N+1 优先使用批量查询接口
合理设置分页大小 每页 20~50 条
Feign 调用加 try-catch 远程调用失败不影响主流程
索引覆盖筛选字段 WHERE 条件中的字段确保有索引
LEFT JOIN 控制数量 避免过多表关联
缓存热点数据 部门、枚举等变化少的数据可以缓存

十、常见问题

10.1 LEFT JOIN 导致数据重复

sql 复制代码
-- 如果关联表中一个 ID 对应多条记录,LEFT JOIN 会产生重复行
-- 解决:加 LIMIT 1 子查询,或在业务层去重,或确保关联字段唯一

10.2 模糊搜索性能

sql 复制代码
-- LIKE '%xxx%' 无法使用索引(左模糊)
-- 数据量大时考虑:全文索引、Elasticsearch、限制最少输入字符数

10.3 时间范围查询索引

sql 复制代码
-- 确保时间字段有索引
CREATE INDEX idx_check_time ON attendance_record (check_time);

十一、最佳实践清单

  1. SQL 层过滤优先:本地表有的字段在 SQL 中过滤,远程数据只做展示补充
  2. 分页参数设默认值:Controller 层处理,避免全表扫描
  3. Feign 调用三重判空:结果不为空 + success=true + data 不为空
  4. Feign 调用加 try-catch:单条失败不影响整体列表
  5. 批量优于逐条:有批量接口时优先使用,减少网络开销
  6. 枚举翻译在 Service 层:不在 SQL 中硬编码中文
  7. 使用 CONCAT + #{} 防注入:模糊搜索不用 ${}
  8. XML 中转义特殊字符>&gt;<&lt;
  9. ORDER BY 配合索引:排序字段建议有索引
  10. 日志记录关键参数:方便排查分页查询问题
相关推荐
一起逃去看海吧1 小时前
对接LangSmith
java·前端·数据库
wyhwust1 小时前
web应用技术-第一次课后作业
java·前端·数据库
hef2881 小时前
SQL角色分组统计与功能扩展实战指南
数据库
隐退山林1 小时前
JavaEE进阶:MyBatis操作数据库(进阶)
数据库·java-ee·mybatis
我是一颗柠檬2 小时前
【MySQL全面教学】MySQL锁机制与并发控制Day10(2026年)
数据库·sql·mysql·database
代码中介商2 小时前
B树:数据库索引的高效基石
数据结构·数据库
fen_fen2 小时前
Oracle12,新增自增主键表和批量插入数据
数据库·sql·mysql
deepin_sir2 小时前
11 - 模块与包
前端·数据库·python
念恒123062 小时前
MySQL索引
数据库·mysql