欢迎光临
我们一直在努力

MyBatis Plus:快速上手指南

MyBatis Plus:快速上手指南

上一篇我们讲了 MyBatis 的架构和 Mapper 代理机制,知道了 Mapper 接口通过动态代理把方法调用转成 SQL 执行。但在实际开发中,大部分 Mapper 方法做的事情都很基础:按 ID 查、按条件查、插入一条、更新一条、删一条。这些 SQL 写法几乎一模一样,每个 Mapper 都要重复一遍。

如果你写过几个 Mapper,很快就会发现,大部分代码其实都在重复:增删改查、按 ID 查询、分页、条件查询……每个 Mapper 都要重新写一遍。MyBatis Plus 干的就是这件事——把这些重复劳动封装掉。

目录

  • MyBatis Plus 是什么
  • 快速接入 Spring Boot
  • BaseMapper:内置的 CRUD 方法
  • 条件构造器:Wrapper 怎么用
  • Service 层封装
  • 自动填充:时间字段不用手动 set
  • 分页查询
  • 逻辑删除
  • 代码生成器
  • MP 和 MyBatis 的关系
  • 小结

MyBatis Plus 是什么

MyBatis Plus(简称 MP)是 MyBatis 的增强工具,只做增强,不做改变。引入 MP 后,MyBatis 的所有功能照常使用,XML 映射文件、动态 SQL、自定义方法都不受影响。

日常开发最常用的也就五件事:CRUD、条件构造、分页、自动填充和逻辑删除,后面基本都会用到。

快速接入 Spring Boot

Spring Boot 项目引入依赖:

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>

启动类加上 MapperScan:

@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

定义实体类:

@TableName("user") // 指定表名
public class User {

@TableId(type = IdType.AUTO) // 主键自增
private Long id;

private String name;

private Integer age;

private String email;

@TableField(fill = FieldFill.INSERT) // 插入时自动填充
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充
private LocalDateTime updateTime;

// getter / setter 省略
}

定义 Mapper:

public interface UserMapper extends BaseMapper<User> {
// 不需要写任何方法,BaseMapper 已经提供了通用 CRUD
}

到这里就能用了。BaseMapper<User> 提供了 17 个内置方法,直接调用就行。

BaseMapper:内置的 CRUD 方法

BaseMapper 是 MP 提供的核心接口,里面定义了通用的 CRUD 方法。你的 Mapper 只要继承它,就自动拥有这些能力。

日常开发真正经常用的其实就下面这些:

方法说明示例
insert 插入一条记录 mapper.insert(user)
deleteById 按主键删除 mapper.deleteById(1L)
deleteBatchIds 批量删除 mapper.deleteBatchIds(Arrays.asList(1L, 2L))
delete 条件删除 mapper.delete(wrapper)
updateById 按主键更新(null 字段不更新) mapper.updateById(user)
update 条件更新 mapper.update(user, wrapper)
selectById 按主键查询 mapper.selectById(1L)
selectBatchIds 批量查询 mapper.selectBatchIds(Arrays.asList(1L, 2L))
selectOne 查询一条 mapper.selectOne(wrapper)
selectList 条件查询列表 mapper.selectList(wrapper)
selectCount 条件计数 mapper.selectCount(wrapper)
selectPage 分页查询 mapper.selectPage(page, wrapper)

光看方法名没什么感觉,直接看几个最常用的。

插入

User user = new User();
user.setName("张三");
user.setAge(25);
user.setEmail("zhangsan@example.com");

// 返回值是受影响的行数
int rows = userMapper.insert(user);

// 插入后,MP 会自动把生成的主键回填到对象里
System.out.println(user.getId()); // 自增主键的值

MP 默认会把 null 字段也拼进 SQL 里。如果你想跳过 null 字段(比如更新时只更新非空字段),MP 的 updateById 方法已经做了这个处理:为 null 的字段不会出现在 SET 子句里。

查询

// 按 ID 查
User user = userMapper.selectById(1L);

// 批量查
List<User> users = userMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L));

// 查全部
List<User> all = userMapper.selectList(null);

更新

User user = new User();
user.setId(1L);
user.setName("李四"); // 只更新 name

// age 为 null,不会出现在 SET 子句里
int rows = userMapper.updateById(user);
// 实际执行:UPDATE user SET name='李四' WHERE id=1

删除

// 按 ID 删
userMapper.deleteById(1L);

// 批量删
userMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));

这些方法覆盖了大部分单表操作场景。但实际业务中,查询条件往往不只是"等于",还有模糊查询、范围查询、排序、指定返回字段等需求。这就需要条件构造器了。

条件构造器:Wrapper 怎么用

