通用Mapper与Example查询:Java后端高效数据库操作实战指南
2026/8/3 8:17:13 网站建设 项目流程

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接口只需要继承一个Mapper<T>接口,一切就绪。这种设计让我感觉更贴近“原生”的MyBatis,学习曲线平缓,在已有项目中引入的风险也更小。

它的核心设计基于JPA(Java Persistence API)的注解风格,使用@Table@Column@Id等注解来建立实体与数据库表的映射关系。插件在运行时,会读取这些注解信息,结合你调用的方法名(如selectByPrimaryKey)或传入的Example对象,动态拼接出正确的SQL语句。这种“运行时生成”的方式,既保证了灵活性,又避免了手动编写SQL的繁琐和错误。

2.2 Example查询的设计哲学:面向对象的条件组装

Example类是通用Mapper的灵魂特性。它的设计灵感来源于Hibernate的Criteria查询,但更加轻量和MyBatis化。其核心思想是:将查询条件抽象为一个对象,通过调用这个对象的方法来逐步添加条件,最终这个对象本身就能完整描述一个WHERE子句

传统的MyBatis动态SQL是这样的:

<select id="selectByCondition" parameterType="map" resultMap="BaseResultMap"> SELECT * FROM user <where> <if test="name != null and name != ''"> AND name like concat('%', #{name}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="startTime != null"> AND create_time >= #{startTime} </if> <if test="endTime != 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(); List<User> userList = userMapper.selectByExample(example);

优势一目了然:

  1. 类型安全andEqualTo(“status”, status),如果status字段是Integer,你传一个String,编译期就会报错。XML中的#{status}可没这待遇。
  2. 代码即文档:查询逻辑清晰地展现在Java代码中,无需在XML和Java文件间来回跳转。
  3. 易于重构:字段名“name”是字符串,配合IDE的重构功能,修改实体字段名时,这里会同步提示错误,避免漏改。
  4. 动态性更强:可以非常方便地在循环中、在逻辑判断中动态添加条件,构建复杂的查询树(通过or()方法)。

2.3 核心类与接口关系图(概念)

虽然不能画图,但我们可以理清关系:

  • Mapper<T>接口:所有通用方法的源头,定义了selectByExample,updateByExampleSelective等方法。
  • Example:条件查询的封装。内部包含:
    • orderByClause:排序子句。
    • distinct:是否去重。
    • 一个或多个Criteria对象(通过createCriteria()or()创建)。
  • Criteria内部类:真正存放条件的地方。每个Criteria对象包含一个List<Criterion>,每个Criterion就是一个具体的条件(如name = ‘张三’)。
  • 实体类(POJO):使用@Table,@Id,@Column等注解与数据库表关联。

你的自定义Mapper接口继承Mapper<T>,便拥有了操作Example的能力。插件在运行时,会解析Example对象和实体类注解,生成最终的SQL。

3. 环境集成与基础配置详解

3.1 Spring Boot项目中的依赖引入

现在最流行的方式是使用mapper-spring-boot-starter,它帮你自动配置好了大部分内容。在你的pom.xml中添加:

<dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-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接口需要继承通用的Mapper<T>接口,并指定泛型为对应的实体类。

import tk.mybatis.mapper.common.Mapper; public interface UserMapper extends Mapper<User> { // 这里可以定义你自己的非通用方法 // 例如复杂的联表查询,仍需在XML中编写SQL List<User> 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提供了丰富的方法:

  1. 等值查询andEqualTo(“字段名”, 值)

    criteria.andEqualTo("status", 1); // WHERE status = 1 criteria.andEqualTo("userName", "张三"); // WHERE user_name = ‘张三’

    这是最常用、最核心的方法。

  2. 不等值查询andNotEqualTo

    criteria.andNotEqualTo("status", 0); // WHERE status <> 0
  3. 范围查询

    • andBetween(“字段名”, 值1, 值2):闭区间。
      criteria.andBetween("age", 18, 30); // WHERE age BETWEEN 18 AND 30
    • andGreaterThan/andGreaterThanOrEqualTo/andLessThan/andLessThanOrEqualTo:开闭区间。
      criteria.andGreaterThan("createTime", startDate); // WHERE create_time > #{startDate}
  4. 模糊查询

    • andLike(“字段名”, 值):值中需自行包含%
      criteria.andLike("userName", "%张%"); // WHERE user_name LIKE ‘%张%’
    • andNotLike:反向模糊匹配。
  5. 空值查询

    criteria.andIsNull("email"); // WHERE email IS NULL criteria.andIsNotNull("phone"); // WHERE phone IS NOT NULL
  6. IN 查询andIn(“字段名”, Collection<?> 值集合)

    List<Integer> 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

  1. 同一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"); // 效果同上,但结构更清晰
  2. 多个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) List<User> list = userMapper.selectByExample(example);

    通过创建多个Criteria,你可以构建出任意复杂的(A AND B) OR (C AND D)这类查询逻辑。

4.3 排序、去重与字段选择

  1. 排序

    // 单字段排序 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
  2. 去重

    example.setDistinct(true); // SELECT DISTINCT ...
  3. 字段选择(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(); List<User> userList = userMapper.selectByExample(example); PageInfo<User> pageInfo = new PageInfo<>(userList); // pageInfo中包含了总数、总页数、当前页数据等所有分页信息

关键点PageHelper.startPage(pageNum, pageSize)必须紧贴在真正的查询方法(selectByExample)调用之前,中间不能有其它数据库查询操作,否则分页会失效或错乱。

5. 增删改查的通用方法实战

Example不仅用于查询(selectByExample),还能用于条件更新和删除,这是它比单纯写SQL更优雅的地方。

5.1 查询操作家族

  • List<T> selectByExample(Example example):最常用的条件查询。
  • T selectOneByExample(Example example):查询单条记录。特别注意:如果查询结果多于一条,会抛出TooManyResultsException。确保你的条件能唯一确定一条记录时使用。
  • int selectCountByExample(Example example):条件计数,用于分页查询总数或统计,性能优于先查列表再size()
  • List<T> selectByExampleAndRowBounds(Example example, RowBounds rowBounds):结合MyBatis原生的RowBounds进行内存分页(不推荐用于大数据量)。

5.2 更新操作:选择性更新与全量更新

这是Example在更新场景下的威力体现。

  1. updateByExample:全量更新。

    User updateUser = new User(); updateUser.setUserName("新名字"); updateUser.setEmail("new@email.com"); // 注意:updateUser中所有属性都会被更新到数据库,包括为null的字段 Example example = new Example(User.class); example.createCriteria().andEqualTo("status", 0); userMapper.updateByExample(updateUser, example); // SQL: UPDATE user SET user_name='新名字', email='new@email.com', ...(所有字段) WHERE status = 0

    风险:会覆盖所有字段,可能误将其他字段更新为null慎用!

  2. updateByExampleSelective:选择性更新(强烈推荐)。

    User updateUser = new User(); updateUser.setEmail("updated@email.com"); // 只设置要更新的字段 Example example = new Example(User.class); example.createCriteria().andEqualTo("id", 1L); userMapper.updateByExampleSelective(updateUser, example); // SQL: UPDATE user SET email='updated@email.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实体,其中有一个Map<String, Object>类型的attributes字段,想以JSON字符串形式存到数据库的TEXT列。

  1. 首先,你需要一个自定义的TypeHandler(例如,使用Jackson进行JSON序列化/反序列化)。
  2. 在实体字段上通过@ColumnType注解指定:
    @ColumnType(typeHandler = JsonTypeHandler.class) // 你的自定义TypeHandler private Map<String, Object> attributes;
    这样,在使用Example查询或插入时,通用Mapper会自动调用这个TypeHandler进行类型转换。

6.2 自定义通用方法

如果通用Mapper自带的方法不能满足你,你可以扩展它。你需要:

  1. 创建一个自定义的接口,继承Mapper<T>和你想要的通用Mapper提供的其他接口(如SelectByIdsMapper<T>)。
    import tk.mybatis.mapper.common.IdsMapper; import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper; public interface MyBaseMapper<T> extends Mapper<T>, MySqlMapper<T>, IdsMapper<T> { // 继承MySqlMapper可以获得MySQL特有的批量插入方法 // 继承IdsMapper可以获得根据主键字符串(逗号分隔)查询和删除的方法 }
  2. 让你的业务Mapper继承这个自定义的MyBaseMapper
  3. 在启动类@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. 在@Columnname属性中使用反引号`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. 为常用查询条件建立数据库索引,并利用ExampleandEqualTo等走索引的方法。
3. 对超长in列表进行分批查询。

7.2 性能优化建议

  1. 索引是王道Example生成的SQL本质还是SQL。确保andEqualToandBetweenorderBy等操作涉及的字段已建立合适的数据库索引。使用EXPLAIN命令分析生成的SQL。
  2. 慎用select *:务必养成使用example.selectProperties(“id”, “name”)的习惯,特别是表中有大字段时。
  3. in查询长度限制:通过andIn进行查询时,如果传入的集合过大(例如超过1000条),某些数据库(如Oracle)可能会报错。需要在业务层进行分批处理。
  4. Example对象复用:在循环中构建相似查询时,注意ExampleCriteria对象的创建开销。虽然不大,但在极高并发下可考虑对象复用或更底层的优化。
  5. 监控生成的SQL:在开发环境,开启MyBatis的SQL日志(logging.level.tk.mybatis.mapper=DEBUG),查看最终生成的SQL语句是否符合预期,这是排查问题最直接的方式。

7.3 关于复杂查询的边界

通用Mapper的Example再强大,也主要服务于单表操作。对于复杂的多表关联查询、嵌套查询、公用表表达式(CTE)等场景,它就显得力不从心了。

我的实践原则是:

  • 单表动态条件查询、更新、删除:一律使用Example,代码简洁又安全。
  • 简单的固定关联查询:可以在自定义的Mapper方法中,使用@Select注解提供SQL。
  • 复杂的、动态的、涉及多表的查询:老老实实回到XML文件中编写<select>语句,利用MyBatis动态SQL标签(<if>,<choose>,<foreach>)来完成。通用Mapper和XML方式在项目中是可以和谐共存的。

最后,再分享一个我个人的小技巧:对于团队项目,可以建立一个“通用Mapper使用规范”文档,明确规定什么场景必须用Example,什么场景用XML,以及实体类注解、Example构建的代码格式。这能极大提升团队代码的一致性和可维护性,让这个优秀的工具发挥出最大的价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询