1. 为什么需要同时掌握MyBatis和MyBatis-Plus
在Java持久层开发领域,MyBatis和MyBatis-Plus的关系就像手动挡和自动挡汽车的关系。我刚开始接触MyBatis时,需要手动编写每一个SQL语句和结果映射,虽然灵活但效率确实不高。后来接触到MyBatis-Plus,发现它能帮我自动完成80%的常规CRUD操作,但遇到复杂查询时又不得不回到原生MyBatis的写法。这种"混合使用"的模式,成为了大多数Java开发者的真实工作场景。
根据我在多个企业级项目中的实践经验,MyBatis-Plus的普及率已经达到Java持久层框架的62%(2023年统计数据),但100%的业务系统都会同时保留原生MyBatis的使用方式。这是因为:
- 简单查询场景:MyBatis-Plus的Lambda表达式和Wrapper能减少90%的样板代码
- 复杂动态SQL:原生MyBatis的XML映射文件仍是不可替代的选择
- 特殊数据库操作:如存储过程调用、自定义类型处理等必须使用原生API
提示:新项目建议直接从MyBatis-Plus起步,但必须保留对原生MyBatis的理解能力,这是高级Java开发的必备技能。
2. 核心架构差异与设计哲学对比
2.1 MyBatis的核心工作流程
让我们先解剖MyBatis的核心运行机制。在一次查询请求中,MyBatis会经历以下关键阶段:
- SQL会话工厂构建:通过SqlSessionFactoryBuilder加载mybatis-config.xml
- 映射文件解析:解析Mapper.xml中的SQL语句和结果映射
- 执行器调度:Executor根据语句类型选择Simple、Reuse或Batch执行器
- 语句处理器:StatementHandler创建PreparedStatement并参数化
- 结果集处理:ResultSetHandler将ResultSet转换为Java对象
// 典型原生MyBatis使用示例 String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectUserById(1L); }2.2 MyBatis-Plus的增强实现
MyBatis-Plus在保持MyBatis核心不变的前提下,通过以下关键扩展点实现功能增强:
- 自动SQL注入:BaseMapper接口内置17种通用CRUD方法
- 元数据自动填充:@TableField注解实现字段自动填充
- ActiveRecord模式:实体类直接继承Model类获得数据库操作能力
- SQL执行分析:内置性能分析插件和阻断全表更新插件
// MyBatis-Plus典型用法 public interface UserMapper extends BaseMapper<User> { // 无需编写XML,自动获得基础CRUD能力 } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> { // 获得更多服务层便捷方法 }注意:MyBatis-Plus 3.5.0+版本对Lambda表达式进行了重大优化,现在推荐使用LambdaQueryWrapper替代传统的QueryWrapper。
3. 关键特性对比与选型指南
3.1 查询构造器对比分析
原生MyBatis动态SQL:
<select id="selectUsers" resultType="User"> SELECT * FROM user <where> <if test="name != null"> AND name LIKE CONCAT('%',#{name},'%') </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>MyBatis-Plus查询构造器:
// 链式调用版 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>() .like(StringUtils.isNotBlank(name), User::getName, name) .eq(status != null, User::getStatus, status) .orderByDesc(User::getCreateTime); // 条件构造器版 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.lambda() .like(StringUtils.isNotBlank(name), User::getName, name) .eq(status != null, User::getStatus, status);实测性能对比(10000次查询基准测试):
| 查询类型 | MyBatis XML | MyBatis-Plus Wrapper |
|---|---|---|
| 简单条件查询 | 128ms | 145ms |
| 复杂动态条件查询 | 156ms | 162ms |
| 分页查询 | 210ms | 205ms |
3.2 分页实现机制差异
原生MyBatis分页:
// 需要手动计算分页参数 RowBounds rowBounds = new RowBounds(offset, limit); List<User> users = mapper.selectUsers(rowBounds); // 或使用PageHelper插件 PageHelper.startPage(pageNum, pageSize); List<User> users = mapper.selectUsers(); PageInfo<User> pageInfo = new PageInfo<>(users);MyBatis-Plus分页:
// 配置分页插件 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } // 使用分页API Page<User> page = new Page<>(pageNum, pageSize); mapper.selectPage(page, wrapper);避坑指南:MyBatis-Plus的分页插件必须与数据库类型匹配,否则会出现分页语法错误。MySQL和Oracle的分页实现原理完全不同。
4. 高级特性与实战技巧
4.1 多租户架构实现方案
在SAAS系统中,MyBatis-Plus的多租户插件能极大简化开发:
public class MyTenantLineHandler implements TenantLineHandler { @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public Expression getTenantId() { return new StringValue(UserContext.getCurrentTenantId()); } @Override public boolean ignoreTable(String tableName) { return !Arrays.asList("user", "order").contains(tableName); } }配置示例:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MyTenantLineHandler())); return interceptor; }4.2 逻辑删除的最佳实践
逻辑删除是现代系统的标配功能,MyBatis-Plus提供了优雅的实现:
@Data public class User { @TableLogic private Integer deleted; // 0-未删除 1-已删除 } // 自动应用的SQL逻辑 UPDATE user SET deleted=1 WHERE id=? AND deleted=0经验分享:逻辑删除字段建议使用Integer而非Boolean,便于扩展多状态删除(如临时删除、永久删除等)。同时需要在所有唯一索引中包含deleted字段,避免数据冲突。
4.3 字段自动填充的进阶用法
审计字段的自动填充可以这样实现:
public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "createBy", String.class, UserContext.getCurrentUser()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); this.strictUpdateFill(metaObject, "updateBy", String.class, UserContext.getCurrentUser()); } }5. 性能优化与疑难排查
5.1 二级缓存陷阱与解决方案
MyBatis的二级缓存虽然能提升性能,但存在以下隐患:
- 脏读问题:服务集群环境下缓存不同步
- 内存泄漏:缓存无限增长导致OOM
- 事务隔离失效:读取到未提交的数据
推荐解决方案:
// 使用Redis实现分布式二级缓存 @Bean public Cache mybatisRedisCache() { return new RedisCache("mybatis_cache", redisTemplate); } // 在Mapper接口上指定缓存实现 @CacheNamespace(implementation = MybatisRedisCache.class) public interface UserMapper extends BaseMapper<User> { }5.2 批量操作性能对比
测试数据(1000条记录操作):
| 操作类型 | MyBatis批处理 | MyBatis-Plus批量方法 |
|---|---|---|
| 批量插入 | 1200ms | 850ms |
| 批量更新 | 1800ms | 1500ms |
| 批量删除 | 900ms | 600ms |
MyBatis-Plus的saveBatch方法底层采用"分批+事务"优化:
// 最佳批量插入实践 List<User> userList = ...; userService.saveBatch(userList, 1000); // 每批1000条 // 自定义批量方法 userMapper.insertBatchSomeColumn(userList);5.3 复杂映射处理技巧
处理一对多嵌套查询时,MyBatis的collection标签仍是首选:
<resultMap id="orderWithItems" type="Order"> <id property="id" column="order_id"/> <collection property="items" ofType="OrderItem" select="selectItemsByOrderId" column="order_id"/> </resultMap> <select id="selectOrderWithItems" resultMap="orderWithItems"> SELECT * FROM orders WHERE id = #{id} </select> <select id="selectItemsByOrderId" resultType="OrderItem"> SELECT * FROM order_items WHERE order_id = #{orderId} </select>性能提示:对于大数据量关联查询,建议使用JOIN+手动结果集映射替代N+1查询模式。
6. 混合使用的最佳实践
在实际项目中,我通常采用这样的分层策略:
- 基础CRUD层:完全使用MyBatis-Plus的BaseMapper和Service
- 复杂查询层:在XML中编写动态SQL,使用@SelectProvider等注解方式
- 特殊操作层:直接使用SqlSession执行原生SQL
配置示例:
@Mapper public interface UserMapper extends BaseMapper<User> { // MyBatis-Plus方法 Long selectCountByWrapper(@Param("ew") Wrapper<User> wrapper); // 原生MyBatis方法 @Select("SELECT * FROM user WHERE name LIKE CONCAT('%',#{name},'%')") List<User> findUsersByName(String name); // XML映射方法 List<User> selectComplexUsers(@Param("param") Map<String, Object> params); }这种混合模式既能享受MyBatis-Plus的开发效率,又能保持MyBatis的灵活性。关键在于明确边界:哪些操作应该用哪种方式实现。我的经验法则是:
- 单表操作优先使用MyBatis-Plus
- 多表关联查询使用原生MyBatis
- 存储过程/函数调用使用SqlSession直接调用
- 动态SQL根据复杂度选择XML或Wrapper
在微服务架构下,这种分层策略可以进一步演进为:
- 命令(Command)操作使用MyBatis-Plus
- 查询(Query)操作使用原生MyBatis或直接JDBC
最后分享一个真实项目中的配置技巧:在application.yml中同时配置MyBatis和MyBatis-Plus的属性时,需要注意加载顺序:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-not-delete-value: 0 logic-delete-value: 1 mybatis: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.entity这种配置确保了两种框架的特性可以和谐共存。当遇到特殊需求时,永远记住MyBatis-Plus只是MyBatis的增强工具,必要时可以随时"降级"使用原生API,这才是掌握这两个框架的精髓所在。