1. 概述
在数据库开发中,跨表查询和分页返回是最常见的需求之一。这篇文章就专门聊聊如何基于MyBatis-Plus框架,实现一个靠谱的跨表查询分页功能。我们会以供应商物料的实际查询场景为例,一步步拆解多表关联查询,并最终拿到一个标准的分页对象。

2. 技术栈
先简单交代一下我们用到的技术组件:
- ORM框架:MyBatis-Plus
- 数据库:MySQL(使用LIMIT进行分页)
- 分页组件:MyBatis-Plus Page对象
- 架构模式:Repository + Mapper + XML三层架构
3. 核心实现原理
3.1 分页策略
这里不搞什么玄乎的自动分页,我们走的是手动控制路线。整个过程分两步:
- 查总数:先跑一个COUNT查询,拿到符合条件的数据总量。
- 查数据:根据偏移量(offset)和页大小(size),精准捞取当前页的数据。
3.2 为什么选择手动分页
你可能要问了,MyBatis-Plus不是自带分页插件吗?为啥还自己动手?原因其实很简单:
- 跨表查询往往涉及LEFT JOIN,如果让框架自动分页,生成的SQL有时并不高效,甚至可能漏数据。
- 手动控制SQL,你就能对每一步的执行细节心里有数,优化起来也更直接。
- 当查询条件复杂、动态拼接的情况多时,手动分页的灵活性和可控性就体现出来了。
4. 代码实现详解
4.1 参数对象设计
首先,我们需要一个参数对象来承载查询条件。它继承了PageQueryParam,这样就有了currentPage和pageSize这两个核心分页参数。
@Getter
@Setter
@ToString
public class SupplierIdParam extends PageQueryParam {
private Long tenantId; // 租户ID
private Long supplierId; // 供应商ID
private String materialCategoryExternalCode; // 物料分类外部编码
private String materialCategoryName; // 物料分类名称
private String materialName; // 物料名称
private String materialExternalCode; // 物料编码
private String materialStatus; // 物料状态
private String pricingType; // 定价类型
// ... 其他字段
}
设计这个对象时,有几点值得留意:
- 继承设计非常实用,分页参数不用重复写。
- 所有字段都支持多维度动态查询,条件可组合、可灵活增减。
- 字段类型都用包装类,方便判断是否传参——这在动态SQL里是基础操作。
4.2 领域对象设计
接着是承载数据的实体,它映射到数据库的t_supplier_material表:
@Getter
@Setter
@TableName(value = "t_supplier_material", autoResultMap = true)
public class SupplierMaterialDO implements BaseEntity {
private Long id;
private Long tenantId;
private Long supplierDataId;
private Long materialCategoryId;
private String materialCategoryExternalCode;
private String materialCategoryName;
private Long materialId;
private String externalCode;
private String name;
// ... 其他字段
}
这里有两个细节:
@TableName里的autoResultMap = true,为复杂结果映射开了绿灯。- 实现了
BaseEntity接口,保证了通用字段的统一管理。
4.3 Repository层实现
到了组织查询逻辑的环节,看SupplierMaterialRepository:
@Repository public class SupplierMaterialRepository extends HussarServiceImpl{ @Resource private SupplierMaterialMapper supplierMaterialMapper; public Page queryMaterialsBySupplierId(SupplierIdParam param) { Long tenantId = param.getTenantId(); // 第一步:查询总数 Long total = supplierMaterialMapper.queryMaterialsBySupplierIdCount(param, tenantId); // 第二步:创建分页对象 Page page = new Page<>(param.getCurrentPage(), param.getPageSize(), total); // 第三步:查询分页数据 List supplierMaterialDOList = supplierMaterialMapper.queryMaterialsBySupplierId(param, tenantId, page.offset(), page.getSize()); // 第四步:设置结果集 if (CollectionUtils.isNotEmpty(supplierMaterialDOList)) { page.setRecords(supplierMaterialDOList); } return page; } }
实现要点其实很清晰:
- 计数查询单独执行,不走数据查询的弯路,能有效提升性能。
page.offset()自动帮我们算出偏移量,省心不少。- 设置结果集前先判空,避免空指针这种低级问题。
- 租户ID全程携带,多租户隔离的机制不能丢。
4.4 Mapper接口定义
数据访问层接口,定义了两个核心方法:
@Mapper public interface SupplierMaterialMapper extends BaseMapper, HussarMapper { /** * 查询总数 */ Long queryMaterialsBySupplierIdCount(@Param("param") SupplierIdParam param, @Param("tenantId") Long tenantId); /** * 查询分页数据 */ List queryMaterialsBySupplierId(@Param("param") SupplierIdParam param, @Param("tenantId") Long tenantId, @Param("offset") Long offset, @Param("size") Long size); }
设计上,有几点值得注意:
- 用
@Param把参数名标清楚,XML里引用时一目了然。 - 偏移量和页大小作为独立参数传递,SQL的意图更明确。
- 返回类型一个是对应总数,一个是对应数据集,职责分明。
4.5 MyBatis XML实现
SQL部分,我们先看总数查询:
再看分页数据查询:
SQL编写上有几个关键点:
- 表别名用
tsm、tm,简短又清晰。 - 动态条件靠
标签实现,而且COUNT和DATA查询的条件必须完全一致。 - 模糊查询用
CONCAT拼接,避免SQL注入风险。 - 软删除条件
delete_flag = '0'是硬性要求,不能漏。 - 排序用
ORDER BY id DESC,保证分页结果稳定。 - 最后用
LIMIT #{offset}, #{size}实现物理分页,简单直接。