第三章: [进阶] 从实体映射到复杂关联查询

第三章. [进阶] 实体映射与 Wrapper 条件构造器

摘要:本章我们将攻克 Mybatis-Plus 最核心的难点——如何用 Java 代码写 SQL。我们将从最基础的实体映射注解开始,逐步掌握 Wrapper 条件构造器的每一个常用 API,建立起“Java 方法”与“SQL 语法”的一一映射关系,最后通过 XML 解决多表关联查询的难题。

本章学习路径

  1. 映射注解:掌握 @TableName@TableId@TableField,解决数据库与 Java 命名不一致的问题。
  2. Wrapper 语法基础:像背单词一样掌握 eq(=)、gt(>)、between 等核心 API 的 SQL 映射规则。
  3. Wrapper 进阶语法:掌握模糊查询 like、逻辑嵌套 or/and 以及字段投影 select 的写法。
  4. 复杂关联:回归 MyBatis XML,解决一对一、一对多等多表关联查询。

3.1. 实体与表映射注解

在开始复杂的查询之前,我们必须先解决一个基础问题:Java 实体类是如何找到数据库中对应的表的?

默认情况下,MP 采用“驼峰转下划线”的规则(如 UserDO -> user_do)。但实际项目中,命名往往不规范,这时就需要注解来帮忙。

3.1.1. 表名与主键映射

场景:数据库表名为 tb_user,但我们的类名是 UserDO;且主键是自增 ID。

文件路径src/main/java/com/example/mpstudy/domain/UserDO.java

1
2
3
4
5
6
7
8
@TableName("tb_user") // 1. 显式指定表名
public class UserDO {

@TableId(type = IdType.AUTO) // 2. 显式指定主键策略为数据库自增
private Long id;

// ... 其他属性
}

3.1.2. 字段映射神器:@TableField

@TableField 是最常用的注解,用于解决属性名与列名不一致,或属性非数据库字段等问题。

1
2
@TableField("username")
private String name;

常见用法速查

场景注解写法解释
改名@TableField("user_email")Java 叫 email,数据库叫 user_email,需手动映射。
忽略@TableField(exist = false)tempCode 是临时字段,数据库没这列,必须 忽略,否则报错。
隐藏@TableField(select = false)password 字段存在,但在 select 查询时默认不查出来,保护隐私。

3.1.3. 主键策略:@TableIdIdType

主键是表的灵魂。MP 提供了多种主键生成策略,通过 IdType 枚举控制。

策略类型枚举值说明适用场景
自增AUTO依赖数据库的 auto_increment 特性。单体应用,开发测试环境。
雪花算法ASSIGN_IDMP 内置算法,生成 19 位唯一 Long 值。分布式系统默认推荐,无中心高性能。
手动输入INPUT插入前必须手动调用 setId()业务主键(如身份证号、学号)。
UUIDASSIGN_UUID生成 32 位字符串。对长度不敏感且需要字符串主键的场景。

代码演示

1
2
3
// 本教程使用 MySQL 自增主键,方便演示
@TableId(type = IdType.AUTO)
private Long id;

3.2. Wrapper:Java 里的 SQL 翻译官

在原生 MyBatis 中,我们需要在 XML 里写 WHERE age > 18。在 MP 中,我们使用 Wrapper(条件构造器) 来生成这些 SQL。

这就好比学英语,我们需要先掌握“单词量”。

3.2.1. 核心选择:LambdaQueryWrapper

MP 提供了 QueryWrapper(普通版)和 LambdaQueryWrapper(Lambda 版)。

场景:你需要查询 name = "Jack" 的用户。

1
2
3
4
5
QueryWrapper<UserDO> wrapper = new QueryWrapper<>();
// 隐患:列名 "name" 是硬编码的字符串。
// 如果数据库字段改名为 "username",或者你不小心手滑写成了 "naem"
// 编译器不会报错!直到上线运行时抛出 SQLSyntaxErrorException。
wrapper.eq("name", "Jack");

场景:同样的查询,使用 Lambda 语法。

1
2
3
4
5
LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();
// 优势:使用方法引用 UserDO::getName。
// 1. 如果你手滑写错方法名,IDE 直接爆红,编译不通过。
// 2. 如果你重构代码将 getName 改为 getUsername,IDE 会自动更新这里。
wrapper.eq(UserDO::getName, "Jack");

底层原理:MP 利用了 Java 8 的 SerializedLambda 特性,能在运行时解析 UserDO::getName 这个方法引用,反向推导出它对应的属性名 name,再结合 @TableField 注解找到数据库列名。这就是“类型安全”的魔法。

