MyBatis、MyBatis-Plus、通用 Mapper:一张图说清三者的血缘关系
引言
某天组长扔过来一个核心项目让你熟悉。你熟练地 clone 代码、导入 IDE、等 Maven 依赖下载完------然后你盯着 pom.xml 愣住了:
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:
java
// 你预想中
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 的代码吗?
这篇文章就用一张血缘关系图 + 三段演进史 + 一份对照表,把这笔糊涂账彻底算清楚。
先看这张图:三者的血缘关系
在讲演进史之前,先把结论摆出来。这是我画的一张关系图:
scss
┌─────────────────────────┐
│ 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,都只增强不替换
关键结论:
- MyBatis 是地基------两个扩展框架都建在它上面,谁也离不开谁
- tk-MyBatis 和 MyBatis-Plus 是兄弟------不是爹和儿子,是同一个爸爸的两个儿子
- tk-MyBatis 更早出生(2014),MyBatis-Plus 是后来者(2016)
- MyBatis-Plus 功能更多,但 tk-MyBatis 更轻量、更贴近原生 MyBatis
第一代:原始 MyBatis --- 屠龙刀,但每次都要从头挥
先回到一切开始的地方。一个典型 MyBatis 项目的 CRUD 长这样:
Mapper 接口
java
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 映射文件
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。
代码长什么样
实体类------用注解描述映射关系:
java
@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 接口------继承通用接口,一行代码搞定:
java
// 继承 tk 的 Mapper<T>,单表 CRUD 全有了
public interface UserMapper extends Mapper<User> {
// 如果你只有单表操作,这里可以是空的!
// 当然,复杂查询还是可以自己写
}
然后直接用:
java
@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 的核心机制
它的原理不复杂,分两步:
第一步:启动时扫描实体类,建立"实体 → 表"的映射字典。
kotlin
User.class + @Table(name = "t_user") → 知道这张表叫 t_user
+ @Id 标注字段 → 知道主键是 id
+ @Column 标注 → 知道 name 列 → name 字段
+ 驼峰转下划线 → 知道 create_time 列 → createTime 字段
第二步:当你调用 Mapper<T> 里的方法时,动态拼出 SQL。
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 的条件查询用的是字符串:
java
// tk-MyBatis:字段名是字符串,重构改字段名 → 这里静悄悄变 Bug
example.createCriteria().andEqualTo("age", 25);
MyBatis-Plus 用 Lambda 把字段名变成了编译期检查:
java
// 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(一个独立的第三方插件):
java
// tk-MyBatis + PageHelper:两个东西配合
PageHelper.startPage(1, 10);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
MyBatis-Plus 把分页内置了:
java
// 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 的代码生成器读一遍数据库的表结构,直接给你吐出:
sql
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,看起来只是换个依赖、换个父接口,实际上要动的:
java
依赖替换: 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
除非你同时满足以下三个条件:
- 项目还在频繁迭代(不是维护模式)
- 团队对 tk-MyBatis 的痛点(字符串字段名、Example API)已经无法忍受
- 有时间和人力做完整的回归测试
否则,把迁移的精力用来写业务代码,ROI 更高。
场景 C:学习阶段,想知道学哪个
→ 学 MyBatis-Plus,但先理解原始 MyBatis
正确的学习路径:
sql
第 1 步:原始 MyBatis(理解 SQL Mapping 的本质)
↓ 知道 XML 怎么绑定接口、动态 SQL 怎么拼
第 2 步:MyBatis-Plus(掌握现代开发效率工具)
↓ 用 LambdaQueryWrapper、分页插件、代码生成器
第 3 步:遇到 tk-MyBatis 项目时(花半天看文档就行)
核心差别就那三个注解 + 继承的接口名不一样
不要跳过第一步直接学 MyBatis-Plus。 否则出问题时你连是 MyBatis 的问题还是 MyBatis-Plus 的问题都分不清,很多"坑"其实是你不知道框架在背后替你做了什么。
总结
最后回到开头那张图的精神:
arduino
MyBatis 是引擎 → 学会它,你理解"数据怎么从数据库到 Java 对象"
tk-MyBatis 是自动挡 → 帮你换挡,但你仍然能手动控制
MyBatis-Plus 是自动驾驶辅助 → 帮你换挡 + 跟车 + 车道保持,但你得知道它什么时候会退出
三个框架不是敌人,不是替代关系 。它们是一条演进链上的三个节点,解决的是同一件事的不同层面:让 Java 开发者花更少的时间在 CRUD 上,花更多的时间在业务逻辑上。
tk-MyBatis 是这条路上的重要一步。它现在不够"时髦"了,但在很多公司代码库里,它仍然稳得一批。下次面试官问你"我们用的是 tk-MyBatis"时,你就可以说:
"我了解。它和 MyBatis-Plus 是兄弟扩展,都基于 MyBatis。核心差别是条件查询的字段名是字符串还是 Lambda 表达式。我之前用 MyBatis-Plus,适应 tk-MyBatis 的 API 只需要半天。"
这句话一说,面试官就知道你是真的理解,而不是背了八股文。