MyBatis-Plus与tk-MyBatis之争:谁更胜一筹?
引言
某天组长扔过来一个核心项目让你熟悉。你熟练地 clone 代码、导入 IDE、等 Maven 依赖下载完——然后你盯着 pom.xml 愣住了:
<!-- 你预想中的依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!-- 实际看到的依赖 --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> </dependency>再打开一个 Mapper 接口——继承的不是BaseMapper,而是Mapper:
// 你预想中 public interface UserMapper extends BaseMapper<User> { } // 实际上 public interface UserMapper extends Mapper<User> { }然后你去网上搜。B 站、掘金、公众号,铺天盖地全是 MyBatis-Plus 的教程。tk-MyBatis?通用 Mapper?首页几乎刷不到。
但你面前的这个项目,已经在生产环境稳稳跑了五六年,几百张表、几十万行代码,全是用这个"搜不到教程"的框架写的。
然后你脑子里冒出一串问题:
• tk-MyBatis 和 MyBatis-Plus 是什么关系?谁抄的谁?
• 为什么老项目用这个,新项目用那个?
• 通用 Mapper 是过时了吗?新项目还能不能用?
• 我学了 MyBatis-Plus,能直接上手 tk-MyBatis 的代码吗?
这篇文章就用一张血缘关系图 + 三段演进史 + 一份对照表,把这笔糊涂账彻底算清楚。
先看这张图:三者的血缘关系
在讲演进史之前,先把结论摆出来。这是我画的一张关系图:
┌─────────────────────────┐ │ Apache iBATIS │ │ (2002, Clinton Begin) │ └────────────┬────────────┘ │ 2010 年改名 ▼ ┌─────────────────────────┐ │ MyBatis │ │ 基础框架:SQL Mapping │ │ • XML Mapper + 接口绑定 │ │ • 动态 SQL │ │ • 结果映射 │ └──────┬──────────┬───────┘ │ │ ┌──────────┘ └──────────┐ │ 扩展 扩展 │ ▼ ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ tk-MyBatis (通用 Mapper) │ │ MyBatis-Plus │ │ (2014, abel533) │ │ (2016, 青苗/miemie) │ │ │ │ │ │ • Mapper<T> 通用接口 │ │ • BaseMapper<T> 通用接口 │ │ • 注解映射实体 → 表 │ │ • LambdaQueryWrapper │ │ • 自动生成单表 CRUD │ │ • 分页插件 │ │ • Example 条件查询 │ │ • 代码生成器 │ │ • 轻量,只做增强 │ │ • 逻辑删除/乐观锁/租户 │ └────────────┬───────────────┘ └────────────┬─────────────┘ │ │ └────────────┬─────────────────────┘ │ ▼ 两者是【平级扩展】,不是父子关系 都依赖 MyBatis,都只增强不替换关键结论:
1.MyBatis 是地基——两个扩展框架都建在它上面,谁也离不开谁
2.tk-MyBatis 和 MyBatis-Plus 是兄弟——不是爹和儿子,是同一个爸爸的两个儿子
3.tk-MyBatis 更早出生(2014),MyBatis-Plus 是后来者(2016)
4.MyBatis-Plus 功能更多,但 tk-MyBatis 更轻量、更贴近原生 MyBatis
第一代:原始 MyBatis — 屠龙刀,但每次都要从头挥
先回到一切开始的地方。一个典型 MyBatis 项目的 CRUD 长这样:
Mapper 接口
public interface UserMapper { User selectById(Long id); List<User> selectAll(); int insert(User user); int updateById(User user); int deleteById(Long id); List<User> selectByCondition(@Param("name") String name, @Param("age") Integer age); }XML 映射文件
<mapper namespace="com.example.mapper.UserMapper"> <select id="selectById" resultType="com.example.entity.User"> SELECT id, name, age, email, phone, create_time, update_time FROM t_user WHERE id = #{id} </select> <select id="selectAll" resultType="com.example.entity.User"> SELECT id, name, age, email, phone, create_time, update_time FROM t_user ORDER BY id DESC </select> <insert id="insert"> INSERT INTO t_user (name, age, email, phone, create_time) VALUES (#{name}, #{age}, #{email}, #{phone}, NOW()) </insert> <update id="updateById"> UPDATE t_user SET name=#{name}, age=#{age}, email=#{email}, phone=#{phone}, update_time=NOW() WHERE id = #{id} </update> <delete id="deleteById"> DELETE FROM t_user WHERE id = #{id} </delete> <select id="selectByCondition" resultType="com.example.entity.User"> SELECT id, name, age, email, phone, create_time, update_time FROM t_user <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="age != null"> AND age = #{age} </if> </where> ORDER BY id DESC </select> </mapper>全项目有 30 张表,你就得写 30 套几乎一模一样的 CRUD XML。而且每张表的字段名还不一样——复制粘贴完还得一个个改字段名,改漏一个就是线上 Bug。
这就是原始 MyBatis 的核心矛盾:灵活是真的灵活(SQL 完全由你控制),繁琐也是真的繁琐(重复劳动占了 80%)。
虽然有些框架都有代码生成器,但CRUD XML的文件数量并没有减少。
第二代:tk-MyBatis — "既然 80% 的 SQL 都一样,那干脆别写了"
它解决了什么
2014 年,一个叫 abel533的开发者忍不了项目里90% 的 CRUD 操作,本质都是单表操作——查一条、查全部、插入、更新、删除、按条件查。这些 SQL 的唯一区别就是表名和字段名不同。
他的思路很简单:
你在实体类上用注解标清楚"哪个字段是主键"、"这个类对应哪张表",然后继承我的
Mapper<T>接口——单表 CRUD 的 SQL 我帮你生成,你不用再写一句 XML。
代码长什么样
实体类——用注解描述映射关系:
@Table(name = "t_user") // 告诉框架:这个实体对应 t_user 表 public class User { @Id // 这是主键 @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增 private Long id; @Column(name = "name") // 列名映射 private String name; private Integer age; // 不写 @Column 默认驼峰转下划线 → age private String email; private String phone; @Column(name = "create_time") private Date createTime; @Column(name = "update_time") private Date updateTime; // getter / setter 省略 }Mapper 接口——继承通用接口,一行代码搞定:
// 继承 tk 的 Mapper<T>,单表 CRUD 全有了 public interface UserMapper extends Mapper<User> { // 如果你只有单表操作,这里可以是空的! // 当然,复杂查询还是可以自己写 }然后直接用:
@Autowired private UserMapper userMapper; // 按主键查 —— 不用写 XML User user = userMapper.selectByPrimaryKey(1L); // 查全部 List<User> users = userMapper.selectAll(); // 按条件查 —— Example 对象 Example example = new Example(User.class); example.createCriteria() .andEqualTo("age", 25) .andLike("name", "%张%"); List<User> result = userMapper.selectByExample(example); // 插入 userMapper.insert(newUser); // 按主键更新(只更新非 null 字段) userMapper.updateByPrimaryKeySelective(user); // 按主键删除 userMapper.deleteByPrimaryKey(1L);XML 文件?单表操作不需要了。只有多表联查、复杂动态 SQL 才需要自己写。
tk-MyBatis 的核心机制
它的原理不复杂,分两步:
第一步:启动时扫描实体类,建立"实体 → 表"的映射字典。
User.class + @Table(name = "t_user") → 知道这张表叫 t_user + @Id 标注字段 → 知道主键是 id + @Column 标注 → 知道 name 列 → name 字段 + 驼峰转下划线 → 知道 create_time 列 → createTime 字段第二步:当你调用Mapper<T>里的方法时,动态拼出 SQL。
selectByPrimaryKey(1L) → SELECT id, name, age, email, phone, create_time, update_time FROM t_user WHERE id = ? updateByPrimaryKeySelective(user) → UPDATE t_user SET name=?, age=?, email=?, phone=?, update_time=? WHERE id = ? (只拼出值不为 null 的字段)它本质上是一个SQL 拼接引擎,启动时读注解建立元数据,运行时根据方法名 + 参数动态组装 SQL。
tk-MyBatis 的优势和局限
优势:
• 消灭了 80% 的 CRUD XML,项目清爽很多
• 轻量,贴近 MyBatis 原生,学习成本低
• 不绑架你——复杂查询还是写 XML,和它和平共处
• 成熟稳定,很多老项目跑了七八年不出问题
局限:
• 条件查询靠
Example对象,API 不够优雅(字符串传列名,重构时容易漏)• 没有 Lambda 表达式支持,字段名是字符串,IDE 重构帮不了你
• 不提供分页插件、代码生成器、逻辑删除等周边功能——想要得自己集成 PageHelper
• 社区活跃度下降,更新频率远不如 MyBatis-Plus
第三代:MyBatis-Plus — "还不够,我要把能省的全省了"
它多做了什么
2016 年,MyBatis-Plus 登场。它的思路和 tk-MyBatis 一样——"单表 CRUD 你别写了,我来生成"。但它问了一个更贪心的问题:
tk-MyBatis 只省了 CRUD,那条件查询的字段名能不能也类型安全?分页能不能一行代码?代码本身能不能自动生成?
于是它在 tk-MyBatis 的基础上(逻辑上,不是代码上),多做了这几件事:
1. Lambda 表达式——字段名再也不是字符串
tk-MyBatis 的条件查询用的是字符串:
// tk-MyBatis:字段名是字符串,重构改字段名 → 这里静悄悄变 Bug example.createCriteria().andEqualTo("age", 25);MyBatis-Plus 用 Lambda 把字段名变成了编译期检查:
// MyBatis-Plus:字段名是 Lambda 表达式,重构改名 → 编译报错,一改全改 List<User> users = userMapper.selectList( new LambdaQueryWrapper<User>() .eq(User::getAge, 25) // User::getAge 不是字符串 .like(User::getName, "张") );这是两个框架体验上最大的分水岭。tk-MyBatis 里重构实体类字段名是一场噩梦——你得全项目搜索那个字段名的字符串。MyBatis-Plus 里重构只是 IDE 一键 rename——因为User::getAge是 Java 方法引用,编译器帮你盯着。
2. 分页插件——真·一行代码分页
tk-MyBatis 时代,分页靠 PageHelper(一个独立的第三方插件):
// tk-MyBatis + PageHelper:两个东西配合 PageHelper.startPage(1, 10); List<User> users = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(users);MyBatis-Plus 把分页内置了:
// MyBatis-Plus:分页是原生的 Page<User> page = new Page<>(1, 10); Page<User> result = userMapper.selectPage(page, new LambdaQueryWrapper<User>().gt(User::getAge, 18));少了一个第三方依赖,配置也更简单——一个MybatisPlusInterceptor注册完就全局生效。
3. 代码生成器——实体、Mapper、Service、Controller 一键生成
这是 tk-MyBatis 完全没有的东西。MyBatis-Plus 的代码生成器读一遍数据库的表结构,直接给你吐出:
User.java ← 实体类 UserMapper.java ← Mapper 接口(继承 BaseMapper<User>) UserMapper.xml ← XML(可能只需要一个空的 <resultMap>) UserService.java ← Service 接口(继承 IService<User>) UserServiceImpl.java ← Service 实现(继承 ServiceImpl<UserMapper, User>) UserController.java ← Controller(RESTful CRUD 全齐)30 张表的 CRUD 工程,5 分钟生成完。然后你只改有特殊逻辑的,其余的直接用。
4. 周边功能全家桶
这些是 tk-MyBatis 没有、MyBatis-Plus 内置的:
功能 | MyBatis-Plus 怎么做 |
|---|---|
| 逻辑删除 | @TableLogic注解,删改自动变 UPDATE set is_deleted=1 |
| 乐观锁 | @Version注解,更新时自动带版本号校验 |
| 自动填充 | @TableField(fill = ...),createTime/updateTime 自动填 |
| 多租户 | 配置一个 TenantLineHandler,所有 SQL 自动拼接 tenant_id |
| 字段加密 | TypeHandler 扩展,存的时候加密取的时候解密 |
| 主键策略 | @TableId(type = IdType.ASSIGN_ID),雪花 ID 开箱即用 |
这些功能 tk-MyBatis 也能实现,但得自己写代码或者集成别的插件。MyBatis-Plus 把它们全部做成了开箱即用的注解/配置。
一张对照表:三者的完整对比
维度 | MyBatis(原始) | tk-MyBatis(通用 Mapper) | MyBatis-Plus |
|---|---|---|---|
| 单表 CRUD | 手写 XML | 自动生成 | 自动生成 |
| 条件查询方式 | XML 动态 SQL | Example 对象(字符串字段名) | LambdaQueryWrapper(类型安全) |
| 分页 | 手写或 PageHelper | 配合 PageHelper | 内置分页插件 |
| 代码生成器 | 无 | 无(可用第三方) | 内置,功能强大 |
| 逻辑删除 | 手写 | 手写 | @TableLogic |
| 乐观锁 | 手写 | 手写 | @Version |
| 自动填充 | 手写 | 手写 | @TableField(fill=...) |
| 多租户 | 手写 | 手写 | 内置拦截器 |
| Lambda 支持 | 无 | 无 | 有(最大亮点) |
| 社区活跃度 | 稳定(Apache 维护) | 低(维护缓慢) | 高(持续迭代) |
| 学习成本 | 中(理解 XML 绑定) | 低(贴近原生 MyBatis) | 中(功能多,有坑) |
| 包大小 | ~1.6MB | ~300KB | ~2.5MB |
| 创业年份 | 2010 | 2014 | 2016 |
| 最新版本 | 3.5.16(2024) | 4.4.2(2023) | 3.5.5(2024) |
重头戏:为什么有的公司还在用 tk-MyBatis?
这可能是你最关心的问题。一个社区不活跃、功能不如 MyBatis-Plus 多的框架,为什么还能在 2026 年的公司代码里看到?
原因 1:历史遗留——"它跑了八年没出过事"
很多公司的核心项目是 2017~2019 年启动的。那时候 MyBatis-Plus 才刚起步(2016 年发布,早期版本 Bug 不少),而 tk-MyBatis 已经稳定运行了 3 年多。
技术选型在那时选了 tk-MyBatis + PageHelper 的组合,跑了八年没出过大问题。对于核心业务系统,"没出过事"比"功能更先进"重要一万倍。技术负责人没有动力冒风险去换一个框架。
原因 2:迁移成本——"不是不能换,是不划算"
从 tk-MyBatis 迁到 MyBatis-Plus,看起来只是换个依赖、换个父接口,实际上要动的:
依赖替换: tk-mybatis → mybatis-plus-boot-starter 接口替换: extends Mapper<T> → extends BaseMapper<T> 注解替换: @Table → @TableName, @Id → @TableId, @Column → @TableField 条件查询: Example 对象 → LambdaQueryWrapper 分页方式: PageHelper → MyBatis-Plus Page 配置变更: MapperScannerConfigurer → @MapperScan + MybatisPlusInterceptor 测试回归: 所有涉及 CRUD 的测试用例全部重跑一个 100 张表的项目,光改注解和接口签名就要二三十人天,再加上回归测试、预发验证——换框架的总成本可能奔着两个月去了。业务部门不会为"代码更优雅"批这个预算。
原因 3:够用原则——"我们又不需要那些高级功能"
很多内部管理系统、后台项目的需求是这样的:
单表 CRUD + 几个多表联查报表。没了。
这种场景下,tk-MyBatis 提供的selectByPrimaryKey、selectByExample、insert、updateByPrimaryKeySelective已经完全足够。Lambda 表达式?逻辑删除?代码生成器?不需要——项目就那 20 张表,手写也花不了一天。
原因 4:tk-MyBatis 本身没那么差
虽然功能不如 MyBatis-Plus 多,但 tk-MyBatis在它定义的边界内做得很扎实:
• 单表 CRUD 的 SQL 生成是正确的
• 不侵入你的业务代码(就是几个注解 + 继承一个接口)
• 和原生 MyBatis 完全兼容(写 XML 照样写)
• 性能开销极小(只比原生 MyBatis 多一点启动时的元数据解析)
对于一个"不需要花里胡哨功能"的项目,tk-MyBatis 是一个非常理性的选择。
选型建议:三个场景,三个答案
场景 A:新项目,小团队,快速开发
→ 选 MyBatis-Plus
代码生成器一分钟建好工程骨架,Lambda 表达式重构不慌,分页/逻辑删除/自动填充开箱即用。你自己只需要写那 20% 的复杂查询。
场景 B:维护老项目,当前是 tk-MyBatis
→ 不建议迁,继续用 tk-MyBatis
除非你同时满足以下三个条件:
1. 项目还在频繁迭代(不是维护模式)
2. 团队对 tk-MyBatis 的痛点(字符串字段名、Example API)已经无法忍受
3. 有时间和人力做完整的回归测试
否则,把迁移的精力用来写业务代码,ROI 更高。
场景 C:学习阶段,想知道学哪个
→ 学 MyBatis-Plus,但先理解原始 MyBatis
正确的学习路径:
第 1 步:原始 MyBatis(理解 SQL Mapping 的本质) ↓ 知道 XML 怎么绑定接口、动态 SQL 怎么拼 第 2 步:MyBatis-Plus(掌握现代开发效率工具) ↓ 用 LambdaQueryWrapper、分页插件、代码生成器 第 3 步:遇到 tk-MyBatis 项目时(花半天看文档就行) 核心差别就那三个注解 + 继承的接口名不一样不要跳过第一步直接学 MyBatis-Plus。否则出问题时你连是 MyBatis 的问题还是 MyBatis-Plus 的问题都分不清,很多"坑"其实是你不知道框架在背后替你做了什么。
总结
最后回到开头那张图的精神:
MyBatis 是引擎 → 学会它,你理解"数据怎么从数据库到 Java 对象" tk-MyBatis 是自动挡 → 帮你换挡,但你仍然能手动控制 MyBatis-Plus 是自动驾驶辅助 → 帮你换挡 + 跟车 + 车道保持,但你得知道它什么时候会退出三个框架不是敌人,不是替代关系。它们是一条演进链上的三个节点,解决的是同一件事的不同层面:让 Java 开发者花更少的时间在 CRUD 上,花更多的时间在业务逻辑上。
tk-MyBatis 是这条路上的重要一步。它现在不够"时髦"了,但在很多公司代码库里,它仍然稳得一批。下次面试官问你"我们用的是 tk-MyBatis"时,你就可以说:
"我了解。它和 MyBatis-Plus 是兄弟扩展,都基于 MyBatis。核心差别是条件查询的字段名是字符串还是 Lambda 表达式。我之前用 MyBatis-Plus,适应 tk-MyBatis 的 API 只需要半天。"
这句话一说,面试官就知道你是真的理解,而不是背了八股文。