3.2.2. 基础比较语法 (eq, ne, gt, lt, between)

这是我们最常用的 SQL 语法翻译。请对照下表进行记忆:

Wrapper 方法SQL 含义英文全称
eq=Equal
ne<>Not Equal
gt>Greater Than
ge>=Greater than or Equal
lt<Less Than
le<=Less than or Equal
betweenBETWEEN v1 AND v2Between

实战演示

我们通过一个测试用例,一次性演练这些基础语法。

文件路径src/test/java/com/example/mpstudy/WrapperTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void testBasicSyntax() {
// 1. 创建 Wrapper
LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();

// 2. 翻译 SQL: WHERE name = 'Tom' AND age > 18
wrapper.eq(UserDO::getName, "Tom") // 等于
.gt(UserDO::getAge, 18); // 大于

// 3. 执行查询
userMapper.selectList(wrapper);

// --- 分隔线:演示 between ---

wrapper.clear(); // 清除之前的条件
// 4. 翻译 SQL: WHERE age BETWEEN 20 AND 30
wrapper.between(UserDO::getAge, 20, 30);

userMapper.selectList(wrapper);
}

3.2.3. 模糊查询 (like)

场景:用户搜索框,输入 “J” 搜索名字包含 J 的人。

Wrapper 方法SQL 含义说明
likeLIKE '%值%'包含,全模糊(索引失效)
likeLeftLIKE '%值'以值结尾
likeRightLIKE '值%'以值开头(走索引,推荐)

代码演示

1
2
3
4
5
6
7
8
9
@Test
void testLike() {
LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();

// 翻译 SQL: WHERE name LIKE '%J%'
wrapper.like(UserDO::getName, "J");

userMapper.selectList(wrapper).forEach(System.out::println);
}

3.2.4. 空值与集合查询 (isNull, in)

场景:查询还没有填写邮箱的用户(email 为 null),或者查询 ID 是 1、3、5 的特定用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void testNullAndIn() {
LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();

// 1. 空值查询: WHERE email IS NULL
wrapper.isNull(UserDO::getEmail);
userMapper.selectList(wrapper);

wrapper.clear();

// 2. 集合查询: WHERE id IN (1, 3, 5)
// Arrays.asList 是 Java 构建 List 的快捷方式
wrapper.in(UserDO::getId, Arrays.asList(1L, 3L, 5L));
userMapper.selectList(wrapper);
}

3.3. Wrapper 进阶语法

掌握了基础单词后,我们需要学习如何构造复杂的句子(逻辑嵌套)以及如何只查询部分内容(字段投影)。

3.3.1. 逻辑运算与嵌套 (or, and, nested)

默认情况下,Wrapper 链式调用拼接的都是 AND

  • wrapper.eq("A").eq("B") -> WHERE A AND B

但如果我们需要 OR,或者 (A AND B) OR C 这种括号逻辑,就需要小心了。

实战演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
void testLogic() {
LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();

// 场景 1: 普通 OR
// SQL: WHERE age > 25 OR name = 'Tom'
wrapper.gt(UserDO::getAge, 25)
.or() // 这里的 .or() 会把前后的条件用 OR 连接
.eq(UserDO::getName, "Tom");

userMapper.selectList(wrapper);

// 场景 2: 括号嵌套 (非常重要!!!)
// SQL: WHERE (age < 20 AND email IS NOT NULL) OR name = 'Jone'
wrapper.clear();

// nested 方法通过 Lambda 表达式创建一个独立的“括号作用域”
wrapper.nested(i -> i.lt(UserDO::getAge, 20).isNotNull(UserDO::getEmail))
.or()
.eq(UserDO::getName, "Jone");

userMapper.selectList(wrapper);
}

为什么必须用 nested?
如果不加 nested 直接写 lt().isNotNull().or().eq(),根据 SQL 优先级 AND > OR,逻辑会变成 age<20 AND (email不空 OR name是Jone),这通常不是我们想要的业务逻辑。

3.3.2. 字段投影 (select)

场景:用户表有 50 个字段,但前端下拉框只需要 idname。如果查所有字段,性能太差。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testSelect() {
LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();

// SQL: SELECT id, name FROM tb_user ...
// 只查询 id 和 name 两个列
wrapper.select(UserDO::getId, UserDO::getName);

List<UserDO> users = userMapper.selectList(wrapper);
// 注意:users 里的 email, age 等未查询字段均为 null
users.forEach(System.out::println);
}

3.4. 复杂关联查询 (XML 实现)

讲完了单表 Wrapper,我们来到最痛的地方:多表关联

