MyBatis-Plus与tk-MyBatis之争:谁更胜一筹?
引言某天组长扔过来一个核心项目让你熟悉。你熟练地 clone 代码、导入 IDE、等 Maven 依赖下载完——然后你盯着 pom.xml 愣住了!-- 你预想中的依赖 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /dependency !-- 实际看到的依赖 -- dependency groupIdtk.mybatis/groupId artifactIdmapper-spring-boot-starter/artifactId /dependency再打开一个 Mapper 接口——继承的不是BaseMapper而是Mapper// 你预想中 public interface UserMapper extends BaseMapperUser { } // 实际上 public interface UserMapper extends MapperUser { }然后你去网上搜。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) │ │ │ │ │ │ • MapperT 通用接口 │ │ • BaseMapperT 通用接口 │ │ • 注解映射实体 → 表 │ │ • LambdaQueryWrapper │ │ • 自动生成单表 CRUD │ │ • 分页插件 │ │ • Example 条件查询 │ │ • 代码生成器 │ │ • 轻量只做增强 │ │ • 逻辑删除/乐观锁/租户 │ └────────────┬───────────────┘ └────────────┬─────────────┘ │ │ └────────────┬─────────────────────┘ │ ▼ 两者是【平级扩展】不是父子关系 都依赖 MyBatis都只增强不替换关键结论1.MyBatis 是地基——两个扩展框架都建在它上面谁也离不开谁2.tk-MyBatis 和 MyBatis-Plus 是兄弟——不是爹和儿子是同一个爸爸的两个儿子3.tk-MyBatis 更早出生2014MyBatis-Plus 是后来者20164.MyBatis-Plus 功能更多但 tk-MyBatis 更轻量、更贴近原生 MyBatis第一代原始 MyBatis — 屠龙刀但每次都要从头挥先回到一切开始的地方。一个典型 MyBatis 项目的 CRUD 长这样Mapper 接口public interface UserMapper { User selectById(Long id); ListUser selectAll(); int insert(User user); int updateById(User user); int deleteById(Long id); ListUser selectByCondition(Param(name) String name, Param(age) Integer age); }XML 映射文件mapper namespacecom.example.mapper.UserMapper select idselectById resultTypecom.example.entity.User SELECT id, name, age, email, phone, create_time, update_time FROM t_user WHERE id #{id} /select select idselectAll resultTypecom.example.entity.User SELECT id, name, age, email, phone, create_time, update_time FROM t_user ORDER BY id DESC /select insert idinsert INSERT INTO t_user (name, age, email, phone, create_time) VALUES (#{name}, #{age}, #{email}, #{phone}, NOW()) /insert update idupdateById UPDATE t_user SET name#{name}, age#{age}, email#{email}, phone#{phone}, update_timeNOW() WHERE id #{id} /update delete iddeleteById DELETE FROM t_user WHERE id #{id} /delete select idselectByCondition resultTypecom.example.entity.User SELECT id, name, age, email, phone, create_time, update_time FROM t_user where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testage ! 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 的唯一区别就是表名和字段名不同。他的思路很简单你在实体类上用注解标清楚哪个字段是主键、这个类对应哪张表然后继承我的MapperT接口——单表 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 的 MapperT单表 CRUD 全有了 public interface UserMapper extends MapperUser { // 如果你只有单表操作这里可以是空的 // 当然复杂查询还是可以自己写 }然后直接用Autowired private UserMapper userMapper; // 按主键查 —— 不用写 XML User user userMapper.selectByPrimaryKey(1L); // 查全部 ListUser users userMapper.selectAll(); // 按条件查 —— Example 对象 Example example new Example(User.class); example.createCriteria() .andEqualTo(age, 25) .andLike(name, %张%); ListUser 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 字段第二步当你调用MapperT里的方法时动态拼出 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 表达式重构改名 → 编译报错一改全改 ListUser users userMapper.selectList( new LambdaQueryWrapperUser() .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); ListUser users userMapper.selectAll(); PageInfoUser pageInfo new PageInfo(users);MyBatis-Plus 把分页内置了// MyBatis-Plus分页是原生的 PageUser page new Page(1, 10); PageUser result userMapper.selectPage(page, new LambdaQueryWrapperUser().gt(User::getAge, 18));少了一个第三方依赖配置也更简单——一个MybatisPlusInterceptor注册完就全局生效。3. 代码生成器——实体、Mapper、Service、Controller 一键生成这是 tk-MyBatis 完全没有的东西。MyBatis-Plus 的代码生成器读一遍数据库的表结构直接给你吐出User.java ← 实体类 UserMapper.java ← Mapper 接口继承 BaseMapperUser UserMapper.xml ← XML可能只需要一个空的 resultMap UserService.java ← Service 接口继承 IServiceUser UserServiceImpl.java ← Service 实现继承 ServiceImplUserMapper, User UserController.java ← ControllerRESTful CRUD 全齐30 张表的 CRUD 工程5 分钟生成完。然后你只改有特殊逻辑的其余的直接用。4. 周边功能全家桶这些是 tk-MyBatis 没有、MyBatis-Plus 内置的功能MyBatis-Plus 怎么做逻辑删除TableLogic注解删改自动变 UPDATE set is_deleted1乐观锁Version注解更新时自动带版本号校验自动填充TableField(fill ...)createTime/updateTime 自动填多租户配置一个 TenantLineHandler所有 SQL 自动拼接 tenant_id字段加密TypeHandler 扩展存的时候加密取的时候解密主键策略TableId(type IdType.ASSIGN_ID)雪花 ID 开箱即用这些功能 tk-MyBatis 也能实现但得自己写代码或者集成别的插件。MyBatis-Plus 把它们全部做成了开箱即用的注解/配置。一张对照表三者的完整对比维度MyBatis原始tk-MyBatis通用 MapperMyBatis-Plus单表 CRUD手写 XML自动生成自动生成条件查询方式XML 动态 SQLExample 对象字符串字段名LambdaQueryWrapper类型安全分页手写或 PageHelper配合 PageHelper内置分页插件代码生成器无无可用第三方内置功能强大逻辑删除手写手写TableLogic乐观锁手写手写Version自动填充手写手写TableField(fill...)多租户手写手写内置拦截器Lambda 支持无无有最大亮点社区活跃度稳定Apache 维护低维护缓慢高持续迭代学习成本中理解 XML 绑定低贴近原生 MyBatis中功能多有坑包大小~1.6MB~300KB~2.5MB创业年份201020142016最新版本3.5.1620244.4.220233.5.52024重头戏为什么有的公司还在用 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 MapperT → extends BaseMapperT 注解替换 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 张表手写也花不了一天。原因 4tk-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 只需要半天。这句话一说面试官就知道你是真的理解而不是背了八股文。