MyBatis、MyBatis-Plus、通用 Mapper:一张图说清三者的血缘关系

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,都只增强不替换

关键结论:

  1. MyBatis 是地基------两个扩展框架都建在它上面,谁也离不开谁
  2. tk-MyBatis 和 MyBatis-Plus 是兄弟------不是爹和儿子,是同一个爸爸的两个儿子
  3. tk-MyBatis 更早出生(2014),MyBatis-Plus 是后来者(2016)
  4. 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 提供的 selectByPrimaryKeyselectByExampleinsertupdateByPrimaryKeySelective 已经完全足够。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

正确的学习路径:

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 只需要半天。"

这句话一说,面试官就知道你是真的理解,而不是背了八股文。

相关推荐
爬楼的猪2 小时前
WSL 的 GUI 为什么总是卡卡的
后端
程序猿DD4 小时前
OPC必备!在 Cloudflare Worker 上免费部署 AI 网关:集中管理与供应 Token
后端
LucianaiB4 小时前
遇到 Seed Evolving,我终于把脑海里的小说世界图谱做出来了:《斗破苍穹》篇
后端
JuiceFS5 小时前
GPFS、Alluxio、JuiceFS 怎么选?一文看懂架构与适用场景
人工智能·后端
武子康5 小时前
1.2GB 离线语音 Agent 真正值得复用的不是 908ms:四阶段职责 + 状态感知 Tool Schema + 可观测时间锚点
人工智能·后端·agent
掘金者阿豪5 小时前
那些年踩过的 MySQL 迁移坑,这次终于不用绕了
后端
用户7713970207065 小时前
深夜食堂:那个让我加班到凌晨的 ! 符号
后端
swipe5 小时前
03|Axios 请求进了后端之后:Controller、Request、Response 是怎么接住它的?
前端·后端·全栈
swipe5 小时前
02|从 `pnpm dev` 到 Spring Boot 启动:后端服务到底怎么跑起来?
前端·后端·全栈