Wrapper 极其擅长单表,但对于“一对多”(一个部门有多个用户)或“一对一”(一个用户属于一个部门)的关联查询,回归 XML 才是最清晰的方案

3.4.1. 场景准备

我们需要模拟一个“部门 (Department)”包含多个“用户 (User)”的场景。

1. 准备数据库

1
2
3
4
5
6
7
8
9
10
11
-- 部门表
CREATE TABLE `tb_department` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(30),
PRIMARY KEY (`id`)
);
-- 给用户表加外键
ALTER TABLE `tb_user` ADD COLUMN `department_id` bigint;
-- 插入测试数据
INSERT INTO `tb_department` VALUES (1, '研发部');
UPDATE `tb_user` SET department_id = 1 WHERE id <= 3;

2. 准备 VO (View Object)
我们需要一个对象来承载“部门+用户列表”的数据。

1
2
3
4
5
@Data
public class DepartmentVO extends DepartmentDO {
// 继承了 id, name,额外增加一个列表属性
private List<UserDO> users;
}

3.4.2. XML <resultMap> 实战

我们要实现的效果是:输入部门 ID,查出部门信息,同时自动查出该部门下的所有员工,填充到 users 列表中。

第一步:定义 Mapper 接口

文件路径src/main/java/com/example/mpstudy/mapper/DepartmentMapper.java

1
2
3
4
public interface DepartmentMapper extends BaseMapper<DepartmentDO> {
// 自定义方法:查询部门及其下的用户
DepartmentVO selectDeptWithUsers(@Param("id") Long id);
}

第二步:编写 XML 映射 (核心)

文件路径src/main/resources/mapper/DepartmentMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<mapper namespace="com.example.mpstudy.mapper.DepartmentMapper">

<!-- resultMap: 告诉 MyBatis 如何把 SQL 结果组装成复杂的 Java 对象 -->
<resultMap id="DeptMap" type="com.example.mpstudy.domain.vo.DepartmentVO">
<!-- 1. 映射部门自己的字段 -->
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>

<!-- 2. 映射一对多集合 (collection) -->
<!-- property: DepartmentVO 里的 List<UserDO> users 属性 -->
<!-- ofType: List 里装的元素类型 -->
<collection property="users" ofType="com.example.mpstudy.domain.UserDO">
<!-- 映射用户表的字段 -->
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
</collection>
</resultMap>

<select id="selectDeptWithUsers" resultMap="DeptMap">
SELECT
d.id as dept_id,
d.name as dept_name,
u.id as user_id,
u.name as user_name,
u.email as user_email
FROM tb_department d
LEFT JOIN tb_user u ON d.id = u.department_id
WHERE d.id = #{id}
</select>
</mapper>

第三步:测试

1
2
3
4
5
6
7
@Test
void testJoin() {
DepartmentVO vo = departmentMapper.selectDeptWithUsers(1L);
System.out.println("部门名: " + vo.getName());
System.out.println("员工数: " + vo.getUsers().size());
vo.getUsers().forEach(System.out::println);
}

3.5. 本章总结与语法速查

3.5.1. Wrapper 语法速查表

场景Wrapper 方法对应 SQL
基础eq / ne= / <>
范围gt / ge> / >=
区间betweenBETWEEN a AND b
模糊like / likeRightLIKE '%a%' / LIKE 'a%'
空值isNull / isNotNullIS NULL / IS NOT NULL
集合in / notInIN (a,b,c) / NOT IN ...
逻辑or / nestedOR / (...)

3.5.2. 核心避坑指南

  1. Wrapper 的 .or() 陷阱

    • 现象wrapper.eq("A").or().eq("B").eq("C")
    • 后果:SQL 变成 A OR B AND C。如果你想表达 A OR (B AND C) 这是对的;但如果你想表达 (A OR B) AND C,这绝对是错的。
    • 对策:遇到复杂逻辑,务必使用 nested 显式加括号,不要依赖默认优先级。
  2. like 的索引失效

    • 现象wrapper.like(...) 使用的是 %value%
    • 后果:全模糊查询不走索引,全表扫描,百万数据直接卡死。
    • 对策:优先使用 likeRight (value%),这能命中索引。
  3. 多表查询别用 Wrapper

    • 建议:虽然网上有 Wrapper 做 Join 的骚操作,但代码可读性极差。请老老实实写 XML,这是对团队协作负责。

下章预告:学会了怎么写查询条件,但如果查出来的数据有 100 万条怎么办?我们不可能一次性全部返回给前端。下一章,我们将解锁 Mybatis-Plus 的插件体系,学习如何实现 物理分页,以及如何利用 乐观锁插件 解决并发更新问题。