Featured image of post MyBatis 增强工具选型:MyBatis-Plus 与通用 Mapper 全面对比

MyBatis 增强工具选型:MyBatis-Plus 与通用 Mapper 全面对比

两套都号称『简化 MyBatis』的增强工具,定位、能力、设计哲学完全不同。一篇讲清两者差异和选型建议

写在前面

如果你写过原生 MyBatis,一定经历过这种崩溃时刻——

1
2
3
4
5
6
7
8
<select id="selectByCondition" resultType="User">
    SELECT id, name, age, status FROM user
    <where>
        <if test="name != null">AND name = #{name}</if>
        <if test="age != null">AND age = #{age}</if>
        <if test="status != null">AND status = #{status}</if>
    </where>
</select>

每张表都要写一遍 CRUD,字段一变就改十几个 XML,任何"按字段查"“按字段更新"都要新加一段 SQL。这种重复劳动催生了两个现象级开源项目

  • 通用 Mapper(tk.mybatis):刘增辉(abel533)2014 年开源
  • MyBatis-Plus(MP):苞米豆团队 2016 年开源

两者都自称"MyBatis 增强工具”,新人选型时常常分不清。本文把这两个项目从设计哲学、能力边界、典型用法、踩坑点一一对照,给出明确的选型建议。


一句话定位

通用 Mapper:在 MyBatis 上提供"通用 SQL 模板",让你不写 XML 就能完成基础 CRUD。

MyBatis-Plus:在 MyBatis 上提供"增强能力 + 工具集",目标是把 MyBatis 用得像 JPA 一样省事,同时保留 MyBatis 的灵活性。

通用 Mapper 是"做减法"——少写 XML;MyBatis-Plus 是"做加法"——给你一整套生态工具。


一、通用 Mapper 是怎么工作的

通用 Mapper 的核心思想:给你一个泛型 Mapper<T>,里面已经定义了一堆通用方法,框架在运行时根据泛型类型生成对应的 SQL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public interface UserMapper extends Mapper<User> {
}

@Service
public class UserService {
    @Autowired private UserMapper userMapper;

    public User get(Long id) { return userMapper.selectByPrimaryKey(id); }
    public int  add(User u) { return userMapper.insert(u); }
    public int  update(User u) { return userMapper.updateByPrimaryKey(u); }
    public int  remove(Long id) { return userMapper.deleteByPrimaryKey(id); }
}

实体类用 JPA 风格注解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Integer age;
    @Column(name = "status_code")
    private Integer status;
}

它的核心方法分几大类:

接口提供能力
BaseSelectMapperselectByPrimaryKey 等
BaseInsertMapperinsert
BaseUpdateMapperupdateByPrimaryKey
BaseDeleteMapperdeleteByPrimaryKey
ConditionMapper通过 Example 条件查询
RowBoundsMapper分页(基于 RowBounds)

复杂条件用 Example

1
2
3
4
5
Example example = new Example(User.class);
example.createCriteria()
        .andEqualTo("status", 1)
        .andLike("name", "张%");
List<User> users = userMapper.selectByExample(example);

风格非常贴近 JPA Specification,有点"老 SSH 时代"的味道。


二、MyBatis-Plus 是怎么工作的

MyBatis-Plus 的核心入口是 BaseMapper<T>IService<T> 两层抽象:

1
2
3
4
5
6
7
8
9
public interface UserMapper extends BaseMapper<User> {
}

public interface UserService extends IService<User> {
}

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

实体类用 MP 自家注解(也可继承 Model<T>):

1
2
3
4
5
6
7
8
9
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    @TableField("status_code")
    private Integer status;
}

最闪光的是它的 LambdaQueryWrapper——以方法引用代替字符串字段名,全程类型安全

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<User> users = userMapper.selectList(
    new LambdaQueryWrapper<User>()
        .eq(User::getStatus, 1)
        .like(User::getName, "张")
        .orderByDesc(User::getId)
);

userMapper.update(null,
    new LambdaUpdateWrapper<User>()
        .set(User::getStatus, 0)
        .eq(User::getId, 100L)
);

字段重命名时编译器会立刻报错,再也不用拼字符串。


三、能力对比

通用 MapperMyBatis-Plus
基础 CRUD
条件构造Example(字符串字段)LambdaQueryWrapper(类型安全)
分页RowBounds(物理分页要插件)PaginationInterceptor(开箱即用)
主键策略JPA @GeneratedValueIdType.AUTO/INPUT/ASSIGN_ID/ASSIGN_UUID
自动填充@TableField(fill = ...) + Handler
逻辑删除@TableLogic 自动 where deleted=0
乐观锁@Version 自动 where version=#{old}
多租户TenantLineInnerInterceptor
数据权限/字段加密插件机制
代码生成器第三方官方代码生成器 + AI Friendly
ActiveRecordModel<T>(争议特性,不推荐用)
性能分析插件慢 SQL 输出、SQL 注入检测

简单说:通用 Mapper 只解决"少写 CRUD SQL",MyBatis-Plus 是一整套生态


四、设计哲学差异

