MyBatis-Plus 从零到实战:一篇真正写给新手的完整指南
MyBatis-Plus 从零到实战:一篇真正写给新手的完整指南
一、MyBatis-Plus 到底解决了什么问题?
在没有 MyBatis-Plus 之前,我们通常这样写 MyBatis:
-
一个表一个
Mapper.xml -
insert / update / selectById都要自己写 SQL -
动态条件靠
<if>、<where>拼
结果就是:
-
CRUD SQL 大量重复
-
Mapper 层越来越臃肿
-
新人很难快速上手
👉 MyBatis-Plus 的定位非常清晰:
在 不改变 MyBatis 使用方式 的前提下,
封装 80% 的单表 CRUD 场景。
它不是 ORM,也不会替你写业务 SQL,它解决的是:
重复、机械、容易出错的那部分数据库操作。
二、实体类与基础注解(一定要打牢)
2.1 示例表结构(贯穿全文)
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
age INT,
status TINYINT,
is_deleted TINYINT DEFAULT 0
);
2.2 @TableName:表名映射
@Data
@TableName("t_user")
public class User {
private Long id;
private String username;
private Integer age;
private Integer status;
}
👉 什么时候必须写 @TableName?
-
表名和类名不一致(
t_uservsUser) -
表有前缀(
t_/sys_)
不写就会出现:
Table 'xxx.user' doesn't exist
2.3 @TableId:主键映射(新手必踩坑)
@TableId(type = IdType.AUTO)
private Long id;
常见 IdType:
AUTO:数据库自增ASSIGN_ID:MP 雪花算法(推荐新项目)
❓ insert 后 id 为 null?
👉 90% 原因:没写 @TableId 或类型不匹配。
2.4 @TableField:字段映射与排除
数据库字段 ≠ Java 字段
@TableField("is_deleted")
private Integer deleted;
👉 原则一句话:
Java 命名服务语义,数据库命名服务历史。
排除非表字段
@TableField(exist = false)
private String roleName; // join 查询字段
不加 exist = false,SQL 会直接报错。
2.5 BaseEntity 封装统一字段(待补充)
id、is_deleted、create_time、update_time
在使用 MyBatis-Plus 时,将实体类(Entity)抽取出一个基类(BaseEntity)是为了实现统一字段管理,使代码更加简洁、易维护。具体的好处包括:
统一字段管理
BaseEntity 通常包含一些通用的字段,比如:
create_time:记录实体的创建时间。update_time:记录实体的更新时间。deleted:逻辑删除标志字段。
这些字段在大多数表中都可能出现,特别是在支持 逻辑删除 和 审计字段(创建时间、更新时间) 的场景中。如果每个实体类都需要单独定义这些字段,会导致代码重复,维护起来也很麻烦。
减少代码重复
通过抽取 BaseEntity 类,可以将这些通用字段统一放在基类中,不需要每个实体类都重复定义。例如:
public class BaseEntity {
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Boolean deleted;
}
然后,其他的实体类继承 BaseEntity,就可以直接拥有这些字段:
public class Question extends BaseEntity {
private Long id;
private String title;
private String content;
}
减少冗余的数据库操作
在 MyBatis-Plus 中,使用基类(BaseEntity)后,可以借助 MyBatis-Plus 提供的自动填充(@TableField)功能,自动对字段(如 create_time 和 update_time)进行填充,减少了手动设置的工作量。
例如,使用 MyBatis-Plus 的 自动填充字段:
public class BaseEntity {
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Boolean deleted;
}
@TableField(fill = FieldFill.INSERT):表示createTime字段在插入时自动填充。@TableField(fill = FieldFill.INSERT_UPDATE):表示updateTime字段在插入和更新时自动填充。@TableLogic:表示启用逻辑删除,自动处理deleted字段。
简化增删改查操作
BaseEntity 还可以配合 MyBatis-Plus 的通用 Mapper(如 BaseMapper)来减少一些通用操作的重复代码,例如:
- 插入数据时自动填充
create_time、update_time字段。 - 删除数据时自动更新
deleted字段(逻辑删除)。
通过继承 BaseEntity,实体类就自动具有了这些通用字段,且 BaseMapper 会自动处理。
增强可维护性
如果需要修改这些通用字段(例如,修改时间戳字段的类型或处理逻辑删除的方式),只需要在 BaseEntity 中进行一次修改,而不需要修改每个实体类。这样可以减少维护成本,提升系统的可扩展性。
自动注入公共字段
在一些开发框架或项目中,BaseEntity 还可以配合 AOP 或 事件监听器 自动填充公共字段。例如,可以在用户登录时,自动注入当前登录用户的 ID 作为 create_by 或 update_by 字段。
自动填充时间字段
/**
* 处理器,处理更新时间和插入时间
*/
@Component
public class MybatisPlusDateFillHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 新增时,自动填充创建时间和更新时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时自动填充更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
小结:
BaseEntity让通用字段(如create_time、update_time、deleted)的管理变得更加规范、统一,减少了代码重复,提高了开发效率。- MyBatis-Plus 提供了 自动填充 和 逻辑删除 的功能,可以使这部分字段自动管理,减少了开发中的手动操作。
因此,将实体类抽取出 BaseEntity 类,是 MyBatis-Plus 提供的一种提升开发效率和代码可维护性的最佳实践。
三、Mapper 层:BaseMapper 的能力、边界与常用方法
3.1 什么是 BaseMapper?
public interface UserMapper extends BaseMapper<User> {
}
只要继承 BaseMapper,你就自动拥有一套单表通用 CRUD 能力,无需 XML、无需手写 SQL。
本质一句话:
BaseMapper 解决的是:如何“操作一张表”。
3.2 BaseMapper 常用方法(你每天都会用)
1️⃣ 新增
int insert(T entity);
- 插入非空字段
- 主键是否回填,取决于
@TableId
示例代码:
@Test
public void test7(){
Student student = new Student();
student.setStudentNo("11");
student.setName("李11");
int insert = studentMapper.insert(student);
System.out.println(insert);
// 获取id
System.out.println("id = " + student.getId());
}
2️⃣ 根据主键查询
T selectById(Serializable id);
-
只适合主键查询
-
条件固定为
where id = ?
示例代码:
@Test
public void test8(){
Student student = studentMapper.selectById(1L);
System.out.println(student);
}
3️⃣ 条件查询(最常用)
List<T> selectList(Wrapper<T> wrapper);
@Test
public void test9(){
LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Student::getGender,1);
List<Student> students = studentMapper.selectList(queryWrapper);
students.forEach(System.out::println);
}
⚠️ wrapper = null 等价于 全表查询。
4️⃣ 更新
int updateById(T entity);
-
只更新非空字段
-
where 条件只能是主键
示例代码:
@Test
public void test10(){
Student student = new Student();
student.setId(5L);
student.setEmail("111@qq.com");
int i = studentMapper.updateById(student);
System.out.println(i);
}
5️⃣ 条件更新
int update(T entity, Wrapper<T> updateWrapper);
- 示例代码:
@Test
public void test11(){
LambdaUpdateWrapper<Student> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(Student::getEmail,"222@qq.com");
updateWrapper.eq(Student::getId,1);
int i = studentMapper.update(updateWrapper);
System.out.println(i);
}
6️⃣ 删除(逻辑删除生效)
int deleteById(Serializable id);
- 开启
@TableLogic后 - 实际执行的是
update - 未开启,则物理删除。
示例代码:
@Test
public void test12(){
int i = studentMapper.deleteById(11);
System.out.println(i);
}
3.3 BaseMapper 的能力边界
✅ 适合:
-
单表操作
-
返回 Entity
-
条件不复杂
❌ 不适合:
-
多表 join
-
VO / DTO 返回
-
聚合统计
3.2 BaseMapper 适合 & 不适合
✅ 适合:
-
单表
-
返回 Entity
-
条件不复杂
❌ 不适合:
-
多表 join
-
VO / DTO
-
聚合统计
3.3 自定义 Mapper(含分页)
IPage<UserVO> selectUserPage(Page<UserVO> page,
@Param("username") String username);
<select id="selectUserPage" resultType="UserVO">
SELECT id, username, age
FROM t_user
WHERE is_deleted = 0
</select>
📌 不要写 limit,分页插件会自动加。
四、Service 层:业务边界与常用方法
一句非常实用的经验法则:
Controller → 只调 Service
Service → 优先用 Service 自带方法,不够再调 Mapper
4.1 为什么 Service 要继承 ServiceImpl?
@Service
public class UserServiceImpl
extends ServiceImpl<UserMapper, User>
implements UserService {
}
ServiceImpl 本质是:
对 BaseMapper 的一层通用业务封装。
它帮你屏蔽了:
-
注入 Mapper
-
重复 CRUD 代码
4.2 Service 层常用方法(高频)
1️⃣ 保存
boolean save(T entity);
boolean saveOrUpdate(T entity);
- 示例代码
@Test
public void test2(){
Student student = new Student();
student.setGender(1);
student.setName("赵六");
student.setStudentNo("123456");
boolean save = studentService.save(student);
System.out.println(save);
}
- 保存或更新,根据主键ID进行判断,有则是更新没有则是保存。
@Test
public void test13(){
Student student = new Student();
student.setGender(1);
student.setStudentNo("13");
student.setName("周13");
boolean b = studentService.saveOrUpdate(student);
System.out.println("b = " + b);
}
2️⃣ 批量保存
boolean saveBatch(Collection<T> list);
- 示例代码
@Test
public void test3(){
Student student = new Student();
student.setName("赵六");
student.setStudentNo("1234566");
Student student1 = new Student();
student1.setName("钱7");
student1.setStudentNo("1234567");
Student student2 = new Student();
student2.setName("孙8");
student2.setStudentNo("12345678");
List<Student> list = List.of(student, student1, student2);
boolean save = studentService.saveBatch(list);
System.out.println(save);
}
3️⃣ 根据 ID 查询
T getById(Serializable id);
- 示例代码:
@SpringBootTest(classes = MybatisPlusMain.class)
public class StudentTest {
@Autowired
private StudentService studentService;
@Test
public void test(){
Student byId = studentService.getById(1L);
System.out.println("byId = " + byId);
}
}
4️⃣ 条件查询
List<T> list(Wrapper<T> wrapper);
@Test
public void test1(){
LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Student::getGender,1);
List<Student> list = studentService.list(queryWrapper);
list.forEach(System.out::println);
}
5️⃣ 更新
boolean updateById(T entity);
boolean update(Wrapper<T> wrapper);
- 示例代码
@Test
public void test4(){
Student student = new Student();
student.setId(4L);
student.setName("赵六1");
studentService.updateById(student);
}
@Test
public void test5(){
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Student::getName,"赵六2");
wrapper.eq(Student::getId,11);
boolean update = studentService.update(wrapper);
System.out.println(update);
}
6️⃣ 删除
boolean removeById(Serializable id);
boolean remove(Wrapper<T> wrapper);
- 示例代码:
@Test
public void test6(){
boolean b = studentService.removeById(13L);
System.out.println(b);
LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Student::getId,12L);
boolean remove = studentService.remove(queryWrapper);
System.out.println(remove);
}
4.3 Service 层应该做什么?
-
组合多个 Mapper
-
控制事务
-
屏蔽数据库结构
❌ 不要:
-
Controller 直接调 Mapper
-
Service 返回 Entity 给前端
4.4 一个推荐的 Service 写法示例
public List<UserVO> listActiveUsers() {
return this.list(
new LambdaQueryWrapper<User>()
.eq(User::getStatus, 1)
).stream()
.map(this::convertToVO)
.toList();
}
这里体现了三点:
-
Wrapper 只存在于 Service
-
Controller 无感知 SQL
-
返回的是 VO
4.2 Service 层应该做什么?
-
组合多个 Mapper
-
控制事务
-
屏蔽表结构
❌ 不要:
-
Controller 直接调 Mapper
-
Service 返回 Entity 给前端
4.3 Service 返回 Entity 还是 VO?
public UserVO getUser(Long id) {
User user = getById(id);
return convertToVO(user);
}
👉 对外返回 VO / DTO,长期更安全。
五、Wrapper 条件构造器(QueryWrapper / LambdaQueryWrapper)
在 MyBatis-Plus 中,Wrapper 是整个查询体系的核心。
很多新手第一次看到 Wrapper,都会被这些类名搞懵:
-
QueryWrapper
-
LambdaQueryWrapper
-
UpdateWrapper
-
LambdaUpdateWrapper
常见疑问:
为什么要搞这么多 Wrapper?
QueryWrapper 和 LambdaQueryWrapper 到底有什么区别?
我应该用哪个?
这一节我们只聚焦 最常用、最容易混淆的一对:QueryWrapper 与 LambdaQueryWrapper。
5.1 QueryWrapper 与 LambdaQueryWrapper 的关系
先说结论:
LambdaQueryWrapper 不是新能力,而是 QueryWrapper 的“安全写法”。
它们的最终目的完全一致:
-
构建 WHERE 条件
-
拼接 SQL
-
执行查询
例如,下面两段代码生成的 SQL 是一模一样的。
QueryWrapper 写法(字符串字段)
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", "Tom")
.gt("age", 18);
List<User> users = userMapper.selectList(wrapper);
LambdaQueryWrapper 写法(方法引用)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, "Tom")
.gt(User::getAge, 18);
List<User> users = userMapper.selectList(wrapper);
👉 SQL 层面没有任何区别。
5.2 为什么不推荐 QueryWrapper(字符串字段)
wrapper.eq("username", "Tom");
这行代码表面没问题,但隐藏了几个新手非常容易踩的坑。
❌ 1️⃣ 字段拼错,编译期发现不了
wrapper.eq("uesrname", "Tom"); // 拼错
-
编译通过
-
运行时报错或查不到数据
❌ 2️⃣ 字段重命名,代码不会报错
数据库字段修改后,所有字符串都需要人工排查。
❌ 3️⃣ 实体字段与数据库字段割裂
@TableField("user_name")
private String username;
Java 用 username,Wrapper 却写 "user_name",认知成本高。
5.3 LambdaQueryWrapper 的优势
LambdaQueryWrapper 用 方法引用代替字符串字段名:
wrapper.eq(User::getUsername, "Tom");
优势总结:
-
✅ 编译期安全
-
✅ 支持 IDE 重构
-
✅ 与实体语义完全一致
5.4 使用建议(直接记)
业务代码中,优先使用 LambdaQueryWrapper。
| 场景 | 推荐 |
|---|---|
| 日常业务 | LambdaQueryWrapper |
| Service 层 | LambdaQueryWrapper |
| 临时调试 | QueryWrapper |
5.2 UpdateWrapper:更新条件
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1)
.set("status", 0);
👉 Query = 查,Update = 改,职责清晰。
5.3 LambdaQueryWrapper(强烈推荐)
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getStatus, 1)
.like(User::getUsername, "tom");
优势:
-
编译期校验
-
重构安全
5.4 condition:动态条件
wrapper.like(
StringUtils.hasText(username),
User::getUsername,
username
);
告别 if + 拼 SQL。
六、逻辑删除:删除行为的改变
逻辑删除并不是 MyBatis-Plus 独有的概念,但 MP 把它做成了一个“默认行为”,这点对新手非常重要。
6.1 基于注解的逻辑删除(回顾)
@TableField("is_deleted")
@TableLogic(value = "0", delval = "1")
private Integer deleted;
当你执行:
userMapper.deleteById(1L);
实际执行的 SQL 是:
UPDATE t_user SET is_deleted = 1 WHERE id = ?;
并且:
-
所有查询 SQL
-
会自动加上
is_deleted = 0
6.2 全局配置逻辑删除(强烈推荐)
在真实项目中,不建议在每个实体上都写 @TableLogic 的 value / delval。
更推荐的方式是:
在
application.yml中做一次全局约定。
6.3 application.yml 全局配置示例
mybatis-plus:
global-config:
db-config:
logic-delete-field: is_deleted
logic-delete-value: 1
logic-not-delete-value: 0
这段配置的含义是:
-
logic-delete-field:- 默认的逻辑删除字段名
-
logic-delete-value:- 表示“已删除”的值
-
logic-not-delete-value:- 表示“未删除”的值
6.4 配置后,实体类可以极度简化
@TableField("is_deleted")
private Integer deleted;
甚至在字段名一致的情况下,可以 完全不写 @TableField:
private Integer isDeleted;
MyBatis-Plus 会自动识别它是逻辑删除字段。
6.5 注解方式 vs 全局配置,如何选择?
| 方式 | 适合场景 |
|---|---|
| @TableLogic | 个别表逻辑删除规则不同 |
| yml 全局配置 | 绝大多数业务项目(推荐) |
6.6 逻辑删除的几个重要认知
-
❌ 不要在 Wrapper 中手动加
is_deleted = 0 -
❌ 不要把逻辑删除当成业务状态
-
✅ 它只是 数据库层面的数据可见性规则
如果你把这一点想清楚,逻辑删除就不会再让你困惑。
七、分页:插件 + 正确使用方式
于
v3.5.9起,PaginationInnerInterceptor已分离出来。如需使用,则需单独引入mybatis-plus-jsqlparser依赖 , 具体请查看 安装 一章。
MybatisPlus自v3.5.9开启将插件相关代码迁移到了mybatis-plus-jsqlparser依赖中,若要使用插件扩展,则需要安装相关依赖。
7.1 安装插件扩展依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.15</version>
</dependency>
<!--
于 v3.5.9 起,PaginationInnerInterceptor 已分离出来。
如需使用,则需单独引入 mybatis-plus-jsqlparser 依赖
-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.15</version>
</dependency>
7.2 新增MybatisPlus配置类,添加分页插件
@Configuration
public class MybatisPlusConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,指定数据库类型:mysql
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
7.3 Mapper 层
- 接口
public interface StudentMapper extends BaseMapper<Student> {
IPage<Student> pageStudentList(Page<Student> page);
IPage<Student> pageStudentListByGender(Page<Student> page, @Param("gender") int gender);
}
- xml:不需要手写
limit
<select id="pageStudentList" resultType="com.framework.demo.entity.Student">
select * from student
</select>
<select id="pageStudentListByGender" resultType="com.framework.demo.entity.Student">
select * from student
<where>
<if test="gender !=null">
gender = #{gender}
</if>
</where>
</select>
7.4 Service层
- 接口
public interface StudentService extends IService<Student> {
/**
* service层:不需要返回IPage对象
* @param page 分页信息
*/
void pageStudentList(Page<Student> page);
void pageStudentListByGender(Page<Student> page, int gender);
}
- 实现类:
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService{
@Autowired
private StudentMapper studentMapper;
@Override
public void pageStudentList(Page<Student> page) {
// 我不需要将mapper结果返回给service,再由service层返回给controller
// 直接借助Page,传递给controller层
// service层,调用mapper查询业务
studentMapper.pageStudentList(page);
}
@Override
public void pageStudentListByGender(Page<Student> page, int gender) {
studentMapper.pageStudentListByGender(page,gender);
}
}
7.5 Controller层:单表查询和自定义SQL查询
@RestController
@Tag(name = "学生模块")
public class StudentController {
@Autowired
private StudentService studentService;
@PostMapping("page1")
public Page<Student> page1(int pageNo, int pageSize) {
Page<Student> page = new Page<>(pageNo, pageSize);
// 单表查询
studentService.page(page);
return page;
}
@PostMapping("page2")
public Page<Student> page2(int pageNo, int pageSize) {
Page<Student> page = new Page<>(pageNo, pageSize);
// 自定义SQL查询、多表查询
studentService.pageStudentList(page);
return page;
}
}
更多推荐



所有评论(0)