MyBatisPlus高阶技巧与性能优化实战

MyBatisPlus高阶技巧与性能优化实战

1. MyBatisPlus核心价值解析

MyBatisPlus作为MyBatis的增强工具包,在Java持久层开发中已经成为事实上的标配。我接触过上百个采用MyBatisPlus的企业项目,发现真正能发挥其全部威力的团队不足20%。大多数开发者仅仅停留在基础CRUD操作,这就像开着跑车却只敢挂一档行驶。

最新3.5.x版本在性能优化和功能扩展上都有显著提升。单就批量插入这一项,经过我的实测对比,比原生MyBatis效率提升可达3-5倍。特别是在处理千万级数据迁移时,合理配置批量处理器可以避免OOM问题。

重要提示:升级到3.5.17+版本时需要注意,新版对Lambda表达式处理做了优化,部分老项目可能需要调整字段引用方式

2. 效率翻倍的12个高阶技巧

2.1 动态表名处理器实战

在多租户系统中,我经常需要根据租户ID动态切换表名。通过实现TableNameHandler接口,可以优雅地解决这个问题:

public class TenantTableNameHandler implements TableNameHandler { @Override public String dynamicTableName(String sql, String tableName) { String tenantId = TenantContext.getCurrentTenant(); return tenantId + "_" + tableName; } }

配置示例:

mybatis-plus: global-config: db-config: table-name-handler: com.example.TenantTableNameHandler

踩坑记录:在分页查询时,COUNT语句的表名也需要同步替换,否则会导致统计错误。解决方法是在handler中判断sql包含"COUNT"时做特殊处理。

2.2 智能字段填充进阶玩法

自动填充字段远不止create_time/update_time这么简单。在电商项目中,我这样配置商品上下架记录:

public class ProductMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "onSale", Boolean.class, false); this.strictInsertFill(metaObject, "approver", String.class, getCurrentUser()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "reviewer", String.class, getCurrentUser()); } }

性能优化点:避免在填充器中执行耗时操作(如远程调用),否则会影响批量操作性能。建议提前缓存必要数据。

2.3 多租户拦截器深度定制

不同业务表可能需要不同的租户字段,通过自定义拦截器实现精细控制:

public class CustomTenantInterceptor extends TenantLineInnerInterceptor { private static final Map<String, String> TABLE_FIELD_MAPPING = ImmutableMap.of( "order", "company_id", "user", "org_id" ); @Override protected String getTenantIdColumn() { String tableName = getTableName(); return TABLE_FIELD_MAPPING.getOrDefault(tableName, "tenant_id"); } }

避坑指南:联表查询时需特别注意,如果关联表有不同的租户字段,需要重写determineTenantIds方法实现多字段过滤。

2.4 分页查询性能优化

突破默认的500条限制其实有更优雅的方式:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 自定义分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL) { @Override protected void handlerLimit() { // 不限制单页大小 } }); return interceptor; }

实测数据:在百万级数据表中,合理设置pageSize(建议1000-5000)比多次小分页查询快3-8倍。但要注意网络传输和内存消耗的平衡。

2.5 逻辑删除的隐藏技巧

除了基础的@TableLogic配置,还可以这样玩:

mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段 logic-not-delete-value: 0 logic-delete-value: 1 logic-delete-update-full: true # 关键配置!

当logic-delete-update-full为true时,执行update会包含逻辑删除字段条件,避免误更新已删除数据。这在多人协作系统中尤为重要。

2.6 自定义TypeHandler大全

处理JSON字段时,通用解决方案:

@MappedTypes(JSONObject.class) public class JsonTypeHandler extends BaseTypeHandler<JSONObject> { @Override public void setNonNullParameter(PreparedStatement ps, int i, JSONObject parameter, JdbcType jdbcType) { ps.setString(i, parameter.toJSONString()); } // 其他方法实现... } // 实体类使用 @TableField(typeHandler = JsonTypeHandler.class) private JSONObject attributes;

类型扩展:同样的模式可以轻松适配Gson、Jackson等其他JSON库,只需要修改具体的序列化逻辑。

3. 企业级实战方案

3.1 多数据源与分布式事务

结合dynamic-datasource实现多数据源切换:

@DS("slave") public List<User> getReadOnlyUsers() { return userMapper.selectList(Wrappers.emptyWrapper()); } @DS("master") @Transactional public void updateUser(User user) { userMapper.updateById(user); logMapper.insert(new Log("update", user.getId())); }

事务陷阱:跨数据源操作需要引入Seata等分布式事务框架,否则@Transactional只能控制当前数据源。

3.2 审计日志自动化

通过MetaObjectHandler自动记录操作日志:

public class AuditMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUser()); this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUser()); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

增强方案:结合Mybatis的ExecutorListener可以实现更细粒度的操作日志记录,包括参数值和影响行数。

4. 性能调优指南

4.1 批量操作优化

正确的批量插入姿势:

// 错误示范:在循环中逐条insert users.forEach(userMapper::insert); // 正确做法:使用批量处理器 SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); UserMapper mapper = session.getMapper(UserMapper.class); users.forEach(mapper::insert); session.commit();

性能对比:万级数据插入测试显示,批量模式比单条插入快40倍以上,且JDBC批处理大小建议设置为500-1000。

4.2 查询结果流式处理

处理海量数据时避免OOM:

try (Cursor<User> cursor = userMapper.selectCursor(queryWrapper)) { cursor.forEach(user -> { // 处理每条数据 processUser(user); }); }

内存监控:使用JDK的VisualVM观察,流式处理百万数据时堆内存保持稳定,而普通查询会导致内存飙升。

5. 插件开发秘籍

5.1 自定义拦截器示例

开发SQL执行时间监控插件:

@Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}) }) public class PerformanceInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); Object result = invocation.proceed(); long end = System.currentTimeMillis(); if (end - start > 100) { // 超过100ms记录警告 log.warn("Slow SQL detected: {}ms", end - start); } return result; } }

扩展思路:可以结合MDC实现请求级别的SQL监控,或者将统计信息推送到Prometheus实现可视化。

5.2 多租户插件增强版

支持按租户隔离的查询缓存:

public class TenantCacheInterceptor implements InnerInterceptor { @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { String tenantId = TenantContext.getCurrentTenant(); String cacheKey = buildCacheKey(ms, boundSql, tenantId); // ...缓存查询逻辑 } }

缓存策略:建议采用二级缓存方案,本地缓存使用Caffeine,分布式缓存使用Redis,并设置合理的过期时间。