外观
查询与分页
Base Business 使用 Query 承载分页参数,使用 Wrapper 或应用 SQL 执行筛选。新增一个 Query 字段不会自动生成 WHERE 条件;用户、租户或工作空间范围需要明确写入查询逻辑。
Query 生命周期
| 字段/方法 | 当前行为 |
|---|---|
page | 首次 init() 时,空或不大于 0 取 1 |
rows | 首次 init() 时,空或不大于 0 取 10,大于 100 限制为 100 |
keywords | 仅承载字符串,需 Service 声明搜索字段才参与默认 Wrapper |
idList | 首次 init() 重建为空列表;复杂分页在查出 ID 后回填 |
page() | 先 init(),再执行 PageHelper.startPage(page, rows) |
getFrom() | 先 init(),返回 page * rows - rows |
init、idList 标记为 JSON 忽略。初始化是一次性的:之后修改 page、rows 不会再执行默认值修正。复制构造器和 extendQuery 只复制基础查询字段,不复制子类条件,idList 也不是深拷贝。
每次请求创建新的 Query。BaseEntityQuery.DEFAULT 是共享可变对象,默认 defaultQuery() 还将它强转为泛型 Query,使用子类 Query 时既有共享状态问题,也可能发生类型转换错误。具体服务应像快速开始一样重写 defaultQuery() 返回新的实际类型。
组合筛选与排序
应用可以重写 queryWrapper(QUERY) 加业务条件,并重写 keywords(QUERY) 返回用于模糊搜索的 SFunction 列表。默认关键字逻辑将这些字段组合成一个括号中的 OR LIKE 条件,再与其他条件 AND 连接;默认字段列表为空,因此仅传 keywords 不会筛选。
lambdaQueryWrapper(query, false) 跳过关键字拼接,但仍调用 queryWrapper(query)。默认 queryWrapper(query) 只是调用无参数 queryWrapper(),不会反射读取 Query 的业务字段。
排序有两个不同入口:无参数 lambdaQueryWrapper() 调用 defaultSort(wrapper);非空 Query 分支不会自动调用 defaultSort。因此分页需要在 Query 条件构造中显式加排序,通常将唯一 ID 作为最终排序字段。快速开始在 queryWrapper(query) 中按创建时间、ID 倒序排列。
调用方直接传入 Wrapper 的列表、计数和删除方法使用该 Wrapper;按 ID 查询使用 mapper.selectById,不会经过 queryWrapper(query)。不能仅重写一个 Query Hook 就认定详情、批量 ID 查询及更新删除都已具备相同的数据权限。
简单分页
service.page(query) 的顺序为:
query.page()开启 PageHelper 分页。listAllVo(query)调用单表查询并把 PO 转为 VO。- 调用
afterList补充当前页数据。 - 将列表和 PageHelper 的总数、页码封装为 PageModel。
需要 PageHelper 插件实际装配,单独注册 Mapper 不足以提供分页。分页开启后应紧接目标查询;在 Wrapper 构造或其他前置处理里另查数据库,可能让分页落到错误的查询上。基类没有包裹异常清理逻辑,应用自己编排 query.page() 时应在 finally 中清理 PageHelper 上下文,避免未执行目标查询时遗留状态。
page(query) 不接受 null Query。listAllVo(query) 本身不分页,listPoByQuery(null) 则使用默认无参数 Wrapper 查询;不要用列表接口返回全表,再误认为 rows 已限制结果。
复杂分页
联表场景可使用 pageForComplex(query),但必须先实现 BasePageMapper 的 SQL:
| 方法 | 需要提供的结果 |
|---|---|
listIds(query) | 根据完整筛选条件、权限范围和稳定排序,查询当前页主键 |
listByIds(query) | 依据回填后的 query.idList 读取 VO,保持与 ID 页一致的顺序和结果粒度 |
list(query)、map(query) | 额外的自定义列表契约;默认简单分页和 MapVo 列表不会自动调用它们 |
流程是开启分页 → listIds → 将 ID 写入 Query → listByIds → afterList → 包装结果。ID 为空时直接返回空列表与已有分页元数据,不执行第二条查询。两次查询之间没有默认事务或快照一致性保证。
应用 XML 或注解 SQL 必须自行完成字段映射、逻辑删除条件和权限过滤;这些方法不会经过 Service 的 Wrapper Hook。联表产生重复主键时,应在 ID 查询阶段按目标实体去重,否则总数及页内实体数量可能不一致。第二条 SQL 的 IN 条件不会自动保留 ID 列表顺序,需要显式排序。
列表与详情的后处理
| 方法 | 后处理边界 |
|---|---|
listAllVo(query) / 无参数版本 | PO 转 VO 后调用 afterList |
listAllVo(wrapper) | 仅转换,不调用 afterList |
listAllMapVo(...) | 调用 toMapVo,不调用完整 VO 的 afterList |
listAllEntity(...) | 使用指定 converter;不调用 afterList,converter 为 null 时返回空列表 |
queryPo* / queryVo* | 未查到时可返回 null,部分重载支持默认 Supplier |
getPo* / getVo* | 未查到且无可用默认对象时抛出 business_not_found,可传自定义响应码 |
afterList 收到的列表可能由 Stream.toList() 产生,不应假定可增删;需要替换结构时构造新列表返回。详情 Hook 的重复调用边界见使用与配置。
countEqual / listEqual 和 countLike / listLike 使用单字段条件;mapPoByIdList 按主键收集为 Map,不保证顺序,重复主键会导致收集异常。selectOne 查询若匹配多条也不能当作任意取第一条使用。
PageModel 的含义
标准业务字段为 page、rows、total、list、isLastPage。total 是记录总数,Java 类型为 Integer;来自 PageHelper 或 MyBatis-Plus 的 long 总数会转为 int,不能用它准确表示超过 Integer 范围的统计结果。
| 构造方式 | 行为 |
|---|---|
| 列表 + Page / PageInfo,或 IPage | 使用数据库分页元数据;列表应已是当前页 |
| 列表 + page/rows/total,或列表 + Query + total | 接收已分页列表,不再次切片 |
| 列表 + Query | 把传入列表当作完整数据,在内存中 subList 切页;先调用 query.init() |
| 仅 Query | 创建 total=0 的空页;先初始化 Query |
| 仅列表 | 只设置 list,分页元数据仍为空 |
无参、createEmptyModel()、静态 EMPTY | 字段默认为空,不是完整的标准空页;EMPTY 还是共享对象 |
通常 isLastPage 由 page * rows >= total 计算;PageInfo 构造器使用其结果。内存分页不会减少数据库取数,且 subList 与原列表共享底层数据,不适合替代大表 SQL 分页。
转换和后处理
convert(function)转换当前列表并保留分页元数据;replace(list)替换列表但不重算总数。filter和peek只登记操作,调用collect()才执行;先过滤,再执行消费者。它们只处理当前列表,不重算 total、rows 或 isLastPage,不能替代数据库条件。collect()不清空已登记操作,再次调用会再次执行。put、concat会修改当前列表,要求字段已初始化且列表可变。put累加 rows,使用另一个模型的 total 和末页标记;concat累加 total 并把末页标记设为 false,都不是自动重新分页。offset(offset, rows)只按参数计算 page 和末页标记,不设置模型 rows、不切片,offset=0 会得到 page=0,不能直接套用常规从 1 开始的页码语义。
向客户端返回时再使用 R.success(pageModel) 包装,外层业务成功标识遵循 Base 约定。