24-MyBatisHelper与autoCount

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桥接。

相关推荐
CRMEB系统商城42 分钟前
汽车养护连锁的数字化底座CRMEB 多门店系统实战方案
java·开发语言·小程序·开源·汽车
小马过河R1 小时前
微信小程序自定义登录态维护:从入门到生产级落地
后端·微信小程序·小程序·架构·登录态
学编程就要猛1 小时前
使用Spring AI配置聊天大模型实现人机交互(初阶)
java·spring ai·advisor·chatclient·chatmodel
努力努力再努力wz1 小时前
【Redis入门系列】:从 RESP 协议到 redis-plus-plus:Redis 客户端编程与 C++ 接口设计
开发语言·数据库·c++·redis·分布式·缓存·架构
代码不停1 小时前
子序列问题
java·算法
计算机学姐1 小时前
基于SpringBoot的高校爱心慈善管理系统
java·vue.js·spring boot·后端·spring·tomcat·mybatis
Dawson Zhu1 小时前
Kafka 在 AI Agent 系统中的应用:任务队列与事件总线的角色辨析及工程实践
人工智能·语言模型·架构·aigc·agi
大梦想家a1 小时前
基于Servlet+JSP的小区物业管理系统设计与实现(Bootstrap+MySQL)
java·servlet·bootstrap
不爱运动的跑者1 小时前
架构可视化不是“画图“:让 AI 产出可追溯架构图的四层工程
人工智能·架构