在 Java 的世界里,数据持久化是任何企业级应用的基石。MyBatis 以其轻量、灵活以及对 SQL 的完全掌控而备受开发者喜爱。然而,随着项目复杂度的增加,开发者们发现自己陷入了编写大量重复性 CRUD(创建、读取、更新、删除)代码的泥潭,这不仅枯燥,也容易出错。
为了将开发者从这些重复劳动中解放出来,MyBatis-Plus (简称 MP) 应运而生。它并非要取代 MyBatis,而是作为其最强大的“增强套件”,在完全兼容 MyBatis 的前提下,提供了海量的便捷功能,其核心目标只有一个:为简化开发、提高效率而生。
本指南将带您从零开始,一步步探索并掌握这个强大的工具。
第一章:初识 MyBatis-Plus
1.1 为什么选择 MyBatis-Plus?
在引入任何新技术之前,我们都应该问“为什么”。选择 MP 的理由非常充分:
- 无侵入性:MP 只是 MyBatis 的一个增强工具,不会对您现有的 MyBatis 项目造成任何影响。您可以随时引入,并与原生功能混合使用。
- 代码极简:对于单表的增删改查,您不再需要编写任何 XML SQL 语句。MP 已经通过内置的
BaseMapper
为您准备好了一切。 - 功能强大:除了基础 CRUD,MP 还内置了分页、逻辑删除、乐观锁、自动填充、性能分析等一系列企业级开发所需的高级功能。
- 类型安全:其独创的
LambdaQueryWrapper
让您告别手写字段名的时代,通过方法引用构建查询条件,安全且易于重构。 - 生态完善:拥有活跃的社区和清晰的官方文档,遇到问题可以快速找到解决方案。
1.2 核心理念
MP 的设计哲学可以总结为“约定优于配置”。它假设了一些通用开发场景(例如,主键名为 id
,逻辑删除字段为 deleted
),并提供了默认实现。您只需遵循这些约定,就能以极少的代码量完成大量工作。当然,所有约定都是可以自定义配置的。
第二章:扬帆起航 - 环境搭建
让我们通过一个标准的 Spring Boot 项目来快速搭建 MyBatis-Plus 环境。
2.1 添加项目依赖
在您的 Maven 项目的 pom.xml
文件中,加入 mybatis-plus-boot-starter
依赖。它会自动管理 MyBatis 和 JDBC 的相关依赖。
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.12</version>
</dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
2.2 配置数据源
在 Spring Boot 的核心配置文件 application.yml
中,配置您的数据库连接信息。
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/your_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: your_passworddriver-class-name: com.mysql.cj.jdbc.Driver
2.3 扫描 Mapper 接口
最后,在您的 Spring Boot 启动类上,使用 @MapperScan
注解来告诉 MP 您的 Mapper 接口存放在哪个包下。
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
@MapperScan("com.yourcompany.project.mapper") // 替换为您的 Mapper 包路径
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}
至此,您的项目已经成功集成了 MyBatis-Plus!
第三章:核心构件 - 实体、Mapper 与 Service
MP 的代码主要围绕这三个核心组件展开。
3.1 实体类 (Entity) 与注解
实体类是数据库表在 Java 代码中的映射。MP 提供了一系列注解来精确描述这种映射关系。
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;@Data
@TableName("sys_user") // 映射数据库中的 `sys_user` 表
public class User {// @TableId:定义主键// value = "user_id": 数据库主键字段名为 `user_id`// type = IdType.AUTO: 主键策略为数据库自增@TableId(value = "user_id", type = IdType.AUTO)private Long id;// @TableField:定义普通字段// value = "user_name": 数据库字段名为 `user_name`// (如果属性名符合驼峰转下划线规则,如 `userName` -> `user_name`,此注解可省略)@TableField("user_name")private String name;private Integer age;private String email;// @TableField(exist = false):表示该字段在数据库表中不存在,仅为业务逻辑需要@TableField(exist = false)private String gender;
}
常用主键策略 (IdType
)
AUTO
: 数据库 ID 自增。ASSIGN_ID
: 雪花算法生成分布式唯一 ID (长整型或字符串)。INPUT
: 用户手动输入。
3.2 Mapper 接口
您只需让您的 Mapper 接口继承 BaseMapper<T>
,便可瞬间拥有强大的 CRUD 能力。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yourcompany.project.entity.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper extends BaseMapper<User> {// 没错,就是这么空!// 所有的基础增、删、改、查、分页查询方法都已经由 BaseMapper 提供了。// 如果您有复杂的多表查询需求,仍然可以像原生 MyBatis 一样在这里定义方法,并编写 XML。
}
3.3 Service 层(最佳实践)
为了更好地进行业务逻辑封装和事务控制,官方强烈推荐使用 Service 层。MP 也为此提供了便利。
-
IService 接口
import com.baomidou.mybatisplus.extension.service.IService; import com.yourcompany.project.entity.User;public interface UserService extends IService<User> {// 定义您自己的业务方法 }
-
ServiceImpl 实现
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yourcompany.project.entity.User; import com.yourcompany.project.mapper.UserMapper; import org.springframework.stereotype.Service;@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {// ServiceImpl 已经帮您注入了 UserMapper,并实现了 IService 的所有方法。// 您可以直接调用如 save(), list(), page() 等便捷方法。 }
第四章:灵魂所在 - 条件构造器 Wrapper
当简单的 CRUD 无法满足复杂的查询需求时,Wrapper
便闪亮登场。它允许您以编程的方式,安全、直观地构建复杂的 WHERE
查询条件。
核心建议:始终使用 LambdaQueryWrapper
,它能提供编译期类型安全检查,让您的代码更健壮。
4.1 核心查询方法详解
方法 | 含义 | 生成 SQL 示例 |
---|---|---|
.eq(User::getName, "张三") |
等于 (Equal) | name = '张三' |
.ne(User::getStatus, 0) |
不等于 (Not Equal) | status <> 0 |
.gt(User::getAge, 18) |
大于 (Greater Than) | age > 18 |
.ge(User::getAge, 18) |
大于等于 (Greater Equal) | age >= 18 |
.lt(User::getAge, 60) |
小于 (Less Than) | age < 60 |
.le(User::getAge, 60) |
小于等于 (Less Equal) | age <= 60 |
.between(User::getAge, 20, 30) |
在...之间 | age BETWEEN 20 AND 30 |
.like(User::getName, "王") |
包含 (LIKE '%王%') | name LIKE '%王%' |
.likeLeft(User::getEmail, "@qq.com") |
左模糊 (LIKE '%@qq.com') | email LIKE '%@qq.com' |
.likeRight(User::getName, "李") |
右模糊 (LIKE '李%') | name LIKE '李%' |
.isNull(User::getManagerId) |
为 NULL | manager_id IS NULL |
.isNotNull(User::getEmail) |
不为 NULL | email IS NOT NULL |
.in(User::getId, 1, 2, 3) |
在集合中 | id IN (1, 2, 3) |
.orderByAsc(User::getAge) |
升序排序 | ORDER BY age ASC |
.orderByDesc(User::getCreateTime) |
降序排序 | ORDER BY create_time DESC |
4.2 实战演练
需求:查询名字以 "李" 开头、年龄在 20 到 40 岁之间、且邮箱不为空的用户,结果按年龄降序排列。
@Autowired
private UserService userService;public List<User> findUsers() {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.likeRight(User::getName, "李").between(User::getAge, 20, 40).isNotNull(User::getEmail).orderByDesc(User::getAge);return userService.list(wrapper);
}
这段代码清晰地表达了业务逻辑,并且完全避免了手写 SQL 可能带来的注入风险和拼写错误。
4.3 更新专用:UpdateWrapper
当您需要更新部分字段时,可以使用 LambdaUpdateWrapper
。
需求:将所有名字为 "张三" 的用户的年龄更新为 30 岁。
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(User::getName, "张三") // WHERE 条件.set(User::getAge, 30); // SET 子句boolean success = userService.update(updateWrapper);
生成 SQL: UPDATE sys_user SET age = 30 WHERE user_name = '张三'
第五章:企业级高级特性
要启用以下大部分功能,您需要配置一个 MybatisPlusInterceptor
拦截器。
@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 1. 添加分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 2. 添加乐观锁插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}
}
5.1 物理分页
配置分页插件后,分页查询变得异常简单。
Java
// 构建分页参数:查询第 1 页,每页 10 条
Page<User> page = new Page<>(1, 10);// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(User::getAge, 18);// 执行分页查询
Page<User> resultPage = userService.page(page, wrapper);System.out.println("总记录数: " + resultPage.getTotal());
System.out.println("当前页数据: " + resultPage.getRecords());
5.2 逻辑删除
在实际业务中,我们通常不会物理删除数据库中的记录,以便于数据审计、追踪或恢复。这种只通过一个字段来标记数据为“已删除”状态的操作,被称为“逻辑删除”或“软删除”。MyBatis-Plus 对此提供了极其优雅和透明的支持。
第一步:在实体类中指定逻辑删除字段
要启用逻辑删除,首先您需要在实体类中代表“删除状态”的字段上添加 @TableLogic
注解。MP 会识别这个注解,并对相关操作进行特殊处理。
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;@Data
public class User {private Long id;private String name;// @TableLogic 是启用逻辑删除的关键@TableLogicprivate Integer deleted;
}
第二步:在 application.yml
中配置全局默认值(可选但推荐)
为了让逻辑删除的行为更加清晰和统一,推荐在 application.yml
中配置全局的“已删除”和“未删除”状态值。
mybatis-plus:global-config:db-config:# 逻辑删除字段名logic-delete-field: deleted# 逻辑已删除值(默认为 1)logic-delete-value: 1# 逻辑未删除值(默认为 0)logic-not-delete-value: 0
logic-delete-value: 1
:定义了当执行删除操作时,@TableLogic
标记的字段值会被更新为1
。logic-not-delete-value: 0
:定义了当执行查询操作时,会自动附加条件,只查询deleted
字段值为0
的记录。
功能效果演示
完成上述配置后,逻辑删除对您的业务代码是完全透明的,您仍然像以前一样调用方法,但底层的 SQL 已经发生了改变。
1. 执行删除操作
您的业务代码:
// 表面上是调用删除方法
userService.removeById(101L);
MyBatis-Plus 实际执行的 SQL:
UPDATE user SET deleted=1 WHERE id=101 AND deleted=0;
解析: DELETE
操作被自动拦截并转换为 UPDATE
操作。它会将 deleted
字段的值从 0
(未删除)更新为 1
(已删除),并且 WHERE
子句中自动加入了 deleted=0
,确保了操作的幂等性。
2. 执行查询操作
您的业务代码:
// 表面上是调用一个普通的查询方法
User user = userService.getById(101L);
MyBatis-Plus 实际执行的 SQL:
SELECT id, name, deleted FROM user WHERE id=101 AND deleted=0;
解析: 任何查询(包括 Wrapper
构建的复杂查询)都会被自动在 WHERE
子句末尾追加 AND deleted=0
条件。这保证了您的业务代码在任何时候获取到的都是“有效”数据,无需手动在每个查询中添加此条件,极大简化了代码并降低了出错的风险。
5.3 乐观锁
用于解决高并发下的数据更新冲突问题。
-
数据库表增加
version
字段 (通常是INT
或BIGINT
)。 -
在实体类对应字段上添加
@Version
注解。@Version private Integer version;
-
配置
OptimisticLockerInnerInterceptor
拦截器。
当您执行更新时,MP 会自动检查并递增 version
字段,确保您是基于最新数据进行的修改。
5.4 自动填充公共字段
在大多数业务表中,create_time
和 update_time
这两个字段几乎是标配。我们希望在新增记录时自动填充创建和更新时间,在更新记录时自动更新时间,而无需在业务代码中手动设置。MP 的自动填充功能完美解决了这个问题。
第一步:实现 MetaObjectHandler 接口
创建一个类并实现 MetaObjectHandler
接口,重写 insertFill
和 updateFill
方法。通过 @Component
注解将其注册为 Spring Bean。
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {// 插入时,同时填充创建时间和更新时间this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());}@Overridepublic void updateFill(MetaObject metaObject) {// 更新时,只填充更新时间this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());}
}
第二步:在实体类字段上添加注解
在需要自动填充的实体类字段上,使用 @TableField
注解并指定 fill
策略。
@Data
public class User {// ... 其他字段// 字段添加填充内容@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
FieldFill.INSERT
: 只在插入时填充。FieldFill.UPDATE
: 只在更新时填充。FieldFill.INSERT_UPDATE
: 插入和更新时都填充。
完成这两步后,您在执行 save(user)
或 updateById(user)
等操作时,MP 将会自动调用 MyMetaObjectHandler
中的逻辑,为相应字段赋值。
第六章:效率神器 - 代码生成器
当开始一个新项目或新模块时,需要创建大量结构相似的 Entity、Mapper、Service、Controller 代码。MyBatis-Plus 提供了强大的代码生成器(AutoGenerator),可以连接数据库,一键生成这些基础代码,极大提升开发效率。
第一步:添加生成器依赖
在 pom.xml
中添加 mybatis-plus-generator
的依赖。
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.7</version>
</dependency>
<dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>2.3</version>
</dependency>
第二步:编写并执行生成代码
创建一个简单的 main
方法来配置和执行代码生成。
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
import java.util.Collections;public class CodeGenerator {public static void main(String[] args) {FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/your_db", "root", "your_password").globalConfig(builder -> {builder.author("YourName") // 设置作者.outputDir(System.getProperty("user.dir") + "/src/main/java"); // 指定输出目录}).packageConfig(builder -> {builder.parent("com.yourcompany.project") // 设置父包名.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper")); // 设置 XML 生成路径}).strategyConfig(builder -> {builder.addInclude("sys_user", "sys_role") // 设置需要生成的表名.addTablePrefix("sys_"); // 设置过滤表前缀}).templateEngine(new VelocityTemplateEngine()) // 使用 Velocity 引擎模板.execute();}
}
配置解析:
FastAutoGenerator.create()
: 配置数据库连接信息。.globalConfig()
: 配置全局选项,如作者、输出路径等。.packageConfig()
: 配置包名信息,以及各类文件(如XML)的单独输出路径。.strategyConfig()
: 策略配置,是最核心的配置。可以指定要生成的表、要忽略的表前缀、Entity/Controller/Service/Mapper 的各种生成策略等。
运行这个 main
方法,您项目对应的包下就会立即生成 sys_user
和 sys_role
两张表的所有后端基础代码。
第七章:灵活性 - 与原生 XML 共存
当遇到极其复杂的报表查询或多表 JOIN
时,Wrapper
可能不够用。此时,您可以无缝地回归 MyBatis 的原生 XML 方式。
-
在
UserMapper
接口中定义方法:List<UserVo> getUserDetailList(@Param("status") Integer status);
-
在
resources/mapper/UserMapper.xml
中编写 SQL:<select id="getUserDetailList" resultType="com.yourcompany.project.vo.UserVo">SELECT u.user_name, d.dept_nameFROM sys_user uLEFT JOIN sys_dept d ON u.dept_id = d.idWHERE u.status = #{status} </select>
MP 的通用方法和您的自定义 XML 方法可以和谐共存,提供了极致的灵活性。
结语
MyBatis-Plus 通过一系列精心设计的功能,极大地简化了 Java 持久层的开发,将开发者从繁重的 CRUD 中解放出来。它易于上手,功能强大,同时又不失 MyBatis 原生的灵活性。
最佳实践回顾:
- 优先使用 Service 层:将业务逻辑封装在
ServiceImpl
中,而不是直接操作Mapper
。 - 拥抱 LambdaWrapper:为了代码的类型安全和可维护性,始终使用
LambdaQueryWrapper
和LambdaUpdateWrapper
。 - 善用高级特性:针对分页、逻辑删除、并发控制等常见场景,积极使用 MP 提供的成熟解决方案。
- 启动项目时利用代码生成器:在新项目或模块开始时,首先使用代码生成器构建基础骨架。
无论您是正在开始一个新项目,还是希望优化现有项目的开发效率,MyBatis-Plus 都绝对是您技术栈中值得拥有的利器。掌握它,意味着您可以用更少的代码,实现更强大的功能,从而更专注于创造性的业务逻辑开发。