1. 项目概述告别重复的SQL拥抱Example查询如果你和我一样常年泡在Java后端开发里尤其是和Spring Boot、MyBatis打交道那你一定对写那些千篇一律的增删改查SQL感到厌倦。每次新加一个字段对应的*Mapper.xml里就得小心翼翼地同步修改好几个方法的SQL生怕漏了一个条件导致查询结果出错。更头疼的是动态查询一堆if test标签嵌套代码又臭又长可读性极差。通用Mapper特别是它的tk.mybatis实现现在主流是mapper-spring-boot-starter就是来解决这个痛点的。它不是一个新框架而是基于MyBatis的一个插件其核心思想是“约定大于配置”。通过继承它提供的通用接口你的Mapper接口瞬间就拥有了数十个常用的单表操作方法无需编写任何SQL。而Example查询则是这个工具集中最闪耀的明珠它让你能用面向对象的方式流畅地构建复杂的动态查询条件彻底告别在XML里拼接字符串的噩梦。简单来说通用Mapper Example的组合能让你在80%的单表业务场景下将数据库操作代码量减少70%以上并且让代码更清晰、更安全避免SQL注入、更易于维护。接下来我就结合自己多年的实战经验带你从零开始深度拆解这套组合拳的威力与细节。2. 核心设计思路与架构解析2.1 为什么是通用Mapper而不是MyBatis-Plus或其他首先得明白tk.mybatis的通用Mapper和MyBatis-Plus的通用Service解决的是类似的问题但哲学和实现路径不同。我选择前者的一个重要原因是它的“侵入性”更低。它本质上是一个MyBatis插件通过动态生成SQL来实现通用CRUD。你的实体类就是普通的POJO你的Mapper接口只需要继承一个MapperT接口一切就绪。这种设计让我感觉更贴近“原生”的MyBatis学习曲线平缓在已有项目中引入的风险也更小。它的核心设计基于JPAJava Persistence API的注解风格使用Table、Column、Id等注解来建立实体与数据库表的映射关系。插件在运行时会读取这些注解信息结合你调用的方法名如selectByPrimaryKey或传入的Example对象动态拼接出正确的SQL语句。这种“运行时生成”的方式既保证了灵活性又避免了手动编写SQL的繁琐和错误。2.2 Example查询的设计哲学面向对象的条件组装Example类是通用Mapper的灵魂特性。它的设计灵感来源于Hibernate的Criteria查询但更加轻量和MyBatis化。其核心思想是将查询条件抽象为一个对象通过调用这个对象的方法来逐步添加条件最终这个对象本身就能完整描述一个WHERE子句。传统的MyBatis动态SQL是这样的select idselectByCondition parameterTypemap resultMapBaseResultMap SELECT * FROM user where if testname ! null and name ! AND name like concat(%, #{name}, %) /if if teststatus ! null AND status #{status} /if if teststartTime ! null AND create_time #{startTime} /if if testendTime ! null AND create_time #{endTime} /if /where ORDER BY create_time DESC /select每增加一个查询字段就需要修改XML和对应的参数Map协作和阅读成本都很高。而使用Example你在Java代码中就可以完成Example example new Example(User.class); Example.Criteria criteria example.createCriteria(); if (StringUtils.isNotBlank(name)) { criteria.andLike(name, % name %); } if (status ! null) { criteria.andEqualTo(status, status); } if (startTime ! null endTime ! null) { criteria.andBetween(createTime, startTime, endTime); } example.orderBy(createTime).desc(); ListUser userList userMapper.selectByExample(example);优势一目了然类型安全andEqualTo(“status”, status)如果status字段是Integer你传一个String编译期就会报错。XML中的#{status}可没这待遇。代码即文档查询逻辑清晰地展现在Java代码中无需在XML和Java文件间来回跳转。易于重构字段名“name”是字符串配合IDE的重构功能修改实体字段名时这里会同步提示错误避免漏改。动态性更强可以非常方便地在循环中、在逻辑判断中动态添加条件构建复杂的查询树通过or()方法。2.3 核心类与接口关系图概念虽然不能画图但我们可以理清关系MapperT接口所有通用方法的源头定义了selectByExample,updateByExampleSelective等方法。Example类条件查询的封装。内部包含orderByClause排序子句。distinct是否去重。一个或多个Criteria对象通过createCriteria()或or()创建。Criteria内部类真正存放条件的地方。每个Criteria对象包含一个ListCriterion每个Criterion就是一个具体的条件如name ‘张三’。实体类POJO使用Table,Id,Column等注解与数据库表关联。你的自定义Mapper接口继承MapperT便拥有了操作Example的能力。插件在运行时会解析Example对象和实体类注解生成最终的SQL。3. 环境集成与基础配置详解3.1 Spring Boot项目中的依赖引入现在最流行的方式是使用mapper-spring-boot-starter它帮你自动配置好了大部分内容。在你的pom.xml中添加dependency groupIdtk.mybatis/groupId artifactIdmapper-spring-boot-starter/artifactId version最新版本/version !-- 例如 4.2.1 -- /dependency这个starter会自动引入mapper-core核心包和MyBatis-Spring-Boot-Starter。注意它可能会和官方的mybatis-spring-boot-starter产生冲突所以通常项目中只保留这一个即可。3.2 实体类的注解配置规范实体类的注解是通用Mapper工作的基石。以下是一个标准的示例import javax.persistence.*; import java.util.Date; Table(name sys_user) // 指定表名若类名与表名符合驼峰转下划线规则可省略 public class User { Id // 标记为主键 GeneratedValue(strategy GenerationType.IDENTITY) // 主键自增策略 private Long id; Column(name user_name) // 指定列名 private String userName; private String email; // 默认按驼峰转下划线规则映射到 email private Integer status; Transient // 此字段非数据库表字段插件会忽略 private String temporaryToken; // getter, setter, toString 省略 }关键注解说明Table(name “实际表名”)最重要的注解之一。如果实体类名遵循驼峰转下划线且与表名一致如UserInfo-user_info可省略。Id必须标注在主键字段上。一个实体类必须有且仅有一个Id注解字段否则通用方法会出错。GeneratedValue指示主键生成策略。IDENTITY对应MySQL的AUTO_INCREMENTUUID可以配合Id注解在插入前生成主键。Column用于指定字段与数据库列的映射关系。最常用的是name属性。如果字段名已符合驼峰转下划线可省略。Transient标记该字段不是数据库表的列插件在生成SQL时会自动忽略它。常用于业务逻辑中的临时属性。实操心得建议即使字段名与列名规则一致也显式地写上Column(name “xxx”)。这有两个好处一是代码意图更清晰二是当数据库列名因历史原因非常不规则时如USER_NAME_可以直接指定避免后续踩坑。3.3 Mapper接口的创建与扫描你的Mapper接口需要继承通用的MapperT接口并指定泛型为对应的实体类。import tk.mybatis.mapper.common.Mapper; public interface UserMapper extends MapperUser { // 这里可以定义你自己的非通用方法 // 例如复杂的联表查询仍需在XML中编写SQL ListUser selectComplexUsersByRole(Param(roleId) Long roleId); }接下来需要在Spring Boot的启动类或配置类上添加MapperScan注解来扫描你的Mapper接口。这里有个巨坑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.MapperScan MapperScan(basePackages com.yourpackage.mapper) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }为什么必须用tk.mybatis的MapperScan因为这个注解器内部会进行特殊处理将你的接口注册为继承了通用Mapper接口的代理Bean。如果用了MyBatis官方的扫描器你的接口就只是一个普通的MyBatis Mapper那些通用的selectByExample等方法将无法被识别和实现。4. Example查询的深度实战与技巧掌握了基础我们来深入Example的每一个角落。Example的强大在于它用简单的API覆盖了绝大部分WHERE子句的场景。4.1 基础条件构造从等值查询到模糊匹配创建一个Example对象是第一步Example example new Example(User.class); Example.Criteria criteria example.createCriteria();Criteria提供了丰富的方法等值查询andEqualTo(“字段名”, 值)criteria.andEqualTo(status, 1); // WHERE status 1 criteria.andEqualTo(userName, 张三); // WHERE user_name ‘张三’这是最常用、最核心的方法。不等值查询andNotEqualTocriteria.andNotEqualTo(status, 0); // WHERE status 0范围查询andBetween(“字段名”, 值1, 值2)闭区间。criteria.andBetween(age, 18, 30); // WHERE age BETWEEN 18 AND 30andGreaterThan/andGreaterThanOrEqualTo/andLessThan/andLessThanOrEqualTo开闭区间。criteria.andGreaterThan(createTime, startDate); // WHERE create_time #{startDate}模糊查询andLike(“字段名”, 值)值中需自行包含%。criteria.andLike(userName, %张%); // WHERE user_name LIKE ‘%张%’andNotLike反向模糊匹配。空值查询criteria.andIsNull(email); // WHERE email IS NULL criteria.andIsNotNull(phone); // WHERE phone IS NOT NULLIN 查询andIn(“字段名”, Collection? 值集合)ListInteger statusList Arrays.asList(1, 2, 3); criteria.andIn(status, statusList); // WHERE status IN (1, 2, 3)注意传入的集合不能为空否则会抛出异常。实践中一定要先判断if (collection ! null !collection.isEmpty())。4.2 复杂条件组合与AND、或OR和子条件单Criteria对象内的所有条件默认是AND关系。如何实现OR同一Criteria内的OR使用or()方法链式调用。criteria.andEqualTo(status, 1) .orEqualTo(userName, admin); // 生成的SQL是WHERE (status 1) OR (user_name ‘admin’) // 注意这个orEqualTo是和前面andEqualTo同级的OR更清晰的写法是使用or(Criteria)criteria.andEqualTo(status, 1); criteria.or().andEqualTo(userName, admin); // 效果同上但结构更清晰多个Criteria实现复杂AND/OR嵌套这是Example的高级用法。Example可以包含多个Criteria它们之间是OR关系。Example example new Example(User.class); // 第一个Criteria状态为1 且 姓名包含‘张’ Example.Criteria criteria1 example.createCriteria(); criteria1.andEqualTo(status, 1); criteria1.andLike(userName, %张%); // 第二个Criteria状态为2 且 邮箱不为空 (与第一个Criteria是OR关系) Example.Criteria criteria2 example.createCriteria(); criteria2.andEqualTo(status, 2); criteria2.andIsNotNull(email); // 最终SQL: WHERE (status 1 AND user_name LIKE ‘%张%’) OR (status 2 AND email IS NOT NULL) ListUser list userMapper.selectByExample(example);通过创建多个Criteria你可以构建出任意复杂的(A AND B) OR (C AND D)这类查询逻辑。4.3 排序、去重与字段选择排序// 单字段排序 example.orderBy(createTime).desc(); // ORDER BY create_time DESC example.orderBy(age).asc(); // ORDER BY age ASC // 多字段排序 example.orderBy(status).asc().orderBy(id).desc(); // ORDER BY status ASC, id DESC去重example.setDistinct(true); // SELECT DISTINCT ...字段选择SelectColumns默认查询所有字段SELECT *。但有时我们只需要特定字段可以使用selectProperties。example.selectProperties(id, userName, email); // 生成的SQL: SELECT id, user_name, email FROM ...这是一个性能优化点特别是对于有BLOB/TEXT大字段的表避免不必要的数据传输和序列化开销。4.4 结合分页插件使用Example通常和分页插件如PageHelper一起使用实现高效的分页查询。这是国内项目非常标准的搭配。import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; // 第2页每页10条并按照创建时间倒序 PageHelper.startPage(2, 10); Example example new Example(User.class); example.createCriteria().andEqualTo(status, 1); example.orderBy(createTime).desc(); ListUser userList userMapper.selectByExample(example); PageInfoUser pageInfo new PageInfo(userList); // pageInfo中包含了总数、总页数、当前页数据等所有分页信息关键点PageHelper.startPage(pageNum, pageSize)必须紧贴在真正的查询方法selectByExample调用之前中间不能有其它数据库查询操作否则分页会失效或错乱。5. 增删改查的通用方法实战Example不仅用于查询selectByExample还能用于条件更新和删除这是它比单纯写SQL更优雅的地方。5.1 查询操作家族ListT selectByExample(Example example)最常用的条件查询。T selectOneByExample(Example example)查询单条记录。特别注意如果查询结果多于一条会抛出TooManyResultsException。确保你的条件能唯一确定一条记录时使用。int selectCountByExample(Example example)条件计数用于分页查询总数或统计性能优于先查列表再size()。ListT selectByExampleAndRowBounds(Example example, RowBounds rowBounds)结合MyBatis原生的RowBounds进行内存分页不推荐用于大数据量。5.2 更新操作选择性更新与全量更新这是Example在更新场景下的威力体现。updateByExample全量更新。User updateUser new User(); updateUser.setUserName(新名字); updateUser.setEmail(newemail.com); // 注意updateUser中所有属性都会被更新到数据库包括为null的字段 Example example new Example(User.class); example.createCriteria().andEqualTo(status, 0); userMapper.updateByExample(updateUser, example); // SQL: UPDATE user SET user_name新名字, emailnewemail.com, ...(所有字段) WHERE status 0风险会覆盖所有字段可能误将其他字段更新为null。慎用updateByExampleSelective选择性更新强烈推荐。User updateUser new User(); updateUser.setEmail(updatedemail.com); // 只设置要更新的字段 Example example new Example(User.class); example.createCriteria().andEqualTo(id, 1L); userMapper.updateByExampleSelective(updateUser, example); // SQL: UPDATE user SET emailupdatedemail.com WHERE id 1这个方法只会更新实体类中非空的属性。这是最安全、最常用的更新方式完美支持部分字段更新。5.3 删除操作int deleteByExample(Example example)根据条件删除。Example example new Example(User.class); example.createCriteria().andIsNull(email); // 删除邮箱为空的用户 int deletedCount userMapper.deleteByExample(example);警告执行删除前务必再三确认Example条件避免误删大量数据。生产环境建议先selectByExample查看一下将要删除的数据。5.4 插入操作虽然插入不直接使用Example但通用Mapper也提供了便捷方法int insert(T record)插入一条记录所有字段都会参与插入null值也会插入。int insertSelective(T record)选择性插入只插入非空字段。对于有默认值的数据库列这是首选。6. 高级特性与自定义扩展6.1 类型处理器TypeHandler的集成通用Mapper完全兼容MyBatis的TypeHandler。例如你有一个User实体其中有一个MapString, Object类型的attributes字段想以JSON字符串形式存到数据库的TEXT列。首先你需要一个自定义的TypeHandler例如使用Jackson进行JSON序列化/反序列化。在实体字段上通过ColumnType注解指定ColumnType(typeHandler JsonTypeHandler.class) // 你的自定义TypeHandler private MapString, Object attributes;这样在使用Example查询或插入时通用Mapper会自动调用这个TypeHandler进行类型转换。6.2 自定义通用方法如果通用Mapper自带的方法不能满足你你可以扩展它。你需要创建一个自定义的接口继承MapperT和你想要的通用Mapper提供的其他接口如SelectByIdsMapperT。import tk.mybatis.mapper.common.IdsMapper; import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper; public interface MyBaseMapperT extends MapperT, MySqlMapperT, IdsMapperT { // 继承MySqlMapper可以获得MySQL特有的批量插入方法 // 继承IdsMapper可以获得根据主键字符串逗号分隔查询和删除的方法 }让你的业务Mapper继承这个自定义的MyBaseMapper。在启动类MapperScan中指定markerInterface属性告诉扫描器你的这个标记接口。MapperScan(basePackages com.xx.mapper, markerInterface MyBaseMapper.class)6.3 乐观锁与逻辑删除的集成通用Mapper社区提供了一些扩展插件来处理常见需求。乐观锁通过Version注解标记版本号字段更新时会自动带上version oldVersion条件并在成功后自增。逻辑删除通过LogicDelete注解标记逻辑删除字段如is_deleted。当调用deleteByExample时实际执行的是UPDATE table SET is_deleted 1 WHERE ...。而所有的select*方法会自动附加AND is_deleted 0条件。 这些功能需要引入额外的依赖如mapper-extra和配置能极大简化业务代码。7. 避坑指南与性能优化7.1 常见问题排查表问题现象可能原因解决方案报错Invalid bound statement (not found)1. Mapper接口未被扫描到。2. 使用了MyBatis官方的MapperScan。3. 方法名与通用Mapper内置方法不匹配。1. 检查MapperScan包路径是否正确。2.确保使用tk.mybatis包下的MapperScan。3. 检查是否错误覆盖了继承的方法。查询结果字段为null1. 实体类字段名与数据库列名映射失败驼峰转换问题。2. 数据库列名有特殊字符或关键字。1. 使用Column(name”xxx”)显式指定。2. 在Column的name属性中使用反引号column_name。selectOne抛出TooManyResultsException查询条件返回了多条结果。确保查询条件能唯一确定一条记录或改用selectByExample返回List。更新/删除了全部数据Example条件构造错误例如criteria.andEqualTo(“status”, null)当值为null时此条件不会被添加到WHERE子句中。在Java代码中做好判空避免构建出无条件的Example。对于更新优先使用updateByExampleSelective。分页插件PageHelper失效PageHelper.startPage()调用位置不对与查询方法之间有其他查询。确保startPage紧贴在目标查询方法之前。性能问题查询慢1. 未使用selectProperties导致查询*。2. 复杂Example条件导致索引失效。3.in查询列表过长。1. 按需选择字段。2. 为常用查询条件建立数据库索引并利用Example的andEqualTo等走索引的方法。3. 对超长in列表进行分批查询。7.2 性能优化建议索引是王道Example生成的SQL本质还是SQL。确保andEqualTo、andBetween、orderBy等操作涉及的字段已建立合适的数据库索引。使用EXPLAIN命令分析生成的SQL。慎用select *务必养成使用example.selectProperties(“id”, “name”)的习惯特别是表中有大字段时。in查询长度限制通过andIn进行查询时如果传入的集合过大例如超过1000条某些数据库如Oracle可能会报错。需要在业务层进行分批处理。Example对象复用在循环中构建相似查询时注意Example和Criteria对象的创建开销。虽然不大但在极高并发下可考虑对象复用或更底层的优化。监控生成的SQL在开发环境开启MyBatis的SQL日志logging.level.tk.mybatis.mapperDEBUG查看最终生成的SQL语句是否符合预期这是排查问题最直接的方式。7.3 关于复杂查询的边界通用Mapper的Example再强大也主要服务于单表操作。对于复杂的多表关联查询、嵌套查询、公用表表达式CTE等场景它就显得力不从心了。我的实践原则是单表动态条件查询、更新、删除一律使用Example代码简洁又安全。简单的固定关联查询可以在自定义的Mapper方法中使用Select注解提供SQL。复杂的、动态的、涉及多表的查询老老实实回到XML文件中编写select语句利用MyBatis动态SQL标签if,choose,foreach来完成。通用Mapper和XML方式在项目中是可以和谐共存的。最后再分享一个我个人的小技巧对于团队项目可以建立一个“通用Mapper使用规范”文档明确规定什么场景必须用Example什么场景用XML以及实体类注解、Example构建的代码格式。这能极大提升团队代码的一致性和可维护性让这个优秀的工具发挥出最大的价值。