ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TKMyBatis实战指南:告别重复CRUD,提升MyBatis开发效率

TKMyBatis实战指南:告别重复CRUD,提升MyBatis开发效率 1. 项目概述为什么我们需要TKMyBatis如果你用MyBatis做过几个项目尤其是那种表结构复杂、实体类众多的后台管理系统你大概率会和我有一样的感受写SQL是爽但写那些重复的增删改查CRUD是真的烦。一个用户表你得写insert、deleteById、updateById、selectById、selectAll... 每个实体类都来这么一套代码里充斥着大量结构相似、只是表名和字段名不同的Mapper XML文件。这不仅是体力活更可怕的是容易出错比如字段名写错、漏写某个查询条件调试起来费时费力。这时候TKMyBatis现在官方叫MyBatis-Plus但很多人还是习惯叫它TKMyBatis或通用Mapper就登场了。它不是一个新框架而是MyBatis的一个增强工具包。它的核心目标就一个让你告别单表的简单CRUD SQL编写。你只需要定义好实体类它就能自动帮你生成这些基础SQL你只需要在接口上继承一个特定的父接口或者在Service层调用几个通用方法就能完成绝大部分的数据操作。我第一次接触它是在一个快速迭代的创业项目中时间紧任务重根本没工夫在那些基础的增删改查上耗着。用了TKMyBatis之后开发效率肉眼可见地提升我可以把更多精力放在复杂的业务逻辑和联表查询上。这篇内容我就结合自己踩过的坑和积累的经验带你从零开始彻底搞明白TKMyBatis怎么用、怎么配、以及怎么避开那些常见的“坑”。无论你是刚接触MyBatis的新手还是被重复CRUD困扰的老鸟这篇都能帮你省下大量时间。2. 核心思路与架构设计解析2.1 TKMyBatis是如何工作的很多人刚开始会疑惑TKMyBatis是不是用了什么“黑魔法”其实它的原理很清晰。它本质上是一个MyBatis的插件Plugin通过拦截MyBatis执行SQL的过程在运行时动态生成SQL语句。核心流程可以这么理解约定大于配置TKMyBatis定义了一套规则。比如你的实体类名对应数据库表名User - user_table实体类的属性名对应表的字段名userName - user_name它支持多种命名策略转换。你的Mapper接口需要继承一个它提供的通用接口比如MapperT。方法名解析当你在Service中调用userMapper.selectByPrimaryKey(1)时MyBatis会去找对应的SQL。因为它发现你的UserMapper接口继承了MapperUser而selectByPrimaryKey是这个通用接口中定义好的方法。SQL动态生成TKMyBatis的插件会拦截这个请求。它根据你调用的方法名如selectByPrimaryKey、传入的参数主键值1以及实体类User的元数据通过注解获取表名、字段名、主键信息在内存中动态拼接出一条完整的SQL语句SELECT id, user_name, ... FROM user_table WHERE id 1。执行与返回生成的SQL会交给MyBatis原生的执行器去执行最终将结果映射回User对象。所以你完全不用写UserMapper.xml文件。这一切都是自动的、动态的。它的设计哲学是把程序员从重复劳动中解放出来去关注更重要的业务差异。2.2 与MyBatis原生方式、JPA的对比为了让你更清楚它的定位我们简单对比一下特性MyBatis原生TKMyBatis (通用Mapper)JPA (Hibernate)SQL控制力最强手写所有SQL灵活精准。强单表SQL自动生成复杂SQL仍需手写或使用其他方式。较弱由框架生成对复杂查询优化控制力弱。开发效率低所有SQL需手动编写。高单表CRUD接近零代码。高基本的CRUD甚至无需写接口。学习成本中需学习XML/注解配置和动态SQL。低在MyBatis基础上只需学习几个注解和通用接口。高需要掌握其复杂的实体关系映射ORM概念和JPQL。性能理论上最佳SQL可深度优化。接近原生生成的SQL是标准的且可被二级缓存等机制优化。需注意可能产生N1查询等问题需要经验优化。适用场景对SQL性能、灵活性要求极高的项目复杂报表、大数据量操作。快速开发的中后台系统、微服务需要高效率完成基础数据操作的场景。需求稳定、以对象操作为核心、团队熟悉JPA规范的项目。我的选择建议是如果你的项目以业务管理后台为主表结构清晰且对开发速度有要求TKMyBatis是绝佳选择。它保留了MyBatis对SQL的最终控制权复杂查询你依然可以手写XML又极大地提升了日常开发的幸福感。3. 环境搭建与基础配置详解光说不练假把式我们一步步把它集成到项目中。这里以Spring Boot项目为例这是目前最主流的搭配。3.1 依赖引入在你的pom.xml文件中添加依赖。请注意TKMyBatis的官方项目现在已迁移到MyBatis-Plus旗下但“通用Mapper”的功能模块是独立的。我们使用目前维护最活跃的mapper-spring-boot-starter。dependencies !-- Spring Boot Web Starter (根据你的项目需要) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis Spring Boot Starter -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version !-- 请使用最新稳定版 -- /dependency !-- 通用Mapper Starter (核心) -- dependency groupIdtk.mybatis/groupId artifactIdmapper-spring-boot-starter/artifactId version4.2.3/version !-- 请使用最新稳定版 -- /dependency !-- 数据库驱动例如MySQL -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok (可选用于简化实体类getter/setter) -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意版本号请务必到Maven中央仓库查询最新稳定版。不同版本间可能存在API差异特别是Spring Boot大版本升级时。3.2 实体类注解配置这是TKMyBatis的“灵魂”所在。通过注解告诉框架你的实体类和数据库表如何映射。import lombok.Data; import javax.persistence.*; import java.util.Date; Data // Lombok注解自动生成getter, setter, toString等 Table(name sys_user) // Table注解指定对应的数据库表名 public class User { /** * Id 注解标记该字段为主键 * GeneratedValue 注解指定主键生成策略。 * strategy GenerationType.IDENTITY 表示使用数据库自增MySQL AUTO_INCREMENT */ Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; /** * Column 注解映射字段名。 * 如果属性名和列名一致如都是user_name可省略。 * 这里属性名是userName列名是user_name通过name指定。 */ Column(name user_name) private String userName; private String password; // 属性名password默认映射到列 password private String email; /** * 对于日期类型可以使用Temporal注解但非必须。 * 框架能自动识别java.util.Date等类型。 */ private Date createTime; private Date updateTime; // 省略构造器、getter/setter由Lombok Data生成 }关键注解解析Table(name “表名”)必须。除非你的实体类名严格遵循驼峰转下划线的规则且表名与之完全匹配否则一定要用。这是最容易出错的地方之一。Id必须。标记主键字段。TKMyBatis的很多通用方法如selectByPrimaryKey,deleteByPrimaryKey都依赖它来识别主键。Column(name “列名”)非必须但建议显式声明。当字段名和列名转换规则不一致时使用。显式声明可以避免因命名策略变化导致的意外错误。GeneratedValue建议添加。用于告知框架主键的生成策略这样在执行insert操作后它能自动将生成的主键值回填到实体对象中。3.3 Mapper接口与Spring Boot配置1. 创建Mapper接口你的Mapper接口不再需要定义任何方法只需要继承TKMyBatis提供的通用接口。最常用的是MapperT它包含了绝大部分单表CRUD方法。import tk.mybatis.mapper.common.Mapper; // 注意这里不要使用Mapper注解这是MyBatis原生的 // 继承的泛型类型一定要写对这里是User public interface UserMapper extends MapperUser { // 你可以在这里添加你的自定义方法非通用方法 // 例如ListUser selectByCustomCondition(Param(name) String name); // 对于自定义方法你仍然需要编写对应的UserMapper.xml文件。 }2. Spring Boot主类或配置类扫描Mapper为了让Spring能管理这些继承了通用Mapper的接口我们需要配置扫描路径。import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import tk.mybatis.spring.annotation.MapperScan; SpringBootApplication // 关键注解使用tk.mybatis的MapperScan并指定你的Mapper接口所在的包 MapperScan(basePackages com.yourproject.mapper) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }3.application.yml配置在配置文件中除了常规的数据源配置还需要配置TKMyBatis的一些行为。spring: datasource: url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver # MyBatis配置 mybatis: # 如果仍有自定义的XML文件指定其位置 mapper-locations: classpath:mapper/*.xml # 配置实体类别名包方便在XML中直接使用类名 type-aliases-package: com.yourproject.entity configuration: # 开启驼峰命名自动转换默认false。这是MyBatis原生配置。 # 如果数据库字段是user_name实体属性是userName开启后会自动映射。 map-underscore-to-camel-case: true # 通用Mapper配置 mapper: # 主键自增回写方法执行顺序, 默认AFTER可选BEFORE identity: mysql # 设置 insert 和 update 中是否判断字符串类型! not-empty: false # 枚举处理简单类型默认false enum-as-simple-type: true实操心得map-underscore-to-camel-case: true这个配置非常有用它和TKMyBatis的注解是互补的。注解的优先级更高。对于没有用Column注解的字段框架会先尝试用注解的name找找不到则利用这个配置进行驼峰转换。建议保持开启。4. 核心功能与API实战演练配置好了我们来看看它到底能做什么。以下示例假设你已经注入了UserMapper。4.1 增删改查CRUD基础操作插入数据// 1. 插入一条数据null的属性也会保存使用数据库默认值 User user new User(); user.setUserName(张三); user.setEmail(zhangsanexample.com); user.setCreateTime(new Date()); int rows userMapper.insert(user); // 返回影响行数 // 插入后如果主键是自增且配置正确user.getId()会自动获取到生成的主键值 System.out.println(插入成功ID为 user.getId()); // 2. 选择性插入推荐只插入非null的属性 User user2 new User(); user2.setUserName(李四); int rows2 userMapper.insertSelective(user2); // email等为null的字段不会出现在SQL中注意事项insertSelective是我最常用的插入方法。在字段有默认值或者允许为空的场景下它更安全、生成的SQL也更简洁。查询数据// 1. 根据主键查询 User user userMapper.selectByPrimaryKey(1L); // 2. 根据实体类中非null的属性作为条件进行查询等值匹配 User condition new User(); condition.setUserName(张三); ListUser userList userMapper.select(condition); // 查询所有user_name张三的记录 // 3. 查询全部 ListUser allUsers userMapper.selectAll(); // 4. 根据条件查询一条记录 User singleUser userMapper.selectOne(condition); // 如果结果不止一条会抛异常更新数据// 1. 根据主键更新所有字段会将所有字段更新即使为null User userToUpdate new User(); userToUpdate.setId(1L); userToUpdate.setUserName(张老三); userToUpdate.setEmail(null); // 注意这里会把email字段更新为NULL int rows userMapper.updateByPrimaryKey(userToUpdate); // 2. 根据主键选择性更新强烈推荐 User userToUpdateSelective new User(); userToUpdateSelective.setId(1L); userToUpdateSelective.setUserName(张老三); // 不设置email则email字段不会被包含在update语句中保持原值 int rows2 userMapper.updateByPrimaryKeySelective(userToUpdateSelective);踩坑实录updateByPrimaryKey是“全量更新”的坑我踩过。不小心把一个User对象查出来改了名字后直接调用这个方法结果把密码、创建时间等字段全更新成了null导致数据丢失。务必、务必、务必使用updateByPrimaryKeySelective。删除数据// 1. 根据主键删除 int rows userMapper.deleteByPrimaryKey(1L); // 2. 根据实体类中非null的属性作为条件删除等值匹配 User condition new User(); condition.setUserName(测试用户); int rows2 userMapper.delete(condition); // 删除所有user_name测试用户的记录4.2 条件查询Example的使用上面的select(condition)是等值查询更复杂的条件怎么办TKMyBatis提供了强大的Example对象用于构造复杂的WHERE条件。import tk.mybatis.mapper.entity.Example; import java.util.Arrays; // 创建Example对象参数是实体类的Class Example example new Example(User.class); // 获取Criteria对象用于添加条件 Example.Criteria criteria example.createCriteria(); // 1. 等值条件 criteria.andEqualTo(userName, 张三); // 2. LIKE条件 criteria.andLike(email, %gmail.com%); // 3. BETWEEN条件 criteria.andBetween(createTime, startDate, endDate); // 4. IN条件 criteria.andIn(id, Arrays.asList(1L, 2L, 3L)); // 5. 大于小于 criteria.andGreaterThan(id, 10L); criteria.andLessThanOrEqualTo(id, 100L); // 设置排序 example.orderBy(createTime).desc(); // 按创建时间倒序 example.orderBy(id).asc(); // 可以链式调用多个排序 // 设置去重 example.setDistinct(true); // 执行查询 ListUser userList userMapper.selectByExample(example);Example的or()条件Example example new Example(User.class); Example.Criteria criteria1 example.createCriteria(); criteria1.andEqualTo(userName, 张三); // 创建一个or条件 Example.Criteria criteria2 example.or(); criteria2.andEqualTo(userName, 李四); // 生成的SQL: WHERE (user_name 张三) OR (user_name 李四) ListUser userList userMapper.selectByExample(example);实操技巧Example虽然强大但只适用于单表查询。对于复杂的多表关联查询它无能为力。这时你依然需要回归MyBatis的本源——手写XML/注解SQL。TKMyBatis和手写SQL是互补关系不是替代关系。4.3 分页查询TKMyBatis本身不提供物理分页实现但它能很好地与第三方分页插件如PageHelper集成这是国内Java生态里最经典的组合。1. 添加PageHelper依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.6/version !-- 使用最新版本 -- /dependency2. 在Service层使用import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; public PageInfoUser getUsersByPage(int pageNum, int pageSize, String keyword) { // 关键在查询方法前调用PageHelper.startPage之后的第一条Mapper查询会被分页 PageHelper.startPage(pageNum, pageSize); // 构建查询条件 Example example new Example(User.class); if (StringUtils.hasText(keyword)) { example.createCriteria().andLike(userName, % keyword %); } example.orderBy(createTime).desc(); // 执行查询此时SQL已被自动添加了LIMIT子句 ListUser userList userMapper.selectByExample(example); // 用PageInfo包装结果它包含了非常丰富的分页信息总记录数、总页数、当前页等 PageInfoUser pageInfo new PageInfo(userList); return pageInfo; }调用getUsersByPage(1, 10, “张”)PageHelper会自动将selectByExample生成的SQL改写成SELECT ... FROM sys_user WHERE user_name LIKE ‘%张%’ ORDER BY create_time DESC LIMIT 0, 10并额外执行一条COUNT(*)查询获取总数。注意事项PageHelper.startPage(pageNum, pageSize)必须紧挨着Mapper查询语句之前调用且中间不能有其它数据库查询操作否则分页会失效或错乱。这是一个非常常见的错误点。5. 高级特性与自定义扩展5.1 自定义SQL与通用Mapper共存TKMyBatis负责简单的复杂的交给你自己。你的Mapper接口完全可以同时拥有通用方法和自定义方法。1. 在Mapper接口中声明自定义方法public interface UserMapper extends MapperUser { /** * 自定义查询根据邮箱后缀查找用户 * param emailSuffix 邮箱后缀如 gmail.com * return 用户列表 */ ListUser selectByEmailSuffix(Param(suffix) String emailSuffix); /** * 自定义更新批量更新用户状态 * param ids 用户ID集合 * param status 目标状态 * return 影响行数 */ int updateStatusBatch(Param(ids) ListLong ids, Param(status) Integer status); }2. 创建对应的UserMapper.xml文件在resources/mapper/目录下创建UserMapper.xml。?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.yourproject.mapper.UserMapper !-- 对应接口的全限定名 -- !-- 自定义查询 -- select idselectByEmailSuffix resultTypeUser SELECT * FROM sys_user WHERE email LIKE CONCAT(%, #{suffix}) /select !-- 自定义批量更新 -- update idupdateStatusBatch UPDATE sys_user SET status #{status} WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /update !-- 你依然可以在这里写任何复杂的SQL与通用Mapper互不干扰 -- /mapper这样你在Service中就可以同时调用userMapper.selectByPrimaryKey(1L)和userMapper.selectByEmailSuffix(“gmail.com”)了。框架会先查找XML中是否有对应id的SQL没有的话再尝试用通用Mapper的逻辑生成。5.2 乐观锁与逻辑删除乐观锁通过版本号防止更新冲突。TKMyBatis通过Version注解支持。public class User { Id private Long id; private String userName; Version // 乐观锁版本字段 private Integer version; // ... other fields }当你使用updateByPrimaryKey或updateByPrimaryKeySelective时框架会自动在WHERE条件中加上AND version #{version}并在SET部分将version1。如果更新行数为0说明数据已被他人修改可据此判断并发冲突。逻辑删除数据不物理删除只是标记一个删除状态。TKMyBatis通过LogicDelete注解支持注意不同版本注解可能不同如Deleted。在实体类删除标志字段上添加注解。在全局配置中指定逻辑删除的值如1代表已删除0代表未删除。调用delete方法时实际执行的是UPDATE语句将该字段值改为删除状态。调用select方法时会自动在WHERE条件中加上AND deleted 0。个人建议对于重要的业务数据强烈建议使用逻辑删除。物理删除一旦发生数据恢复成本极高。逻辑删除配合数据归档策略是更稳妥的做法。6. 生产环境避坑指南与性能优化6.1 常见问题排查问题1报错Invalid bound statement (not found)原因这是最常见的问题。MyBatis找不到Mapper接口对应的方法的SQL语句。排查检查MapperScan注解的包路径是否正确是否扫描到了你的Mapper接口。检查你的Mapper接口是否继承了MapperT等正确的通用接口。如果你有自定义方法检查XML文件中的namespace是否和Mapper接口全限定名一致id是否和方法名一致。检查application.yml中mybatis.mapper-locations配置的路径是否包含了你的XML文件。终极方法开启MyBatis日志查看启动时加载了哪些Mapper和SQL语句。在配置文件中加logging.level.com.yourproject.mapperDEBUG。问题2插入后主键没有回填到实体对象原因主键生成策略配置错误或数据库不支持。解决确保实体类主键字段有Id和GeneratedValue(strategy GenerationType.IDENTITY)注解。确保数据库表主键是自增的对于MySQL。在全局配置中确认mapper.identity设置正确MySQL一般为MYSQL。问题3字段值为null时在update或insert时被意外更新/插入原因错误使用了全量更新方法updateByPrimaryKey或全量插入。解决养成习惯永远优先使用xxxSelective方法如insertSelective和updateByPrimaryKeySelective。问题4分页查询总数不对或性能慢原因PageHelper自动生成的COUNT(*)语句可能包含不必要的ORDER BY或复杂子查询或者表数据量太大。优化对于极其复杂的查询可以考虑手动写分页SQL或者使用PageHelper的count查询提示。确保分页查询的WHERE条件字段有合适的索引。考虑使用延迟关联等深度分页优化技巧这属于SQL优化范畴。6.2 性能优化建议索引是根本Example构造的条件最终会生成WHERE子句。务必为经常作为查询条件的字段建立索引特别是andEqualTo、andIn、andBetween用到的字段。避免select *通用Mapper的select系列方法默认查询所有字段。如果实体类字段很多但业务只需要其中几个会造成网络和内存浪费。解决方案使用Example的selectProperties方法指定查询字段。Example example new Example(User.class); example.selectProperties(id, userName, email); // 只查询这三个字段 ListUser list userMapper.selectByExample(example);或者为不同的业务场景定义不同的查询视图View Object在自定义SQL中只查询需要的字段。批量操作对于批量插入或更新通用Mapper提供的批量方法如insertList可能不是最优的。在数据量极大时如一次插入上万条考虑使用MyBatis的foreach标签配合ExecutorType.BATCH模式或者直接使用JDBC批处理性能会有数量级提升。监控生成的SQL在开发环境务必把MyBatis的SQL日志打开logging.level.你的Mapper包路径DEBUG检查生成的SQL是否符合预期是否存在N1查询问题在循环中查询数据库等。6.3 关于Service层的封装虽然Mapper层已经很方便但在实际项目中我通常会在Service层再做一层薄薄的封装原因有二统一事务管理Transactional注解通常加在Service方法上。将多个Mapper调用放在一个Service方法里可以保证事务一致性。业务逻辑归拢一些简单的业务判断如更新前校验数据是否存在放在Service层更合适。例如Service public class UserService { Autowired private UserMapper userMapper; Transactional public void updateUserEmail(Long userId, String newEmail) { User user userMapper.selectByPrimaryKey(userId); if (user null) { throw new RuntimeException(用户不存在); } // 可以在这里添加更多的业务逻辑如邮箱格式校验、重复性校验等 user.setEmail(newEmail); userMapper.updateByPrimaryKeySelective(user); // 使用Selective // ... 可以继续调用其他Mapper操作 } }TKMyBatis不是一个“银弹”它完美地解决了MyBatis在单表操作上的短板让你我这样的开发者能更专注于业务逻辑本身。从我自己的项目经验来看它极大地提升了开发效率和代码的整洁度。当然它也有其边界复杂的多表关联和动态SQL依然需要你亲手编写。把这套工具用好关键在于理解其原理遵循它的约定并在合适的场景使用它。希望这篇内容能帮你绕过我当年踩过的那些坑顺畅地把它应用到你的项目里。如果在使用中遇到具体问题多看看日志里生成的SQL那往往是解决问题的钥匙。
返回列表