24-MyBatisHelper:查询分页与autoCount
data模块自己的MyBatis门面------queryList/queryOne/queryPage三组查询、insert/update/delete三组写、saveBatch一个批量事务方法。核心亮点是autoCount:从MappedStatement抓BoundSql包一层
SELECT COUNT(1) FROM (...) _t。这篇拆245行,重点讲autoCount怎么从MyBatis内部拿SQL和参数。
文章目录
源码:
browise-data/src/main/java/com/browise/data/mybatis/MyBatisHelper.java(245行)
一、类定位:MyBatis的轻门面
java
public class MyBatisHelper {
private final SqlSessionFactory sqlSessionFactory;
private volatile EntityLifecycleListener lifecycleListener; // 桥接监听器
}
为什么不直接用BaseMapper ------MyBatis-Plus的BaseMapper走Spring注入的SqlSessionTemplate,事务由Spring管理。MyBatisHelper的场景是脱离Spring事务的服务端批处理/导出/EA引擎内部------自己openSession自己关(try-with-resources)。两者并存:Controller里的CRUD走MP,批处理走Helper。
lifecycleListener是第01篇讲的"data不依赖crypto"桥接点------beforeSaveBatch/afterQueryBatch钩子在这里触发,starter把CryptoHelper包成CryptoEntityLifecycleListener注入进来(第33篇)。
二、三组查询:语句ID的拼法
java
public <T> List<T> queryList(Class<?> mapperClass, String method, Object param) {
try (SqlSession session = sqlSessionFactory.openSession()) {
List<T> result = session.selectList(
mapperClass.getName() + "." + method, // 语句ID = Mapper全限定名.方法名
param);
if (lifecycleListener != null) lifecycleListener.afterQueryBatch(result);
return result;
}
}
语句ID的拼接约定 ------com.browise.platform.entity.SysUserMapper.selectByUnit。MyBatis的statement就是这个名字(接口方法绑定XML的id)。BaseEntity.save()传的insertMethod/updateMethod也拼进这个约定(第20篇)------方法名字符串就是持久化的路由键。
查询后afterQueryBatch触发批量解密------一批实体里的加密字段一次性解掉(比逐个afterQuery少N-1次策略调用判断)。
三、queryPage的两个重载
java
// 重载一:不知道总数------total=-1,前端显示"共?条"
public <T> PageResult<T> queryPage(mapperClass, method, param, pageNumber, pageSize) {
int offset = (int) ((pageNumber - 1) * pageSize);
List<T> records = sessionSelectList(..., new RowBounds(offset, (int) pageSize));
return new PageResult<>(records, -1, pageNumber, pageSize);
}
// 重载二:给总数或自动算
public <T> PageResult<T> queryPage(..., long recordCount) {
long total = recordCount;
if (total <= 0) total = autoCount(mapperClass, method, param); // ≤0触发自动
...
}
三个total策略 :调用方知道总数(外层已查过)→直接传;不知道→传0触发autoCount;故意不要总数(导出场景,省一次COUNT)→用重载一,total=-1。
pageSize=0的分支------不分页查全量(PageResult(all, all.size(), 1, all.size()))。pageSize=0当"查所有"的信号------调用方不用换方法。
四、RowBounds:内存分页的坑
java
sessionSelectList(mapperClass, method, param, new RowBounds(offset, pageSize));
MyBatis的RowBounds默认是内存分页------查出全部结果再skip/limit!百万行的查询用RowBounds=OOM。
那browise怎么活?------PaginationInterceptor (data模块的MyBatis插件)拦截Executor的query,检测到RowBounds时改写成物理分页(Oracle ROWNUM/MySQL LIMIT)。这就是BROWISE-GUIDE里"改MyBatis源码1行搞定物理分页"那篇已发布文章对应的实现------Helper只管传RowBounds,插件负责翻译成真分页。
RowBounds在这个体系里是分页意图的标记,不是分页的执行者。执行语义被插件重定义------这是MyBatis插件机制的标准玩法(PageHelper同款思路)。
五、autoCount:从MyBatis肚子里掏SQL
核心方法,24行:
java
public long autoCount(Class<?> mapperClass, String method, Object param) {
String msId = mapperClass.getName() + "." + method;
try (SqlSession session = sqlSessionFactory.openSession()) {
// ①拿MappedStatement(MyBatis解析好的语句定义)
MappedStatement ms = session.getConfiguration().getMappedStatement(msId);
// ②拿BoundSql------带#{}已解析成?的最终SQL+参数映射
BoundSql boundSql = ms.getBoundSql(param);
// ③无脑包一层COUNT
String countSql = "SELECT COUNT(1) FROM (" + boundSql.getSql() + ") _t";
try (Connection conn = session.getConnection();
PreparedStatement ps = conn.prepareStatement(countSql)) {
// ④手动绑定参数------按BoundSql的ParameterMapping逐个取值
List<ParameterMapping> mappings = boundSql.getParameterMappings();
for (int i = 0; i < mappings.size(); i++) {
Object value = boundSql.getParameterObject() != null
? extractParamValue(boundSql.getParameterObject(),
mappings.get(i).getProperty())
: null;
ps.setObject(i + 1, value);
}
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getLong(1) : 0L;
}
}
} catch (Exception e) {
log.warn("autoCount failed", e);
return 0L; // 失败返回0------降级不抛
}
}
getBoundSql(param)发生了什么
传入param后MyBatis执行动态SQL解析------<if> <where> 这些动态标签按这个param求值 ,生成最终SQL(#{}已成?)和ParameterMapping列表。这就是为什么autoCount要传param------不传的话动态条件不知道走哪个分支,SQL可能和真实查询不一致。
_t 别名的来历
FROM (...) _t------子查询必须有别名,_t是"temp/table"的缩写。与行协议的_t撞名纯属巧合 (EA引擎的countWrap用a做别名------两处各自命名)。子查询别名在Oracle/MySQL都合法且无语义。
extractParamValue的两种取值
java
private Object extractParamValue(Object paramObj, String property) {
if (paramObj instanceof Map) return ((Map) paramObj).get(property); // Map参数
Field f = paramObj.getClass().getDeclaredField(property); // 实体参数
return f.get(paramObj);
}
参数是Map(多参数@Param场景MyBatis包成ParamMap)→按key取;参数是实体→反射按字段取。拿到的值再 setObject 绑回? ------等于手工重演了MyBatis的参数绑定。为什么不用session自己selectOne?因为statement的resultType不是Long(是List<实体>)------复用原statement拿不到count结果,只能裸JDBC执行包装SQL。
六、saveBatch:一个事务的批量清算
java
public <T extends BaseEntity> int[] saveBatch(
Class<?> mapperClass, TypedStoreSaveConfig config, List<T> entities) {
int[] results = new int[entities.size()];
try (SqlSession session = sqlSessionFactory.openSession(false)) { // 不自动提交
try {
if (lifecycleListener != null) lifecycleListener.beforeSaveBatch(entities);
for (int i = 0; i < entities.size(); i++) {
T entity = entities.get(i);
String stmt;
if (entity.isInsert()) stmt = ...config.getInsertMethod();
else if (entity.isUpdate()) stmt = ...config.getUpdateMethod();
else stmt = ...config.getDeleteMethod();
results[i] = session.insert/update/delete(stmt, entity);
}
session.commit(); // 全部成功才提交
return results;
} catch (Exception e) {
session.rollback(); // 任一失败整体回滚
throw e;
}
}
}
与commonSave的关键差异------这里有真事务 (openSession(false)+commit/rollback)。TypedRowSet.collect()的混合清单进来,要么全成要么全滚。这是第23篇"逐行小事务"之外的批处理通道:单表批量、原子性优先。
beforeSaveBatch在事务内触发------批量加密发生在同一个事务里,加密失败回滚,不会留半批明文。
✅ 亮点:245行MyBatis门面拆成三组查询/三组写/saveBatch,重点讲autoCount从getBoundSql掏SQL+手工重演参数绑定的实现、RowBounds被插件重定义为分页意图标记、saveBatch与commonSave的事务语义对照。适合写MyBatis辅助层的人。扩展方向:第10篇EA引擎countWrap对照、第21篇TypedRowSet分类收集、第33篇CryptoAspect桥接。