通用 Mapper:贴近 JPA、最小侵入

  • 用的是 javax.persistence.* 注解,和 JPA 互通——理论上可以无缝切到 Spring Data JPA
  • 不引入新概念,学习成本极低
  • 一切以"少写 SQL"为目标,不试图替你做更多事

MyBatis-Plus:全套生态、约定大于配置

  • 自家注解 @TableId@TableField@TableLogic 等,绑定较深
  • 提供了大量"开箱即用的便利"——逻辑删除、乐观锁、自动填充、分页都不用自己写代码
  • 从 ORM 一直管到代码生成、慢 SQL 监控

简单总结:

通用 Mapper 是"工具",MyBatis-Plus 是"框架"。


五、典型场景对比

场景 1:分页

通用 Mapper 自己只支持 RowBounds,物理分页要配合 PageHelper:

1
2
3
PageHelper.startPage(1, 10);
List<User> users = userMapper.selectAll();
PageInfo<User> page = new PageInfo<>(users);

MyBatis-Plus 自带分页插件:

1
2
3
4
IPage<User> page = userMapper.selectPage(
    new Page<>(1, 10),
    new LambdaQueryWrapper<User>().eq(User::getStatus, 1)
);

场景 2:逻辑删除

通用 Mapper 没有原生支持,要么写 SQL,要么自己注入 SqlInterceptor。

MyBatis-Plus 一个注解搞定:

1
2
@TableLogic
private Integer deleted;   // 0 未删,1 已删

之后所有 selectXxxdeleteXxx 自动带上 WHERE deleted = 0,省心。

场景 3:自动填充 createTime / updateTime

通用 Mapper 要靠拦截器或自己手写。

MyBatis-Plus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

@Component
public class MetaHandler implements MetaObjectHandler {
    @Override public void insertFill(MetaObject m) {
        this.strictInsertFill(m, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(m, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
    @Override public void updateFill(MetaObject m) {
        this.strictUpdateFill(m, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

这种"业务无感"的能力是 MP 的杀手锏。


六、坑与争议

通用 Mapper 的坑

  • Example 用字符串字段名——重构改字段名时不报错,运行时才挂
  • 生态薄——分页、性能监控、代码生成都要自己整合第三方
  • 维护活跃度低——近几年更新明显放缓

MyBatis-Plus 的争议点

  • Wrapper 写多了 SQL 会变难读——复杂查询还是建议回到 XML
  • ActiveRecord (Model<T>) 模式虽然炫,但耦合实体和持久层,不推荐生产用
  • 绑定较深——后期想换框架成本比通用 Mapper 高
  • 历史上有过 SQL 注入 CVE——升级版本要及时跟

最值得提醒的一点:Wrapper 不要写超过 3 层的复杂条件。可读性是 SQL 维护的天花板。


七、选型建议

通用 MapperMyBatis-Plus
单表 CRUD 占主导✓✓ (更顺手)
复杂多表 SQLXML 写XML + Wrapper 混用
团队熟悉 JPA
想用全套生态工具
介意框架侵入✓ 轻△ 较重
项目活跃度需求
学习曲线略陡(Wrapper、插件多)
社区资料较少大量

给一个直接的建议:

新项目优先 MyBatis-Plus,社区活跃、生态完整、Lambda 类型安全,几乎没有理由选通用 Mapper。

老项目已用通用 Mapper 且稳定,没必要为了 MP 重构;对接新表时可以两者共存,MP 接 BaseMapper<T>,通用 Mapper 留旧 Mapper<T>,互不干扰。


八、共存方案

如果项目里同时存在两套 Mapper 也不冲突,但有几个细节:

  1. 包路径要分开——别让一个 Mapper 同时继承两套接口
  2. MapperScan 要明确
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Configuration
public class MybatisConfig {
    @Bean
    public MapperScannerConfigurer tk() {
        MapperScannerConfigurer s = new MapperScannerConfigurer();
        s.setBasePackage("com.app.mapper.tk");
        s.setMarkerInterface(tk.mybatis.mapper.common.Mapper.class);
        return s;
    }

    @Bean
    public MapperScannerConfigurer mp() {
        MapperScannerConfigurer s = new MapperScannerConfigurer();
        s.setBasePackage("com.app.mapper.mp");
        s.setMarkerInterface(com.baomidou.mybatisplus.core.mapper.BaseMapper.class);
        return s;
    }
}
  1. 实体类注解最好不要混用——同一个实体要么用 javax.persistence.*,要么用 @TableName/@TableId,别两边都加

小结

把全文压成一句话:

通用 Mapper 是 MyBatis 的"轻量减法",MyBatis-Plus 是 MyBatis 的"完整加法"。在 2019 年之后的新项目,几乎没有理由再选通用 Mapper。

不管选哪个,记住几个一致的工程纪律:

  • 不要被工具骗了,别碰复杂多表 Wrapper——XML 永远是复杂查询的归宿
  • 不要写 ActiveRecord——实体类只描述结构,不调持久层方法
  • 不要忘了原生 MyBatis 的能力——MP/通用 Mapper 都只是 MyBatis 的扩展,复杂场景永远可以回到 Mapper.xml

把工具当工具用,别让工具反过来定义你的代码风格——这是任何框架选型都通用的原则。

使用 Hugo 构建
主题 StackJimmy 设计