JavaEE进阶——MyBatis-Plus新手完全攻略 - 实践
目录
2.1 实体类 (Entity) 与 Lombok 的最佳实践
2.2 注解映射 (Mapping Annotations) —— MP 是如何“看懂”数据库的
3.1 QueryWrapper (基础版 —— 并不推荐在生产环境大量使用)
3.2 LambdaQueryWrapper (进阶推荐版 —— 必须掌握)
4.1 逻辑删除 (Logical Delete) —— 数据安全的安全带
4.2 自动填充 (Auto Fill) —— 告别重复劳动
方法一:使用 IDEA 的“断点调试” (Debug 模式) —— 上帝视角 ️♂️

你好!作为新手小白,面对 MyBatis-Plus (MP) 这样强大的框架,感到困惑是很正常的。MyBatis-Plus 其实就像是给 MyBatis 穿上了一套“钢铁侠战衣”,让原本繁琐的数据库操作变得极其简单。
为了让你能够彻底理解,我为你准备了一份超详细的沉浸式学习指南。这不仅仅是代码,更是一份涵盖了原理、扩展知识和最佳实践的教程。
我为你生成了 5 个文件:
MyBatisPlus_Guide.md: 核心教程文档。包含对所有知识点的深度解析、扩展说明、背景知识(如 ORM、Lambda 表达式原理)以及“为什么这样写”的解释。请务必配合代码阅读。UserInfo.java: 实体类代码。包含极其详细的注解和字段说明。UserInfoMapper.java: Mapper 接口代码。包含自定义 SQL 的定义。MybatisPlusTests.java: 测试用例代码。这是核心,包含了 PDF 中所有的增删改查、复杂查询、Lambda 构造器等操作,每一行都有详细注释。schema.sql: 数据库脚本。用于初始化环境。
请先阅读 Guide 文档,然后对照 Tests 代码 进行实践。
MyBatis-Plus 超详细学习指南 (新手必读).md
MyBatis-Plus 新手完全攻略:从入门到精通
1. 什么是 MyBatis-Plus?(核心概念)
1.1 背景知识:从 JDBC 到 ORM 的进化之路
在学习 MyBatis-Plus (简称 MP) 之前,你需要先理解为什么我们需要它。这一切都要从 Java 连接数据库的历史说起:
JDBC (Java Database Connectivity) —— 痛苦的起点:
这是 Java 连接数据库的最底层标准。虽然功能强大,但写起来非常折磨人。
繁琐的样板代码:为了执行一个简单的 SQL,你需要手动加载驱动、通过
DriverManager获取连接、创建Statement、处理ResultSet结果集,最后还要在finally块中小心翼翼地关闭资源以防内存泄漏。硬编码的噩梦:SQL 语句是写死在 Java 代码里的字符串。一旦数据库字段改名,你需要在 Java 代码里满世界搜索替换,极易出错。
ORM (Object-Relational Mapping) —— 桥梁:
为了解决上述痛苦,ORM(对象关系映射)应运而生。它的核心思想是建立表 (Table) 和 类 (Class) 之间的映射关系,让程序员像操作 Java 对象一样操作数据库。
数据库表的一行数据 (
Row) <---> Java 类的一个对象 (Object)数据库表的列 (
Column) <---> Java 类的属性 (Field)MyBatis 是一个优秀的“半自动化” ORM 框架。它成功地把 SQL 从 Java 代码中剥离到了 XML 配置文件中,解决了硬编码问题。但它仍然要求你为每一个查询方法(甚至是最简单的
selectById)编写 SQL 语句,这在表字段很多时依然是一项繁重的工作。1.2 MP 的出现:懒人的福音与效率的飞跃
MyBatis-Plus 的口号是 "只做增强不做改变",这句话有两个层面的含义:
不改变(兼容性):它完全兼容 MyBatis。你原本习惯手写的 XML SQL、复杂的关联查询 (
ResultMap)、动态 SQL 标签,在引入 MP 后依然可以照常使用。MP 不会破坏你现有的工程结构。只增强(生产力):这是 MP 的杀手锏。它内置了通用的
BaseMapper接口。
以前的痛点:哪怕只是写一个“根据 ID 查询用户”或“插入一个用户”,你都需要先定义 Mapper 接口方法,再去 XML 里写
<select>或<insert>标签,如果不小心把列名拼错了,程序跑起来才会报错。现在的爽点:你的 Mapper 接口只需要继承
BaseMapper,这就好比让你的接口“继承”了一个拥有标准 CRUD 能力的父类。MP 会在启动时自动帮你注入insert、deleteById、updateById、selectById等十几个通用方法的 SQL 实现。对于单表操作,你真的一行 SQL 都不用写!2. 代码中的核心知识点详解与避坑指南
在阅读生成的 Java 代码前,请先掌握以下核心概念,这对应了代码中的关键部分。
2.1 实体类 (Entity) 与 Lombok 的最佳实践
在
UserInfo.java中,你会看到@Data注解。
知识点:Lombok 是一个在 Java 编译阶段生效的工具库。
深层作用:它不是在运行时通过反射生成代码,而是利用 Java 的注解处理器(Annotation Processing),在代码编译成
.class文件时,自动“织入”了get、set、toString、hashCode、equals等方法的字节码。新手扩展:
为什么要用? 如果没有 Lombok,一个拥有 10 个字段的
UserInfo类,其 Getter/Setter 代码可能占据 100 多行。这不仅让核心逻辑(字段定义)被淹没,而且当你修改字段类型时(比如把int改成Integer),还需要手动去改下面的配套方法,非常容易漏改。注意:使用 Lombok 需要在 IDE(如 IDEA)中安装 Lombok 插件,否则 IDE 无法识别这些自动生成的方法,会提示代码报错。
2.2 注解映射 (Mapping Annotations) —— MP 是如何“看懂”数据库的
MP 如何知道 Java 类对应数据库哪张表?靠的是约定大于配置的设计理念以及注解辅助。
@TableName("user_info"):
约定:默认情况下,MP 采用“驼峰转下划线”的规则。如果你的类名是
UserInfo,MP 会默认去数据库找user_info表。配置:现实中数据库表名千奇百怪。如果表名是
t_sys_user,或者为了兼容旧系统叫tblUser,这就破坏了约定。此时必须使用@TableName("t_sys_user")显式告诉 MP:“别猜了,就是这张表”。
@TableId(主键策略):
核心作用:告诉 MP 哪个字段是主键。这至关重要,因为
updateById、deleteById等方法生成的WHERE子句完全依赖于此。深度扩展(IdType):
IdType.AUTO:数据库自增。依赖数据库本身的AUTO_INCREMENT特性。插入时 Java 传null,数据库生成 ID 后,MP 会自动把新 ID 回填到对象中。
IdType.ASSIGN_ID(默认):雪花算法 (Snowflake)。这是 MP 默认的策略。即使你没配置自增,MP 也会在内存中生成一个唯一的 19 位 Long 类型数字作为 ID。这在分布式系统中非常有用,避免了数据库自增锁的性能瓶颈。
@TableField:
字段名映射:处理 Java 属性名 (
userEmail) 和数据库列名 (email_addr) 不一致的情况。新手避坑:
exist = false:有时候你的实体类里需要一个辅助属性(例如private String fullDescription)用来在前端展示,但数据库表中并没有这个列。此时必须加上@TableField(exist = false),否则 MP 在生成 SQL 时会试图去查full_description列,导致报错Unknown column。2.3 BaseMapper 的泛型魔法
在
UserInfoMapper.java中,这一行代码价值千金:public interface UserInfoMapper extends BaseMapper{}
泛型
<UserInfo>的意义:
这是 MP 智能化的源头。你可能会问:“为什么我什么都没写,它就知道要查
user_info表,而不是order_info表?”原理揭秘:MP 在 Spring 启动阶段,会通过 Java 反射机制解析
UserInfoMapper父接口上的泛型参数。它拿到了UserInfo.class,然后去扫描这个类上的@TableName、@TableField等注解。结果:MP 就像一个动态的 SQL 拼装工厂,它在内存中自动组装出了标准 SQL 语句(如
SELECT id, username... FROM user_info),并将其注册到 MyBatis 的核心配置中。这就好比 MP 替你连夜写好了 XML 文件。3. 条件构造器 (Wrapper) - 代码中的重难点
这是新手最容易晕,也是 MP 最灵活、最强大的地方。当你需要摆脱简单的 ID 查询,进行复杂条件筛选时,Wrapper 就是你的 SQL 生成器。
3.1 QueryWrapper (基础版 —— 并不推荐在生产环境大量使用)
QueryWrapperwrapper = new QueryWrapper<>(); wrapper.eq("age", 18) .like("username", "min") .or() .gt("age", 30);
逻辑:上述代码会被翻译成
WHERE (age = 18 AND username LIKE '%min%') OR (age > 30)。致命缺点(魔法值):你需要手动输入字符串
"age"、"username"。
场景演绎:假设数据库管理员把
age字段改成了user_age。你修改了 Java 实体类的属性名,但你忘记(或很难)搜索到代码里所有散落的字符串"age"。编译时一切正常,直到上线后用户点击查询,程序直接崩溃报错Unknown column 'age'。这被称为“魔法值炸弹”。3.2 LambdaQueryWrapper (进阶推荐版 —— 必须掌握)
LambdaQueryWrapperwrapper = new LambdaQueryWrapper<>(); wrapper.eq(UserInfo::getAge, 18) .like(UserInfo::getUsername, "min");
知识点:方法引用 (Method Reference)。
UserInfo::getAge是 Java 8 引入的语法糖,它本质上指向了属性本身。绝对优势:
编译期安全:如果你手抖写成了
UserInfo::getAggge,IDE 会立刻标红,代码根本无法编译通过。这意味着你永远不会因为拼写错误而导致 SQL 运行失败。重构友好:当你在 IDE 中使用重构功能(Refactor -> Rename)将实体类的
age属性改为userAge时,所有使用UserInfo::getAge的地方都会自动同步修改。这是字符串硬编码绝对做不到的。新手建议:除非是极其动态的场景(列名本身就是变量),否则请无脑选择 LambdaQueryWrapper!
4. 扩展知识点 (面试与实战必知)
4.1 逻辑删除 (Logical Delete) —— 数据安全的安全带
代码中出现了
delete_flag。这是企业级开发中的标配。
物理删除 (Physical Delete):执行
DELETE FROM user WHERE id = 1。数据从硬盘上彻底消失。如果员工手滑误删了重要数据,除了去翻数据库备份(可能是一天前的),别无他法。逻辑删除 (Logical Delete):
概念:数据并没有真正消失,只是被打上了“已删除”的标签。通常用
0表示正常,1表示删除。MP 的黑科技:MP 提供了透明化的逻辑删除支持。你只需要在配置文件配置好逻辑删除字段,或者在字段上加
@TableLogic注解。效果:
当你调用
mapper.deleteById(1)时,MP 拦截了这个请求,偷偷把它变成了UPDATE user SET delete_flag = 1 WHERE id = 1。当你调用
mapper.selectById(1)或selectList时,MP 会自动在 SQL 末尾追加AND delete_flag = 0,确保你查不到已删除的数据。价值:既保证了业务上看数据删除了,又保留了数据“尸体”用于审计、数据恢复或历史分析。
4.2 自动填充 (Auto Fill) —— 告别重复劳动
代码中有
createTime和updateTime。
痛点:在传统的开发中,每次写
insert都要记得user.setCreateTime(new Date()),每次update都要记得user.setUpdateTime(new Date())。只要有一处忘了,数据库里就会出现null或旧时间,导致数据不一致。扩展方案:MP 提供了
MetaObjectHandler接口。你可以创建一个配置类实现它,告诉 MP:“每当执行插入操作时,帮我把createTime填上当前时间;每当更新时,帮我更新updateTime”。这样,业务代码中再也不用出现时间设置的代码,既干净又可靠。4.3 为什么不要在循环中调用数据库?(N+1 问题)
在测试代码
testSelectByIds中,使用的是selectBatchIds(批量查询)。这是一个非常重要的性能知识点。
错误示范 (新手常犯):
Listids = Arrays.asList(1L, 2L, 3L, ..., 1000L); for (Long id : ids) { // 灾难现场:在循环里调用 DAO/Mapper mapper.selectById(id); } 后果推演:
网络开销:每一次
selectById都意味着一次【建立连接 -> 发送 SQL -> 数据库执行 -> 返回结果 -> 关闭连接/归还连接池】的全过程。如果 List 有 1000 个 ID,你就与数据库进行了 1000 次网络交互。这比 SQL 执行本身慢得多。连接池耗尽:高并发下,这种代码会瞬间占满数据库连接池,导致其他用户的请求被阻塞,系统瘫痪。
正确做法:使用
selectBatchIds。
它会生成一条 SQL:
SELECT ... FROM ... WHERE id IN (1, 2, 3, ...)。效率:1 次网络交互 vs 1000 次网络交互。效率提升是数量级的。
5. 如何阅读接下来的代码?
看注释:我为几乎每一行代码都添加了详细的
//注释,不仅解释这行代码在做什么,还解释了为什么要这么写。关注 Import:新手常忽略
import部分,导致自己写的时候找不到类。请特别留意com.baomidou.mybatisplus包下的类,不要误引入了其他包的同名类。动手尝试:代码中的
testLambdaQueryWrapper是最精华的部分,建议多读几遍,体会链式调用的优雅。
数据库初始化
-- -------------------------------------------------------- -- 1. 创建数据库 -- IF EXISTS 是为了防止数据库已存在报错 -- DEFAULT CHARACTER SET utf8mb4 是为了支持 Emoji 表情等特殊字符 -- -------------------------------------------------------- DROP DATABASE IF EXISTS mybatis_test; CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4; -- 切换到刚创建的数据库 USE mybatis_test; -- -------------------------------------------------------- -- 2. 创建用户表 (user_info) -- -------------------------------------------------------- DROP TABLE IF EXISTS user_info; CREATE TABLE `user_info` ( `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID,自增', `username` VARCHAR(127) NOT NULL COMMENT '用户名', `password` VARCHAR(127) NOT NULL COMMENT '密码', `age` TINYINT(4) NOT NULL COMMENT '年龄', `gender` TINYINT(4) DEFAULT '0' COMMENT '性别:0-默认,1-男,2-女', `phone` VARCHAR(15) DEFAULT NULL COMMENT '手机号', `delete_flag` TINYINT(4) DEFAULT '0' COMMENT '逻辑删除标志:0-正常,1-已删除', `create_time` DATETIME DEFAULT NOW() COMMENT '创建时间,默认当前时间', `update_time` DATETIME DEFAULT NOW() COMMENT '更新时间,默认当前时间', PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- -------------------------------------------------------- -- 3. 插入初始化测试数据 -- -------------------------------------------------------- INSERT INTO user_info (username, password, age, gender, phone) VALUES ('admin', 'admin', 18, 1, '18612340001'), ('zhangsan', 'zhangsan', 18, 1, '18612340002'), ('lisi', 'lisi', 18, 1, '18612340003'), ('wangwu', 'wangwu', 18, 1, '18612340004');
实体类
package com.bite.mybatis.plus.entity; // 导入 Lombok 的注解,用于自动生成 getter/setter/toString 等方法 import lombok.Data; // 导入 MyBatis-Plus 的注解 import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.IdType; import java.util.Date; /** * 实体类:对应数据库中的表 user_info * * @Data: Lombok 注解。 * 只要加了这个注解,编译后会自动生成所有属性的: * getXXX(), setXXX(), toString(), equals(), hashCode() 方法。 * 省去了手动编写这些样板代码的麻烦。 * * @TableName("user_info"): MyBatis-Plus 注解。 * 告诉框架,这个 UserInfo 类对应数据库里的 "user_info" 表。 * 如果不加这个注解,MP 默认会把类名转换成下划线形式(user_info)去查找表, * 但显式写出来更清晰,也防止类名修改后找不到表。 */ @Data @TableName("user_info") public class UserInfo { /** * @TableId: 标识这是主键字段。 * value = "id": 对应数据库表中的列名 "id"。 * type = IdType.AUTO: 指定主键生成策略。 * AUTO 代表数据库自增 (Auto Increment)。这意味着插入数据时, * Java 代码不需要设置 id 的值,数据库会自动生成。 */ @TableId(value = "id", type = IdType.AUTO) private Integer id; // 使用 Integer 而不是 int,因为 id 可能为 null (虽然主键一般不为空,但在插入前是空的) // 用户名,对应数据库 username 字段 private String username; // 密码,对应数据库 password 字段 private String password; // 年龄 private Integer age; // 性别 private Integer gender; // 手机号 private String phone; /** * @TableField("delete_flag"): 映射非主键字段。 * 场景:如果 Java 属性名叫 deleteFlag (驼峰),数据库列名叫 delete_flag (下划线), * MP 其实会自动映射。 * 但如果数据库列名叫 is_deleted,属性名叫 deleteFlag,就必须用此注解指定: * @TableField("is_deleted") * * 这里显式写出来是为了演示该注解的用法。 */ @TableField("delete_flag") private Integer deleteFlag; // 创建时间 private Date createTime; // 更新时间 private Date updateTime; }
Mapper接口
package com.bite.mybatis.plus.mapper; // 导入 MyBatis-Plus 的基础 Mapper 接口 import com.baomidou.mybatisplus.core.mapper.BaseMapper; // 导入注解和常量,用于自定义 SQL import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.bite.mybatis.plus.entity.UserInfo; import java.util.List; /** * Mapper 接口 * * 1. @Mapper: 这是 Spring/MyBatis 的注解,标记这是一个 Mapper 组件, * Spring Boot 启动时会扫描到它,并创建代理对象注入到容器中。 * * 2. extends BaseMapper: 这是 MP 最核心的用法! * 继承了 BaseMapper,并指定泛型为 。 * 结果:UserInfoMapper 瞬间自动拥有了针对 UserInfo 表的 * insert, delete, update, select 等一系列 CRUD 方法。 * 不需要写任何 XML,也不需要写任何实现类。 */ @Mapper public interface UserInfoMapper extends BaseMapper { // ========================================================= // 以下是 PDF 3.4 节提到的 "自定义 SQL" 示例 // 当 MP 内置的方法满足不了极其复杂的业务需求时,我们可以结合 Wrapper 和手写 SQL // ========================================================= /** * 示例 1: 使用注解方式自定义查询 * * ${ew.customSqlSegment}: * ew 是 EntityWrapper 的缩写(参数名必须叫 ew 或者用 @Param 指定)。 * customSqlSegment 是 Wrapper 对象根据你 Java 代码生成的 SQL 片段(例如 "WHERE age > 18")。 * 注意这里用的是 ${} 进行字符串拼接,因为 Wrapper 生成的是 SQL 关键字,不能用 #{} 预编译。 */ @Select("select id, username, password, age FROM user_info ${ew.customSqlSegment}") List queryUserByCustom(@Param(Constants.WRAPPER) Wrapper wrapper); /** * 示例 2: 对应 XML 配置方式 (假设有 XML 文件) * 这里只定义接口,XML 内容请参考 PDF 页面 16 */ List queryUserByCustom2(@Param(Constants.WRAPPER) Wrapper wrapper); /** * 示例 3: 自定义更新 SQL * * 场景:把某些用户的年龄增加指定的值 (age = age + ?)。 * MP 内置的 update 主要是覆盖值,这种基于原值的运算适合自定义 SQL。 * * #{addAge}: 这是一个普通参数,会被预编译成 ?,防止 SQL 注入。 * ${ew.customSqlSegment}: 这是 Wrapper 生成的条件部分 (例如 "WHERE id IN (1,2,3)")。 */ @Update("UPDATE user_info SET age = age + #{addAge} ${ew.customSqlSegment}") void updateUserByCustom(@Param("addAge") int addAge, @Param(Constants.WRAPPER) Wrapper wrapper); }
测试类(核心逻辑)
package com.bite.mybatis.plus; // 导入测试相关的类 import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; // 导入 MP 的条件构造器 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; // 导入我们的实体和 Mapper import com.bite.mybatis.plus.entity.UserInfo; import com.bite.mybatis.plus.mapper.UserInfoMapper; import java.util.List; import java.util.Arrays; // 注意:PDF里用了 List.of (Java 9+),这里兼容性起见可能用到 Arrays.asList /** * 单元测试类 * * @SpringBootTest: 启动 Spring Boot 上下文。 * 这样我们可以使用 @Autowired 注入 Mapper,像在真实的 Controller 里一样运行代码。 */ @SpringBootTest class MybatisPlusDemoApplicationTests { // 依赖注入:将 Spring 容器管理的 UserInfoMapper 实例注入进来 @Autowired private UserInfoMapper userInfoMapper; // ========================================================= // 2.3 基础 CRUD 测试 (对应 PDF 第 6 页) // ========================================================= /** * 测试插入 (Insert) * MP 会自动根据实体类属性生成 SQL: INSERT INTO user_info (username, ...) VALUES (...) */ @Test void testInsert() { System.out.println("----- 测试插入 -----"); UserInfo user = new UserInfo(); user.setUsername("bite"); // 设置用户名 user.setPassword("123456"); // 设置密码 user.setAge(11); user.setGender(0); user.setPhone("18610001234"); // 注意:我们没有设置 ID,因为配置了 AUTO 自增,数据库会处理 // 调用 BaseMapper 提供的 insert 方法 int result = userInfoMapper.insert(user); // 扩展知识:插入成功后,MP 会自动把数据库生成的 ID 回填到 user 对象中 System.out.println("影响行数: " + result); System.out.println("插入后的主键ID: " + user.getId()); } /** * 测试根据 ID 查询 */ @Test void testSelectById() { System.out.println("----- 测试根据 ID 查询 -----"); // 这里的 1L 表示 Long 类型,或者直接写 1 UserInfo user = userInfoMapper.selectById(1); System.out.println("查询结果: " + user); } /** * 测试批量查询 ID (WHERE id IN (...)) */ @Test void testSelectByIds() { System.out.println("----- 测试批量查询 -----"); // List.of 是 Java 9+ 语法,如果报错请换成 Arrays.asList(1, 2, 3, 4) Listids = Arrays.asList(1, 2, 3, 4); List users = userInfoMapper.selectBatchIds(ids); // 使用 Lambda 表达式打印结果 users.forEach(System.out::println); } /** * 测试根据 ID 更新 * 注意:MP 的 updateById 是"动态 SQL",它只会更新你设置了非空值的字段。 * 如果你只 setPassword,SQL 就只会 UPDATE ... SET password = ? ... * 这里的 age、phone 等其他字段不会被修改为 null。 */ @Test void testUpdateById() { System.out.println("----- 测试更新 -----"); UserInfo user = new UserInfo(); user.setId(1); // 必须指定 ID,否则 MP 不知道更新哪条 user.setPassword("4444444"); // 只更新密码 userInfoMapper.updateById(user); } /** * 测试根据 ID 删除 */ @Test void testDelete() { System.out.println("----- 测试删除 -----"); userInfoMapper.deleteById(5); // 删除 ID 为 5 的记录 } // ========================================================= // 3.3 条件构造器测试 (Wrapper) - 重点知识! // ========================================================= /** * 3.3.1 QueryWrapper (普通查询构造器) * 需求:SELECT ... WHERE age = 18 AND username LIKE '%min%' */ @Test void testQueryWrapper() { System.out.println("----- QueryWrapper 测试 -----"); QueryWrapper wrapper = new QueryWrapper<>(); // 链式调用 wrapper.select("id", "username", "password", "age") // 指定查询哪些列,不查全部 .eq("age", 18) // eq: equal (等于) -> age = 18 .like("username", "min"); // like: 模糊查询 -> username like '%min%' List userInfos = userInfoMapper.selectList(wrapper); userInfos.forEach(System.out::println); } /** * QueryWrapper 用于更新 * 需求:UPDATE user_info SET delete_flag = 1 WHERE age < 20 */ @Test void testUpdateByQueryWrapper() { System.out.println("----- QueryWrapper 更新测试 -----"); // 1. 定义更新的条件 (WHERE age < 20) QueryWrapper wrapper = new QueryWrapper<>(); wrapper.lt("age", 20); // lt: less than (小于) // 2. 定义要更新成什么样 (SET delete_flag = 1) UserInfo userInfo = new UserInfo(); userInfo.setDeleteFlag(1); // 执行:把符合 wrapper 条件的数据,更新为 userInfo 里的样子 userInfoMapper.update(userInfo, wrapper); } /** * 3.3.2 UpdateWrapper (专门用于更新的构造器) * 场景:不创建 UserInfo 实体对象,直接设置 SET 语句 * 需求:UPDATE user_info SET delete_flag=0, age=5 WHERE id IN (1,2,3) */ @Test void testUpdateByUpdateWrapper() { System.out.println("----- UpdateWrapper 测试 -----"); UpdateWrapper updateWrapper = new UpdateWrapper<>(); updateWrapper.set("delete_flag", 0) // 显式设置 SET 字段 .set("age", 5) .in("id", Arrays.asList(1, 2, 3)); // WHERE id IN ... userInfoMapper.update(null, updateWrapper); // 第一个参数传 null,因为 Set 子句已经在 wrapper 里了 } /** * UpdateWrapper 执行 SQL 片段 * 需求:UPDATE user_info SET age = age + 10 WHERE id IN (1,2,3) */ @Test void testUpdateBySQLUpdateWrapper() { System.out.println("----- UpdateWrapper SQL 片段测试 -----"); UpdateWrapper updateWrapper = new UpdateWrapper<>(); updateWrapper.setSql("age = age + 10") // 直接拼接 SQL 片段,适合做数学运算 .in("id", Arrays.asList(1, 2, 3)); userInfoMapper.update(null, updateWrapper); } // ========================================================= // 3.3.3 LambdaQueryWrapper (推荐使用!!) // 优点:防手抖。不用写 "age" 字符串,而是用 UserInfo::getAge 方法引用。 // 如果实体类属性名改了,这里会自动报错,而不是运行时才报错。 // ========================================================= @Test void testLambdaQueryWrapper() { System.out.println("----- LambdaQueryWrapper 测试 (强力推荐) -----"); // 写法 1:new 对象 // LambdaQueryWrapper lambda = new LambdaQueryWrapper<>(); // 写法 2:通过 QueryWrapper 转 lambda (更常用) QueryWrapper wrapper = new QueryWrapper<>(); List list = userInfoMapper.selectList(wrapper.lambda() .select(UserInfo::getUsername, UserInfo::getPassword, UserInfo::getAge) // 看着很长,但全是IDE自动补全的,很安全 .eq(UserInfo::getId, 1) // WHERE id = 1 ); list.forEach(System.out::println); } /** * 3.3.4 LambdaUpdateWrapper */ @Test void testLambdUpdateByUpdateWrapper() { System.out.println("----- LambdaUpdateWrapper 测试 -----"); LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.set(UserInfo::getDeleteFlag, 0) // SET delete_flag = 0 .set(UserInfo::getAge, 5) // SET age = 5 .in(UserInfo::getId, Arrays.asList(1, 2, 3)); // WHERE id IN (1,2,3) userInfoMapper.update(null, updateWrapper); } // ========================================================= // 3.4 自定义 SQL 测试 // ========================================================= @Test void testQueryUserByCustom() { System.out.println("----- 自定义 SQL 测试 -----"); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("username", "admin"); // 调用 Mapper 中我们自己定义的 queryUserByCustom 方法 // 此时 wrapper 生成的 SQL 会自动填充到 ${ew.customSqlSegment} 中 List result = userInfoMapper.queryUserByCustom(wrapper); result.forEach(System.out::println); } @Test void updateUserByCustom() { System.out.println("----- 自定义 Update SQL 测试 -----"); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.in("id", Arrays.asList(1, 2, 3)); // 将年龄统一增加 10 岁 userInfoMapper.updateUserByCustom(10, wrapper); } }
以上为您生成了完整的学习资料,建议您按照 Guide 文档 -> 实体类 -> Mapper -> 测试类 的顺序阅读,祝您学习愉快!
为了让你能顺利运行,我为你补充了 3 个必不可少的配置文件:
pom.xml: 项目的“购物清单”。告诉 Maven 我们需要下载 Spring Boot、MyBatis-Plus、MySQL 驱动等所有依赖。application.yml: 项目的“遥控器”。配置你的数据库账号密码。MybatisPlusApplication.java: 项目的“启动钥匙”。Spring Boot 的主启动类。README_Run_Guide.md: 保姆级运行手册。一步步教你在 IDEA 里怎么操作。
请按照下面的 README 手册 一步步操作,保证能跑通!
如何运行本项目 (保姆级教程).md
MyBatis-Plus 实战项目运行指南
你好!请按照以下步骤,在你的电脑桌面上搭建并运行这个项目。
第一步:环境准备 (必看)
在开始之前,请确保你的电脑安装了以下软件:
JDK: 推荐 JDK 17 或 JDK 8。
IntelliJ IDEA: 推荐使用社区版 (Community) 或 旗舰版 (Ultimate)。
MySQL 数据库: 确保数据库服务已开启,且记得你的账号和密码。
Maven: 通常 IDEA 自带了 Maven,不需要额外安装。
第二步:搭建工程骨架
新建文件夹:
在桌面上新建一个文件夹,命名为
mybatis-plus-demo。用 IDEA 打开:
启动 IDEA,点击
Open(打开),选择桌面上的mybatis-plus-demo文件夹。创建标准目录结构:
在 IDEA 左侧的项目栏中,右键点击项目名,按照以下结构新建文件夹(Directory):
mybatis-plus-demo ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── bite │ │ │ └── mybatis │ │ │ └── plus │ │ │ ├── entity (存放 UserInfo.java) │ │ │ └── mapper (存放 UserInfoMapper.java) │ │ └── resources (存放 application.yml) │ └── test │ └── java │ └── com │ └── bite │ └── mybatis │ └── plus (存放 MybatisPlusTests.java) └── pom.xml (直接放在最外层)第三步:复制文件 (核心步骤)
请将我生成的代码复制到对应的位置:
pom.xml:
在项目最外层新建
pom.xml文件。复制 我刚才提供的
pom.xml代码进去。重要:复制完后,点击 IDEA 右上角出现的一个小图标(类似于“M”形状的刷新按钮),或者右键
pom.xml选择Maven->Reload Project。这会自动下载依赖包(需要联网)。
application.yml:
在
src/main/resources目录下新建application.yml。复制 代码进去。
修改密码:找到
password: root这一行,把root改成你本地 MySQL 的真实密码!(如果是空密码就留空,但通常不建议)。Java 代码:
UserInfo.java: 放入src/main/java/com/bite/mybatis/plus/entity
UserInfoMapper.java: 放入src/main/java/com/bite/mybatis/plus/mapper
MybatisPlusApplication.java: 放入src/main/java/com/bite/mybatis/plus(这是启动类,放在包的最外层)
MybatisPlusTests.java: 放入src/test/java/com/bite/mybatis/plus第四步:初始化数据库
打开你的数据库工具(如 MySQL Workbench, Navicat, DBeaver, 或 IDEA 自带的 Database 面板)。
复制 我提供的
schema.sql文件的内容。执行 SQL:全选并在数据库中运行。
这会创建名为
mybatis_test的数据库。创建
user_info表。插入 4 条初始数据。
第五步:运行测试
现在是最激动人心的时刻!
打开
src/test/java/.../MybatisPlusTests.java文件。你会看到很多
@Test标记的方法,比如testInsert(),testSelectById()。点击运行:
点击方法名左边的小绿三角 ▶️,选择
Run 'testInsert()'。观察控制台:
IDEA 下方的控制台会打印出日志。
如果是
testInsert,你会看到Insert语句和“插入成功”的日志。如果是
testSelectById,你会看到查询到的 User 对象。恭喜你!你已经成功运行了一个企业级的 MyBatis-Plus 项目!
Maven配置文件
4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.0 com.bite mybatis-plus-demo 0.0.1-SNAPSHOT mybatis-plus-demo MyBatis-Plus 新手实战项目 17 org.springframework.boot spring-boot-starter-web com.baomidou mybatis-plus-spring-boot3-starter 3.5.5 com.mysql mysql-connector-j runtime org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
数据库配置
# 数据库连接配置 spring: datasource: # 数据库驱动,新版 MySQL 驱动类名 driver-class-name: com.mysql.cj.jdbc.Driver # 数据库 URL # jdbc:mysql://IP地址:端口/数据库名 # useSSL=false: 关闭 SSL 安全连接(开发环境通常关闭以避免报错) # serverTimezone=Asia/Shanghai: 设置时区,防止时间差 8 小时 # allowPublicKeyRetrieval=true: 允许公钥检索(解决某些连接错误) url: jdbc:mysql://127.0.0.1:3306/mybatis_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true # 数据库账号 username: root # 数据库密码 【!!!请修改这里为你自己的密码!!!】 password: root # MyBatis-Plus 配置 mybatis-plus: configuration: # 开启控制台 SQL 日志打印 # 这样你运行测试时,控制台会打印出实际执行的 SQL 语句,非常适合学习! log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 逻辑删除配置:1 表示已删除,0 表示未删除 logic-delete-value: 1 logic-not-delete-value: 0
Spring Boot启动类
package com.bite.mybatis.plus; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 项目的启动类 * @SpringBootApplication: 标记这是一个 Spring Boot 应用 * @MapperScan: 扫描 Mapper 接口所在的包 * 告诉 MP 去哪里找我们定义的 UserInfoMapper。 * 如果不加这个注解,或者包路径写错了,会报错 "Field userInfoMapper in ... required a bean of type ..." */ @SpringBootApplication @MapperScan("com.bite.mybatis.plus.mapper") public class MybatisPlusApplication { public static void main(String[] args) { SpringApplication.run(MybatisPlusApplication.class, args); } }
好的,所有的材料都准备齐全了。请按照 README_Run_Guide.md 中的步骤开始你的实战之旅吧!如果在运行过程中遇到任何红色报错,随时把报错信息发给我,我来帮你诊断。加油!
太棒了!Exit code 0 代表程序完美运行,没有任何报错。恭喜你,你已经正式跨过了 MyBatis-Plus 的门槛!
这不仅仅是一堆日志,这其实是 MyBatis-Plus (MP) 在向你汇报工作。这个项目通过一系列测试,模拟了真实开发中对数据库的各种操作。
让我们像看“体检报告”一样,根据你控制台打印出的日志,一条条解读 这个项目到底干了啥:
1. 批量修改数据 (UpdateWrapper)
日志证据:
Plaintext----- UpdateWrapper 测试 ----- ==> Preparing: UPDATE user_info SET delete_flag=?,age=? WHERE (id IN (?,?,?)) ==> Parameters: 0(Integer), 5(Integer), 1(Integer), 2(Integer), 3(Integer) <== Updates: 3解读:MP 帮你把 ID 为 1, 2, 3 的用户,一次性把
delete_flag改为了 0,age改为了 5。牛逼之处:你没写
WHERE id IN这种复杂的 SQL,是 Java 代码自动生成的。
2. 数学运算更新 (SQL 片段)
日志证据:
Plaintext----- UpdateWrapper SQL 片段测试 ----- ==> Preparing: UPDATE user_info SET age = age + 10 WHERE (id IN (?,?,?))解读:它给 ID 为 1, 2, 3 的用户,年龄统统加了 10 岁。这展示了 MP 允许你在自动生成的 SQL 里夹带“私货”(自定义 SQL 片段)。
3. 单条更新 (UpdateById)
日志证据:
Plaintext----- 测试更新 ----- ==> Preparing: UPDATE user_info SET password=? WHERE id=? ==> Parameters: 4444444(String), 1(Integer)解读:专门把 ID 为 1 的用户(admin)的密码改成了 "4444444"。注意,它只更新了密码,其他字段没动,这就是“动态 SQL”的智能之处。
4. 精准查询 (LambdaQueryWrapper)
日志证据:
Plaintext----- LambdaQueryWrapper 测试 (强力推荐) ----- ==> Preparing: SELECT username,password,age FROM user_info WHERE (id = ?)解读:它去数据库查了 ID 为 1 的用户,但只取了 用户名、密码和年龄这三个字段,没取别的。这是一种优化性能的查询方式。
5. 批量查询 (SelectByIds)
日志证据:
Plaintext----- 测试批量查询 ----- ==> Preparing: SELECT ... FROM user_info WHERE id IN ( ? , ? , ? , ? ) <== Row: 1, admin... <== Row: 2, zhangsan... <== Total: 4解读:一次性把 ID 为 1, 2, 3, 4 的四个用户全查出来了。这避免了你写循环去查库(N+1 问题)。
6. 条件更新 (QueryWrapper)
日志证据:
Plaintext----- QueryWrapper 更新测试 ----- ==> Preparing: UPDATE user_info SET delete_flag=? WHERE (age < ?) ==> Parameters: 1(Integer), 20(Integer) <== Updates: 4解读:它执行了一个业务逻辑——把所有“年龄小于 20 岁”的用户,
delete_flag标记为 1(逻辑删除)。日志显示有 4 个人符合条件被修改了。
7. 删除操作 (Delete)
日志证据:
Plaintext----- 测试删除 ----- ==> Preparing: DELETE FROM user_info WHERE id=? ==> Parameters: 5(Integer) <== Updates: 0解读:它尝试删除 ID 为 5 的用户。
注意:这里显示
Updates: 0,说明删除失败了(没删掉任何行)。为什么?因为代码运行顺序的问题,ID 为 5 的用户在这一步还没被插入呢!(看下一步)
8. 插入新用户 (Insert)
日志证据:
Plaintext----- 测试插入 ----- ==> Preparing: INSERT INTO user_info ( username, ... ) VALUES ( ?, ... ) ==> Parameters: bite(String), ... <== Updates: 1 插入后的主键ID: 5解读:成功向数据库插入了一个叫 "bite" 的新用户。数据库自动给它分配了 ID 5。
(所以如果在 "测试删除" 之前运行这一步,删除就会成功了)
总结
这个项目通过实战向你展示了 MyBatis-Plus 的核心价值观:
极简主义:你没有手写一句
INSERT、UPDATE或SELECT语句,但完成了所有复杂的增删改查。安全性:所有的参数(比如
admin,18)都通过?占位符传递,防止了 SQL 注入攻击。灵活性:既能全自动(BaseMapper),也能半自动(Wrapper 组装条件),还能搞特权(自定义 SQL)。
现在的你,已经拥有了一个可以随时用来实验数据库操作的“演武场”!
这是一个非常棒的问题!你现在遇到的情况是:代码运行速度太快(几毫秒就跑完了),就像一阵风刮过,你还没来得及眨眼,数据库里的数据可能已经经历了“插入 -> 修改 -> 删除”的一生。
要想亲眼看到数据变化的路径(比如:先变出一条数据,然后年龄变了,最后消失了),你需要让时间“慢下来”,或者让程序“停下来”等你查岗。
这里有三种方法,推荐程度由高到低,强烈推荐方法一:
方法一:使用 IDEA 的“断点调试” (Debug 模式) —— 上帝视角 ️♂️
这是程序员最常用的方法,就像给程序按下了“暂停键”。
操作步骤:
打断点 (红点):
打开你的
MybatisPlusTests.java。找到
testUpdateById方法。在
userInfoMapper.updateById(user);这一行的行号左边,用鼠标左键点一下。你会看到一个 红色的圆点。这就是断点。
启动调试 (Debug):
不要点绿色三角形的 Run。
要点绿色三角形旁边那个 虫子图标 (Debug)。
或者右键方法名,选择 Debug 'testUpdateById()'。
观察变化:
程序运行到红点处会自动卡住(暂停)。
此时动作 1:切换到你的数据库软件(Navicat / DBeaver / IDEA Database),刷新表
user_info。你会发现数据还没变。此时动作 2:回到 IDEA,按一下 F8 (Step Over) 或者点击调试面板上的 折线箭头(步过)。这代表让程序执行这一行代码。
此时动作 3:再次切换到数据库软件,刷新表。见证奇迹! 数据变了!
通过这种方式,你可以一步步控制代码执行,每走一步就去数据库看一眼,完全掌握数据变化的每一个瞬间。
方法二:手动“单步执行” —— 稳扎稳打
如果你觉得 Debug 有点复杂,可以笨办法:不要运行整个类,而是手动一个个运行方法,并配合数据库刷新。
前提:先去数据库执行 TRUNCATE TABLE user_info; 把表清空,或者恢复到初始状态。
操作步骤:
第一步:只运行插入
找到
testInsert方法。点击左边绿色箭头 ->
Run 'testInsert()'.看数据库:刷新,你会看到多了一行数据(假设 ID 是 5)。
第二步:只运行更新
找到
testUpdateById方法。关键修改:把代码里的
user.setId(1)改成刚才插入的 ID(比如user.setId(5))。点击左边绿色箭头 ->
Run 'testUpdateById()'.看数据库:刷新,ID 为 5 的用户密码变了。
第三步:只运行删除
找到
testDelete方法。修改 ID 为 5。
运行 -> 看数据库:刷新,ID 为 5 的用户
delete_flag变成了 1(逻辑删除)或者行消失了(物理删除)。
方法三:在代码里“强行睡觉” (不推荐但直观)
你可以强行让代码执行完一步后,睡个 10 秒钟,给你留出刷新数据库的时间。
操作步骤:
修改你的测试代码,加上 Thread.sleep:
Java
@Test
void testLifeCycle() throws InterruptedException {
// 1. 插入
UserInfo user = new UserInfo();
user.setUsername("Flash");
user.setAge(20);
user.setPassword("123");
userInfoMapper.insert(user);
System.out.println(">>> 插入完成!快去数据库看!给你10秒钟...");
Thread.sleep(10000); // 暂停 10 秒
// 2. 更新
user.setAge(99);
userInfoMapper.updateById(user);
System.out.println(">>> 更新完成!快去数据库看!给你10秒钟...");
Thread.sleep(10000); // 暂停 10 秒
// 3. 删除
userInfoMapper.deleteById(user.getId());
System.out.println(">>> 删除完成!去看看还在不在?");
}
运行这个方法,你就有 10 秒钟的时间切屏去数据库刷新查看变化了。
总结建议
作为新手,我强烈建议你尝试方法一(Debug 模式)。
虽然它看起来有点高级,但这是程序员最核心的技能之一。学会了 Debug,你不仅能看到数据的变化路径,以后遇到 Bug 也能看到程序是怎么“跑偏”的。
现在,去试着在一个 update 语句上打个红点,然后点那个 虫子图标试试吧!
浙公网安备 33010602011771号