1. 概述
本文档详细介绍基于MyBatis-Plus框架实现的跨表查询分页功能。以供应商物料查询为例,展示如何通过多表关联查询并返回标准分页对象的技术实现方案。
2. 技术栈
ORM框架: MyBatis-Plus
数据库: MySQL(使用LIMIT进行分页)
分页组件: MyBatis-Plus Page对象
架构模式: Repository + Mapper + XML三层架构
3. 核心实现原理
3.1 分页策略
采用手动分页方式,分为两步执行:
查询总数: 执行COUNT查询获取符合条件的总记录数
查询数据: 根据偏移量(offset)和页大小(size)查询具体数据
3.2 为什么选择手动分页?
跨表查询涉及LEFT JOIN,MyBatis-Plus自动分页可能存在性能问题
可以精确控制SQL语句,优化查询性能
支持复杂的动态条件拼接
4. 代码实现详解
4.1 参数对象设计
SupplierIdParam.java - 查询参数封装
java
@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; // 定价类型
// ... 其他字段
}
关键点:
继承PageQueryParam,包含currentPage和pageSize分页参数
支持多维度动态查询条件
使用包装类型,便于判断参数是否为空
4.2 领域对象设计
SupplierMaterialDO.java - 供应商物料实体
java
@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.java
java
@Repository
public class SupplierMaterialRepository extends HussarServiceImpl<SupplierMaterialMapper, SupplierMaterialDO> {
@Resource
private SupplierMaterialMapper supplierMaterialMapper;
public Page<SupplierMaterialDO> queryMaterialsBySupplierId(SupplierIdParam param) {
Long tenantId = param.getTenantId();
// 第一步:查询总数
Long total = supplierMaterialMapper.queryMaterialsBySupplierIdCount(param, tenantId);
// 第二步:创建分页对象
Page<SupplierMaterialDO> page = new Page<>(param.getCurrentPage(), param.getPageSize(), total);
// 第三步:查询分页数据
List<SupplierMaterialDO> supplierMaterialDOList =
supplierMaterialMapper.queryMaterialsBySupplierId(param, tenantId, page.offset(), page.getSize());
// 第四步:设置结果集
if (CollectionUtils.isNotEmpty(supplierMaterialDOList)) {
page.setRecords(supplierMaterialDOList);
}
return page;
}
}
实现要点:
分离计数查询: 单独执行COUNT查询,避免查询不必要的数据列
计算偏移量: 使用page.offset()方法自动计算OFFSET值
空值保护: 检查查询结果是否为空,避免设置null集合
租户隔离: 所有查询都携带tenantId,实现多租户数据隔离
4.4 Mapper接口定义
SupplierMaterialMapper.java
java
@Mapper
public interface SupplierMaterialMapper extends BaseMapper<SupplierMaterialDO>, HussarMapper<SupplierMaterialDO> {
/**
* 查询总数
*/
Long queryMaterialsBySupplierIdCount(@Param("param") SupplierIdParam param,
@Param("tenantId") Long tenantId);
/**
* 查询分页数据
*/
List<SupplierMaterialDO> queryMaterialsBySupplierId(@Param("param") SupplierIdParam param,
@Param("tenantId") Long tenantId,
@Param("offset") Long offset,
@Param("size") Long size);
}
设计说明:
使用@Param注解明确参数名称,便于XML中引用
将offset和size作为独立参数传递,提高SQL可读性
返回类型分别为Long和List<SupplierMaterialDO>
4.5 MyBatis XML实现
SupplierMaterialMapper.xml
4.5.1 总数查询SQL
XML
<select id="queryMaterialsBySupplierIdCount" resultType="java.lang.Long">
SELECT count(*) AS total
FROM t_supplier_material tsm
LEFT JOIN t_material tm ON tsm.material_id = tm.id
WHERE tsm.delete_flag = '0'
AND tsm.tenant_id = #{tenantId}
<if test="param.supplierId != null and param.supplierId != ''">
AND supplier_id = #{param.supplierId}
</if>
<if test="param.materialCategoryExternalCode != null and param.materialCategoryExternalCode != ''">
AND tsm.material_category_external_code LIKE CONCAT('%', #{param.materialCategoryExternalCode}, '%')
</if>
<if test="param.materialCategoryName != null and param.materialCategoryName != ''">
AND tsm.material_category_name LIKE CONCAT('%', #{param.materialCategoryName}, '%')
</if>
<if test="param.materialName != null and param.materialName != ''">
AND tsm.name LIKE CONCAT('%', #{param.materialName}, '%')
</if>
<if test="param.materialExternalCode != null and param.materialExternalCode != ''">
AND tsm.external_code LIKE CONCAT('%', #{param.materialExternalCode}, '%')
</if>
<if test="param.materialStatus != null and param.materialStatus != ''">
AND tm.status = #{param.materialStatus}
</if>
<if test="param.pricingType != null and param.pricingType != ''">
AND tm.pricing_type = #{param.pricingType}
</if>
</select>
4.5.2 分页数据查询SQL
XML
<select id="queryMaterialsBySupplierId"
resultType="com.mdgyl.hussar.basic.supplier.masterdata.domain.SupplierMaterialDO">
SELECT tsm.*
FROM t_supplier_material tsm
LEFT JOIN t_material tm ON tsm.material_id = tm.id
WHERE tsm.delete_flag = '0'
AND tsm.tenant_id = #{tenantId}
<!-- 动态条件与COUNT查询保持一致 -->
<if test="param.supplierId != null and param.supplierId != ''">
AND supplier_id = #{param.supplierId}
</if>
<if test="param.materialCategoryExternalCode != null and param.materialCategoryExternalCode != ''">
AND tsm.material_category_external_code LIKE CONCAT('%', #{param.materialCategoryExternalCode}, '%')
</if>
<!-- ... 其他动态条件 ... -->
ORDER BY tsm.id DESC
LIMIT #{offset}, #{size}
</select>
SQL编写要点:
表别名规范: 使用简短且有意义的别名(如tsm、tm)
动态条件: 使用<if>标签实现条件可选,确保COUNT和DATA查询条件一致
模糊查询: 使用CONCAT('%', value, '%')实现LIKE模糊匹配
软删除过滤: 始终添加delete_flag = '0'条件
排序规则: 使用ORDER BY id DESC保证数据稳定性
LIMIT分页: 使用LIMIT #{offset}, #{size}实现物理分页