
1. 从MyBatis的“体力活”到TkMyBatis的“自动化”如果你用过原生的MyBatis肯定对下面这种场景不陌生为了一个简单的用户表user的增删改查你需要手动编写UserMapper.xml里面充斥着大量重复的SQL片段。比如一个根据主键查询的语句你得写select idselectByPrimaryKey resultMapBaseResultMapSELECT * FROM user WHERE id #{id}/select一个插入语句你得把十几个字段一个个列出来。这还只是一个实体当项目有几十上百张表时这种重复的“体力劳动”不仅枯燥还极易出错字段漏写、类型不匹配都是家常便饭。TkMyBatis的出现就是为了把开发者从这种重复的SQL编写中解放出来。它不是一个全新的ORM框架而是基于MyBatis的一款增强工具包。它的核心思想是“通用Mapper”通过继承一个预定义好的Mapper接口你的实体类就能自动获得一套完备的单表操作方法无需再写任何XML映射文件。你只需要定义好实体类并让它继承一个特定的父类或使用注解TkMyBatis就能在运行时根据实体类的字段信息动态生成对应的SQL语句。简单来说TkMyBatis让MyBatis用起来有了点JPAJava Persistence API那种“约定大于配置”的味道但又保留了MyBatis对SQL的灵活控制力。你既享受了自动化CRUD的便利在需要复杂查询时又能随时手写SQL两者可以完美结合。对于追求开发效率的中小型项目或者那些单表操作占绝大多数的业务模块TkMyBatis是一个能显著提升生产力的利器。2. TkMyBatis核心机制通用Mapper与动态SQL生成要理解TkMyBatis怎么工作得先拆解它的两个核心部件通用Mapper接口和基于反射的SQL生成器。2.1 通用Mapper接口定义操作契约TkMyBatis提供了一系列通用的Mapper接口最常用的是BaseMapperT。这个接口是一个泛型接口其中的T代表你的实体类类型。它里面预先定义了几十个常用的单表操作方法。例如int insert(T record): 插入一条记录所有字段都会参与插入。int insertSelective(T record): 选择性插入只有不为null的字段会出现在SQL中。int deleteByPrimaryKey(Object key): 根据主键删除。T selectByPrimaryKey(Object key): 根据主键查询。ListT selectAll(): 查询所有记录。int updateByPrimaryKey(T record): 根据主键更新所有字段。int updateByPrimaryKeySelective(T record): 根据主键选择性更新只更新非null字段。ListT selectByExample(Object example): 根据条件查询使用Example对象。你的业务Mapper接口只需要继承这个BaseMapper并指定泛型就自动拥有了所有这些方法。// 你的业务Mapper接口 public interface UserMapper extends BaseMapperUser { // 这里可以添加自定义的复杂查询方法 ListUser selectComplexQuery(Param(param) SomeParam param); } // 对应的User实体类 Data // 使用Lombok简化代码 Table(name user) // 指定表名 public class User { Id // 标记为主键 GeneratedValue(strategy GenerationType.IDENTITY) // 主键自增策略 private Long id; private String username; private String email; // ... 其他字段和getter/setter }注意你不需要为UserMapper编写对应的UserMapper.xml来实现insert、selectByPrimaryKey等方法。这就是“通用”的含义。2.2 动态SQL生成反射与注解的魔法那么UserMapper.insert(user)被调用时SQL语句INSERT INTO user (id, username, email ...) VALUES (?, ?, ? ...)是从哪里来的呢这就是TkMyBatis的SQL生成器在起作用。启动时扫描在Spring Boot项目启动时TkMyBatis的配置类通常是MapperScan注解配合tk.mybatis.spring.annotation.MapperScan会扫描所有继承了BaseMapper的接口。注册MapperMyBatis的Configuration对象中会为这些接口注册一个特殊的MapperProxy代理。当调用接口方法时会被这个代理拦截。方法解析代理根据调用的方法名如insertSelective和实体类的泛型信息User.class判断需要执行何种SQL操作。反射获取元数据通过Java反射获取User类的所有字段信息。同时会读取字段上的注解如Table(name“user”)获取表名Id识别主键Column(name “user_name”)获取数据库列名如果字段名和列名不一致。构建SQL根据操作类型INSERT/SELECT/UPDATE/DELETE和获取的元数据表名、列名、主键动态拼接出完整的SQL语句。对于insertSelective和updateByPrimaryKeySelective生成逻辑会判断实体对象中每个字段的值是否为null从而动态决定是否将该字段加入SQL。执行与映射生成的SQL被交给MyBatis执行结果集再通过反射映射回User对象。整个过程对开发者透明你感觉就像在调用一个已经实现了的方法一样。这种机制极大地减少了样板代码但前提是实体类的定义必须规范注解要使用正确否则SQL生成器会“猜”错你的意图。3. 从零开始Spring Boot整合TkMyBatis全流程理论讲完了我们动手搭一个。这里以Spring Boot 2.x MySQL为例演示最标准的集成流程。3.1 项目初始化与依赖引入首先创建一个标准的Spring Boot项目。在pom.xml中除了基础的Spring Boot Starter依赖关键是要引入tk.mybatis的Spring Boot Starter。这里有一个版本匹配的大坑TkMyBatis的版本需要与你的MyBatis版本以及MyBatis-Spring-Boot-Starter版本兼容。一个经过验证的、稳定的依赖组合如下dependencies !-- Spring Boot Web (根据项目需要) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- MyBatis官方Spring Boot Starter -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version !-- 注意版本 -- /dependency !-- TkMyBatis的核心启动器 -- dependency groupIdtk.mybatis/groupId artifactIdmapper-spring-boot-starter/artifactId version2.1.5/version !-- 注意版本 -- /dependency !-- 代码简化工具非必须但强烈推荐 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意mybatis-spring-boot-starter2.2.x 版本与mapper-spring-boot-starter2.1.5 是经过社区大量项目验证的稳定搭配。切勿盲目使用最新版否则可能遇到类冲突、配置不生效等奇怪问题。如果你用的是Spring Boot 3.x需要寻找支持MyBatis 3.5.x和Spring 6.x的对应TkMyBatis版本社区可能有不同的分支或替代方案。3.2 实体类与Mapper接口定义实体类 (User.java)这是TkMyBatis工作的基石注解必须打对。package com.example.demo.entity; import lombok.Data; import javax.persistence.*; import java.util.Date; Data // Lombok注解自动生成getter, setter, toString等 Table(name t_user) // Table是JPA注解name属性指定数据库表名 public class User { /** * 主键字段。 * Id 标识为主键。 * GeneratedValue 主键生成策略。GenerationType.IDENTITY 对应数据库自增如MySQL的AUTO_INCREMENT。 * 如果是UUID或者程序分配策略不同。 */ Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; /** * 如果实体类字段名与数据库列名不一致使用Column注解映射。 * 例如数据库列名为 user_name而字段名为 username。 */ Column(name user_name) private String username; private String password; // 字段名与列名相同可省略Column private String email; /** * 一些特殊类型如日期。 * 确保数据库类型如datetime/timestamp与Java类型能正确转换。 */ private Date createTime; private Date updateTime; // ... 其他字段 }Mapper接口 (UserMapper.java)极其简洁继承BaseMapper并指定泛型即可。package com.example.demo.mapper; import com.example.demo.entity.User; import org.springframework.stereotype.Repository; import tk.mybatis.mapper.common.Mapper; Repository // Spring的注解可被扫描到也可用Mapper。但通常用下面的MapperScan统一扫描。 public interface UserMapper extends MapperUser { // 此处可以定义自己的非通用方法但需要配套的XML或Select注解提供SQL // 例如 ListUser selectByCustomCondition(String condition); }这里继承的是tk.mybatis.mapper.common.Mapper它是BaseMapper的父接口提供了最基础的方法。你也可以继承BaseMapper它包含更多方法如selectByIds。根据你的需求选择。3.3 核心配置application.yml在resources/application.yml中需要配置数据源和MyBatis。TkMyBatis的Starter已经帮我们做了很多自动配置我们只需要关注最基本的。spring: datasource: url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: # 配置type-aliases包路径这样在XML里就可以用类名代替全限定名 type-aliases-package: com.example.demo.entity # 如果你有自定义的XML文件需要配置mapper-locations # mapper-locations: classpath:mapper/*.xml configuration: # 开启驼峰命名自动映射。数据库列名 user_name 会自动映射到实体类字段 userName map-underscore-to-camel-case: true # 打印SQL日志调试时非常有用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # TkMyBatis 特定配置 (非必须有默认值) mapper: # 通用Mapper的基接口多个用逗号隔开。通常不需要改。 mappers: tk.mybatis.mapper.common.Mapper # 主键自增回写方法执行顺序,默认AFTER可选BEFORE identity: MYSQL # 设置是否支持方法上的注解默认false。如果为trueMapper接口上的Select等注解会生效。 enable-method-annotations: false最关键的是mybatis.configuration.map-underscore-to-camel-case: true。这是MyBatis的配置但对TkMyBatis同样重要。因为数据库列名常用下划线user_name而Java字段常用驼峰userName开启这个选项后TkMyBatis在生成SQL和映射结果时会自动进行转换你就不必为每个字段写Column注解了。3.4 启动类与Mapper扫描最后在Spring Boot启动类上使用TkMyBatis提供的MapperScan注解来扫描你的Mapper接口。package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import tk.mybatis.spring.annotation.MapperScan; SpringBootApplication // 关键使用tk.mybatis的MapperScan而不是org.mybatis.spring.annotation的。 // basePackages指定你的Mapper接口所在的包。 MapperScan(basePackages com.example.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }这里是最容易踩坑的地方之一如果你错误地导入了MyBatis官方的MapperScanTkMyBatis的通用Mapper机制将无法注册你的UserMapper虽然能被Spring管理但继承自BaseMapper的那些方法会找不到对应的SQL语句调用时直接报错Invalid bound statement (not found)。完成以上四步一个整合了TkMyBatis的Spring Boot项目骨架就搭建好了。启动项目如果控制台没有报错并且能成功打印出数据源初始化的日志就说明整合成功。4. 实战CRUD不只是简单的调用环境搭好了我们来实际使用一下。创建一个UserService和UserController。4.1 基础CRUD操作示例Service public class UserService { Autowired private UserMapper userMapper; // 1. 插入 public int addUser(User user) { // insert 方法会插入所有字段即使为null // return userMapper.insert(user); // 更推荐使用 insertSelective只会插入非null字段更灵活兼容性更好 return userMapper.insertSelective(user); } // 2. 根据主键查询 public User getUserById(Long id) { return userMapper.selectByPrimaryKey(id); } // 3. 更新 public int updateUser(User user) { // updateByPrimaryKey 更新所有字段如果某字段为null数据库里就会被更新为null // return userMapper.updateByPrimaryKey(user); // 更推荐使用 updateByPrimaryKeySelective只更新非null字段 return userMapper.updateByPrimaryKeySelective(user); } // 4. 删除 public int deleteUser(Long id) { return userMapper.deleteByPrimaryKey(id); } // 5. 查询所有 public ListUser getAllUsers() { return userMapper.selectAll(); } }在Controller里调用这些Service方法通过API测试你会发现基本的增删改查已经全部实现而你一行SQL都没写。这就是TkMyBatis的基础魅力。4.2 条件查询的利器Example对象单表查询不可能总是selectAll或selectByPrimaryKey按条件查询才是常态。TkMyBatis提供了Example对象来构建动态查询条件它比手写where标签更类型安全也更面向对象。public ListUser getUsersByCondition(String usernameKeyword, String email) { // 创建Example对象传入实体类Class Example example new Example(User.class); // 创建Criteria条件一个Example可以包含多个Criteria用or连接 Example.Criteria criteria example.createCriteria(); // 1. 模糊查询查询username包含关键词的记录 if (StringUtils.hasText(usernameKeyword)) { // andLike 表示添加一个AND连接的LIKE条件 criteria.andLike(username, % usernameKeyword %); } // 2. 精确查询邮箱等于指定值 if (StringUtils.hasText(email)) { criteria.andEqualTo(email, email); } // 3. 排序按创建时间倒序 example.orderBy(createTime).desc(); // 4. 去重 // example.setDistinct(true); // 5. 选择特定列只查询id和username // example.selectProperties(id, username); // 执行查询 return userMapper.selectByExample(example); }Example对象非常强大它支持几乎所有的SQL条件操作andEqualTo/andNotEqualTo: 等于 / 不等于andGreaterThan/andLessThan: 大于 / 小于andIn/andNotIn: 在集合内 / 不在集合内andIsNull/andIsNotNull: 是NULL / 不是NULLandBetween: 在某个区间内andLike/andNotLike: 模糊匹配一个重要的实操心得Example条件中使用的属性名是实体类的字段名Java属性名而不是数据库的列名。例如如果你的实体类字段是userName驼峰数据库列是user_name这里应该写userName。TkMyBatis会结合map-underscore-to-camel-case配置自动转换。如果写错了运行时不会报错但条件不会生效这是一个很隐蔽的坑。4.3 复杂场景自定义SQL与通用Mapper共存TkMyBatis的通用Mapper不是万能的对于多表关联查询、复杂的聚合统计或者需要特定数据库函数优化的查询我们仍然需要手写SQL。幸运的是两者可以无缝共存。方式一使用Select等注解适合简单SQL在UserMapper接口中直接定义方法并用MyBatis注解编写SQL。public interface UserMapper extends MapperUser { // 使用注解定义自定义查询 Select(SELECT u.*, d.department_name FROM t_user u LEFT JOIN t_department d ON u.dept_id d.id WHERE u.id #{userId}) User selectUserWithDepartment(Param(userId) Long userId); // 使用XML映射文件推荐更清晰支持动态SQL ListUser selectByComplexCondition(Param(param) QueryParam param); }方式二使用XML映射文件推荐功能最全在resources目录下创建mapper文件夹并新建UserMapper.xml。在application.yml中配置mybatis.mapper-locations: classpath:mapper/*.xml。在XML中编写复杂的SQL。?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd !-- namespace必须对应Mapper接口的全限定名 -- mapper namespacecom.example.demo.mapper.UserMapper select idselectByComplexCondition resultTypeUser SELECT * FROM t_user where if testparam.startTime ! null AND create_time #{param.startTime} /if if testparam.endTime ! null AND create_time lt; #{param.endTime} /if if testparam.statusList ! null and param.statusList.size() 0 AND status IN foreach collectionparam.statusList itemstatus open( separator, close) #{status} /foreach /if !-- 可以在这里使用$进行动态排序但要注意SQL注入风险 -- if testparam.orderBy ! null ORDER BY ${param.orderBy} /if /where /select /mapper这样你的UserMapper就同时拥有了通用Mapper提供的几十个方法和你自己定义的复杂查询方法。MyBatis在启动时会正确地将它们整合在一起。5. 避坑指南与进阶技巧用了这么久TkMyBatis我踩过的坑和总结的经验可能比官方文档更实用。5.1 版本兼容性最大的“玄学”问题正如前面依赖部分提到的版本冲突是TkMyBatis集成失败的首要原因。除了MyBatis Starter的版本还需要注意Spring Boot版本Spring Boot 2.x 和 3.x 的底层依赖差异巨大。TkMyBatis 2.x 系列主要针对Spring Boot 2.x。对于Spring Boot 3.x你需要寻找社区维护的适配版本例如mapper-spring-boot-starter的3.x分支或者考虑其他类似框架如MyBatis-Plus。Java版本确保你的JDK版本与依赖兼容。数据库驱动不同版本的MySQL驱动可能对某些数据类型如新的时间类型的支持有差异。建议在一个新项目中先从一个被广泛验证的稳定版本组合开始例如本文给出的组合不要盲目追新。遇到ClassNotFoundException、NoSuchMethodError或配置不生效首先检查依赖树mvn dependency:tree是否有冲突。5.2 主键策略与回写插入后拿到ID这是非常常见的需求。在User实体中我们使用了GeneratedValue(strategy GenerationType.IDENTITY)这对应MySQL的AUTO_INCREMENT。当你调用insertSelective(user)后user对象的id字段仍然是null吗不一定这取决于配置。在application.yml中mapper.identity: MYSQL这个配置很关键。它告诉TkMyBatis对于像MySQL这样支持自增主键并能在同一条语句中返回生成ID的数据库使用JDBC3KeyGenerator。这样在插入完成后生成的ID会自动回填到实体对象的id属性中。User user new User(); user.setUsername(test); user.setEmail(testexample.com); userMapper.insertSelective(user); // 插入数据库 System.out.println(user.getId()); // 这里打印的id应该就是数据库生成的自增ID不再是null如果你的数据库不是MySQL或者主键不是自增比如UUID你需要使用不同的策略。例如对于UUID你可以在Java代码中生成并设置ID或者使用GeneratedValue(generator UUID)配合其他生成器。5.3 字段映射的“坑”驼峰、注解与空值驼峰与下划线再次强调确保mybatis.configuration.map-underscore-to-camel-case: true已开启。如果没开你的实体类字段userName将无法映射到数据库列user_name查询结果会是null。如果个别字段需要特殊处理就用Column注解覆盖。Transient注解如果你的实体类中有一些字段不需要持久化到数据库比如计算字段、临时属性一定要加上JPA的javax.persistence.Transient注解。否则TkMyBatis会认为它也是表字段在生成insert或update的SQL时尝试包含它导致SQL语法错误。选择性插入/更新insertSelective和updateByPrimaryKeySelective是默认的、也是最安全的选择。它们只处理非null字段。如果你用了insert而实体对象中某些字段为null数据库里对应的列就会被插入NULL这可能不是你想要的尤其是那些有默认值的列。updateByPrimaryKey同理它会用实体对象的所有字段去更新包括null字段这可能会意外地清空数据。5.4 分页查询与PageHelper无缝集成TkMyBatis本身不提供分页功能但它与另一款国人开发的优秀插件PageHelper堪称“黄金搭档”。集成后分页变得异常简单。添加依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.6/version !-- 注意选择兼容版本 -- /dependency使用分页在查询方法之前调用PageHelper.startPage即可。Service public class UserService { public PageInfoUser getUsersByPage(int pageNum, int pageSize) { // 关键紧跟在查询方法前调用传入页码和每页数量 PageHelper.startPage(pageNum, pageSize); // 接下来执行的第一个MyBatis查询方法会自动被分页 ListUser userList userMapper.selectAll(); // 这里会被改写为分页SQL // 用PageInfo包装结果它包含了非常丰富的分页信息 return new PageInfo(userList); } }PageInfo对象包含了当前页数据列表、总记录数、总页数、当前页码、每页大小等信息直接返回给前端非常方便。重要提示PageHelper.startPage(pageNum, pageSize)必须紧邻Mapper查询方法中间不能有其它数据库查询或可能抛出异常的操作否则分页拦截器可能失效。这是一个需要严格遵守的约定。5.5 性能与最佳实践避免N1查询这是ORM的通病。如果你用Example查询出一批User每个User关联一个Department在循环中通过user.getDeptId()再去查一次DepartmentMapper就会产生N1次查询。解决方法是使用自定义的SQL进行关联查询如4.3节所示一次查出所有数据。批量操作通用Mapper提供了insertList方法进行批量插入效率远高于循环调用insert。但要注意这个方法可能受限于数据库驱动对批量SQL的支持。对于超大批量数据考虑使用MyBatis的ExecutorType.BATCH模式。索引优化TkMyBatis生成的where条件顺序是固定的按Example中添加条件的顺序。确保你的查询条件能有效利用数据库索引。对于复杂的组合查询可能需要分析生成的SQL并在数据库层面建立合适的复合索引。监控生成的SQL在开发阶段务必开启MyBatis的SQL日志log-impl: StdOutImpl检查TkMyBatis动态生成的SQL是否符合你的预期特别是条件查询和更新操作。这是排查问题最快的方式。TkMyBatis用好了是神器能省下大量开发时间。它的学习曲线平缓核心在于理解“通用Mapper注解驱动”的思想并注意版本兼容性和一些细节配置。对于绝大多数单表操作场景它都能优雅地应对。当遇到复杂查询时又能随时退回到手写SQL的“安全区”这种灵活性是它经久不衰的原因。在实际项目中我通常会为每个实体创建继承通用Mapper的接口大部分业务直接使用少数复杂场景辅以XML文件这种混合模式在效率和灵活性上取得了很好的平衡。