MP 的条件构造器有两种:QueryWrapper 和 LambdaQueryWrapper。它们的作用是用 Java 代码拼出 WHERE 条件。

QueryWrapper

QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("age", 25) // age = 25
.like("name", "张") // name LIKE '%张%'
.between("age", 20, 30) // age BETWEEN 20 AND 30
.orderByDesc("create_time") // ORDER BY create_time DESC
.select("id", "name", "age"); // 只返回指定字段

List<User> users = userMapper.selectList(wrapper);

生成的 SQL:

SELECT id, name, age FROM user
WHERE age = 25 AND name LIKE '%张%' AND age BETWEEN 20 AND 30
ORDER BY create_time DESC

QueryWrapper 的问题是字段名用字符串硬编码,拼错了编译不报错,运行时才发现。LambdaQueryWrapper 解决了这个问题。

LambdaQueryWrapper

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getAge, 25)
.like(User::getName, "张")
.between(User::getAge, 20, 30)
.orderByDesc(User::getCreateTime)
.select(User::getId, User::getName, User::getAge);

List<User> users = userMapper.selectList(wrapper);

效果一样,但字段通过方法引用(User::getName)指定,重构时改了字段名会自动跟着变,编译期就能发现问题。

我现在基本不会再写 QueryWrapper 了,全部换成 LambdaQueryWrapper。虽然代码稍微长一点,但重构的时候不用担心字段名写错,IDE 还能直接跟踪引用,长期维护舒服很多。

常用条件方法

方法SQL 对应示例
eq = .eq(User::getName, "张三")
ne <> .ne(User::getStatus, 0)
like LIKE '%x%' .like(User::getName, "张")
likeLeft LIKE '%x' .likeLeft(User::getName, "三")
likeRight LIKE 'x%' .likeRight(User::getName, "张")
between BETWEEN x AND y .between(User::getAge, 20, 30)
in IN (…) .in(User::getId, Arrays.asList(1, 2, 3))
isNull IS NULL .isNull(User::getEmail)
isNotNull IS NOT NULL .isNotNull(User::getEmail)
orderByDesc ORDER BY x DESC .orderByDesc(User::getCreateTime)
select 指定返回字段 .select(User::getId, User::getName)

条件构造器还支持嵌套条件:

wrapper.eq(User::getStatus, 1)
.and(w -> w.gt(User::getAge, 20).or().like(User::getName, "张"));
// WHERE status = 1 AND (age > 20 OR name LIKE '%张%')

更新条件

条件构造器不只能用于查询,更新也能用:

LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, 1L)
.set(User::getName, "王五")
.set(User::getAge, null); // set null

userMapper.update(null, wrapper);
// UPDATE user SET name='王五', age=NULL WHERE id=1

Service 层封装

MP 还提供了 IService 接口和 ServiceImpl 实现类,把 Service 层的通用 CRUD 也封装了。

// 接口继承 IService
public interface UserService extends IService<User> {
}

// 实现类继承 ServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
}

IService 提供的方法比 BaseMapper 更丰富,常用的:

// 批量插入(自动分批,避免 SQL 过长)
userService.saveBatch(userList, 1000);

// 按条件查询一条
User user = userService.getOne(
new LambdaQueryWrapper<User>().eq(User::getName, "张三")
);

// 按条件计数
long count = userService.count(
new LambdaQueryWrapper<User>().gt(User::getAge, 20)
);

// 批量查询
List<User> users = userService.listByIds(Arrays.asList(1L, 2L));

// 链式查询
List<User> result = userService.lambdaQuery()
.eq(User::getStatus, 1)
.gt(User::getAge, 20)
.orderByDesc(User::getCreateTime)
.list();

lambdaQuery() 和 lambdaUpdate() 返回链式调用的构造器,写起来更简洁。

自动填充:时间字段不用手动 set

表里的 create_time、update_time 这类字段,每次插入或更新都要手动赋值,容易遗漏。MP 支持自动填充。

第一步,实体类加注解:

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

FieldFill 的取值:

值含义
DEFAULT 不自动填充
INSERT 插入时填充
UPDATE 更新时填充
INSERT_UPDATE 插入和更新时都填充

第二步,实现 MetaObjectHandler:

@Component
public class MyMetaObjectHandler 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());
}
}

之后调用 insert 或 updateById 时,createTime 和 updateTime 会自动赋值,不需要手动 set。

不过这里有个容易踩坑的地方:只有通过 MP 提供的方法(如 insert、updateById)执行时,自动填充才会生效。如果自己写 XML 更新 SQL,MetaObjectHandler 是不会执行的。很多人以为配了自动填充就万事大吉,结果自定义 SQL 里的时间字段全是 null,排查半天才发现是这个问题。

分页查询

MP 内置了分页功能,需要先注册分页插件:

@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

分页插件一定要注册,不然 selectPage() 虽然能调用,但不会自动拼 LIMIT,很多人第一次接 MP 都会踩这个坑。

使用方式:

// 第 1 页,每页 10 条
Page<User> page = new Page<>(1, 10);

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1)
.orderByDesc(User::getCreateTime);

// 分页查询
Page<User> result = userMapper.selectPage(page, wrapper);

// 获取结果
List<User> records = result.getRecords(); // 当前页数据
long total = result.getTotal(); // 总记录数
long pages = result.getPages(); // 总页数

MP 的分页是物理分页,生成的 SQL 带 LIMIT:

SELECT * FROM user WHERE status = 1 ORDER BY create_time DESC LIMIT 0, 10

同时会自动执行一条 SELECT COUNT(*) 获取总记录数。

如果不想查总数,可以关闭:

Page<User> page = new Page<>(1, 10, false); // 第三个参数 false 表示不查 count

逻辑删除

逻辑删除不是真正删除数据,而是把记录标记为"已删除"。比如加一个 deleted 字段,0 表示正常,1 表示已删除。

配置文件:

mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段
logic-delete-value: 1 # 已删除的值
logic-not-delete-value: 0 # 未删除的值

实体类加上字段:

@TableLogic
private Integer deleted;

之后调用 deleteById 时,执行的不是 DELETE,而是 UPDATE:

userMapper.deleteById(1L);
// 实际执行:UPDATE user SET deleted=1 WHERE id=1 AND deleted=0

查询时也会自动过滤已删除的记录:

userMapper.selectById(1L);
// 实际执行:SELECT * FROM user WHERE id=1 AND deleted=0

逻辑删除对开发者是透明的,查询、更新、删除的方法调用方式不变,MP 自动在 SQL 里加上 deleted=0 条件。

但有个坑需要提前知道:逻辑删除只是帮你把 DELETE 改成了 UPDATE,数据库里的数据其实一直都在。所以涉及唯一索引时需要额外注意,否则可能出现"删了却不能重新插入"的问题。比如用户名加了唯一索引,逻辑删除"张三"后再注册一个"张三",数据库会报唯一键冲突,因为那条"已删除"的记录还在。

代码生成器

MP 提供了代码生成器,根据数据库表自动生成 Entity、Mapper、Service、Controller。

FastAutoGenerator.create(
"jdbc:mysql://localhost:3306/test", "root", "password")
.globalConfig(builder -> {
builder.author("developer") // 作者
.outputDir("src/main/java") // 输出目录
.disableOpenDir(); // 生成后不自动打开目录
})
.packageConfig(builder -> {
builder.parent("com.example") // 父包名
.moduleName("demo") // 模块名
.mapper("mapper") // Mapper 包名
.service("service") // Service 包名
.controller("controller"); // Controller 包名
})
.strategyConfig(builder -> {
builder.addInclude("user", "order") // 要生成的表
.entityBuilder()
.enableLombok() // Entity 用 Lombok
.serviceBuilder()
.formatServiceFileName("%sService");
})
.templateEngine(new FreemarkerTemplateEngine())
.execute();

生成的目录结构:

src/main/java/com/example/demo/
├── entity/
│ └── User.java
├── mapper/
│ └── UserMapper.java
├── service/
│ ├── UserService.java
│ └── impl/
│ └── UserServiceImpl.java
└── controller/
└── UserController.java

生成的代码可以直接跑,但通常需要根据业务需求做调整,比如加自定义方法、改查询逻辑等。

MP 和 MyBatis 的关系

MP 是 MyBatis 的增强,不是替代。它们的关系:

维度MyBatisMyBatis Plus
单表 CRUD 手写 XML 或注解 BaseMapper 内置方法
条件查询 XML 里写 <where> <if> Wrapper 链式调用
分页 需要第三方插件(PageHelper) 内置分页插件
逻辑删除 手动实现 注解 + 配置即可
自动填充 手动实现 MetaObjectHandler
复杂查询 XML 动态 SQL 同样支持 XML 动态 SQL
多表联查 XML 写 resultMap 和 JOIN 同样用 XML 写

小结

如果让我总结 MyBatis Plus,我觉得它最大的价值不是"功能多",而是把那些机械重复的 CRUD 全部接管了,让开发者把精力放到真正的业务逻辑上。但是,多表联查、复杂 SQL这些还是老老实实写 MyBatis 更舒服。项目里一般也是两者一起用:简单 CRUD 交给 MP,复杂查询继续写 XML,这也是大多数开发者比较常见的用法。

赞(0)
未经允许不得转载:171主机测评 » MyBatis Plus:快速上手指南
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址