MyBatis-Plus 单表开发与增强
本文介绍 MyBatis-Plus 在 Java 后端项目中的单表开发与增强能力,覆盖快速入门、BaseMapper、通用 CRUD、Wrapper 条件构造器、IService、分页插件、自动填充、逻辑删除、代码生成器和多表查询边界。文章帮助读者理解 MyBatis-Plus 如何提升常规数据库开发效率。
第一章 为什么需要 MyBatis-Plus?
在了解 MyBatis-Plus 之前,我们先回想一下使用原生 MyBatis 开发时的痛点:
- 大量重复的 SQL:每张表的基础 CRUD(增删改查)都需要手写一遍 XML 或者注解,比如
insert、selectById、update,代码极其冗余。 - 实体类属性映射繁琐:如果数据库字段和 Java 实体类属性命名不一致,还需要写大量的
@Result或者<resultMap>映射。 - 动态 SQL 编写易错:使用
<if>、<where>拼接条件容易出现语法错误,而且不够面向对象。
MyBatis-Plus(简称 MP) 就是为了解决这些痛点而生的。它的宗旨是:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。 使用它,你甚至不需要写一行 SQL 就能完成单表的大部分操作。
第二章 快速入门
2.1 引入依赖
在 Spring Boot 项目中,只需要引入 mybatis-plus-boot-starter。注意:不要同时引入原生的 mybatis-spring-boot-starter,MP 已经包含了它。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version> <!-- 请使用最新稳定版 -->
</dependency>
2.2 核心体验:继承 BaseMapper
我们定义一个 Mapper 接口,直接继承 MP 提供的 BaseMapper<T> 接口,其中 T 是实体类。
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 已经自动拥有了所有的基础 CRUD 方法!
}
测试一下:
@Autowired
private UserMapper userMapper;
@Test
public void testSelect() {
// 根据 ID 查询
User user = userMapper.selectById(1L);
System.out.println(user);
// 查询所有数据
List<User> users = userMapper.selectList(null);
}
2.3 核心注解
如果实体类与数据库表结构没有完美对应,MP 提供了几个核心注解来解决映射问题:
@TableName:指定实体类对应的数据库表名。如果类名叫User,表名叫t_user,就需要加@TableName("t_user")。@TableId:指定实体类的哪个属性是主键。可以指定生成策略:IdType.AUTO:数据库自增(数据库必须设置了自增)。IdType.ASSIGN_ID:雪花算法生成分布式唯一 ID(默认策略,适合大厂和分库分表)。@TableField:解决普通字段的映射问题。- 属性名和字段名不一致:
@TableField("db_col_name")。 - 实体类中有,但数据库没有的字段(排除非表字段):
@TableField(exist = false)。
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("user_name")
private String name;
private Integer age;
@TableField(exist = false)
private String token; // 数据库中没有该字段
}
第三章 通用 CRUD 操作
继承 BaseMapper 后,你不仅拥有了 selectById,还有一系列强大的方法:
- 新增:
int insert(T entity); - 删除:
- 根据 ID 删除:
int deleteById(Serializable id); - 根据 ID 批量删除:
int deleteBatchIds(Collection<?> idList); - 根据条件删除:
int delete(Wrapper<T> queryWrapper); - 修改:
- 根据 ID 修改:
int updateById(T entity);(只会更新不为 null 的字段) - 查询:
- 根据 ID 批量查询:
List<T> selectBatchIds(Collection<?> idList); - 根据条件查询一条:
T selectOne(Wrapper<T> queryWrapper); - 根据条件查询列表:
List<T> selectList(Wrapper<T> queryWrapper);
第四章 核心武器:条件构造器 (Wrapper)
如何不写 SQL 也能实现类似 WHERE name = '张三' AND age > 18 的查询?MP 提供了条件构造器 Wrapper。
4.1 QueryWrapper
传统的 Wrapper,通过传入数据库字段名(字符串)来构建条件。
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", "张三") // name = '张三'
.gt("age", 18) // age > 18
.like("email", "test"); // email LIKE '%test%'
List<User> list = userMapper.selectList(wrapper);
缺点:硬编码字段名 "user_name",一旦数据库改名或者拼写错误,编译期不会报错,运行时才会抛异常。
4.2 LambdaQueryWrapper(强烈推荐)
为了解决硬编码问题,MP 引入了 Lambda 条件构造器,通过方法引用来指定字段,类型安全,防误写。
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// User::getName 直接获取了属性,MP 底层会自动转换为列名
wrapper.eq(User::getName, "张三")
.gt(User::getAge, 18)
.like(User::getEmail, "test")
.orderByDesc(User::getCreateTime); // ORDER BY create_time DESC
List<User> list = userMapper.selectList(wrapper);
4.3 常见条件方法速查
eq:等于=ne:不等于<>gt:大于>ge:大于等于>=lt:小于<le:小于等于<=between:BETWEEN 值1 AND 值2like:LIKE '%值%'likeLeft:LIKE '%值'likeRight:LIKE '值%'isNull/isNotNull:字段 IS NULL / IS NOT NULLin/notIn:IN (值1, 值2...)
第五章 IService 接口增强
除了 Mapper 层的 BaseMapper,MP 还提供了 Service 层的通用接口 IService 和它的实现类 ServiceImpl。
5.1 为什么要用 IService?
BaseMapper 只能单条插入,如果我有 1 万条数据要入库怎么办?循环调 insert 性能极差。IService 提供了 批量插入 等更高级的功能。
5.2 如何使用?
1. 定义 Service 接口,继承 IService<T>
public interface UserService extends IService<User> {
// 可以在这里写自定义业务方法
}
2. 编写实现类,继承 ServiceImpl<M, T> 并实现自定义接口
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
5.3 IService 带来的超能力
@Autowired
private UserService userService;
// 1. 批量保存 (底层会自动分批处理,性能极高)
List<User> users = buildUserList();
userService.saveBatch(users);
// 2. 批量保存或更新 (有 ID 且存在则更新,否则新增)
userService.saveOrUpdateBatch(users);
// 3. 链式查询 (更加优雅)
List<User> result = userService.lambdaQuery()
.like(User::getName, "李")
.ge(User::getAge, 20)
.list();
第六章 进阶高级特性
6.1 分页插件
分页是极高频的需求。MP 提供了拦截器插件,配置后即可使用 Page 对象进行物理分页(底层自动拼接 LIMIT)。
配置拦截器:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,指定数据库类型为 MySQL
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
使用分页:
// 第 1 页,每页 10 条
Page<User> page = new Page<>(1, 10);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getAge, 18);
// 执行分页查询,结果会塞回 page 对象中
Page<User> resultPage = userMapper.selectPage(page, wrapper);
System.out.println("总条数: " + resultPage.getTotal());
System.out.println("总页数: " + resultPage.getPages());
System.out.println("当前页数据: " + resultPage.getRecords());
6.2 自动填充功能
很多表都有 create_time 和 update_time,每次 insert 和 update 手动设值很麻烦。
- 实体类加注解声明填充策略:
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
- 编写
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());
}
}
6.3 逻辑删除
真实项目中,数据通常不真删(物理删除),而是把一个状态字段标记为已删除(逻辑删除)。 MP 原生支持逻辑删除:
在实体类对应的字段加上 @TableLogic:
@TableLogic
private Integer deleted; // 默认 0 未删除,1 已删除
加上之后:
- 调用
deleteById时,底层自动变成:UPDATE user SET deleted = 1 WHERE id = ? - 调用
selectList等查询方法时,底层自动追加:WHERE deleted = 0。
完全对业务层透明,极其省心!
第七章 代码生成器 (Generator)
面对几十张表,手动建实体类、Mapper、Service 也是体力活。MyBatis-Plus 提供了代码生成器(MyBatis-Plus Generator)或者第三方 IDEA 插件(如 MyBatisX),连上数据库,一键生成所有的基础代码。
7.1 使用 MyBatisX 插件(推荐)
在现代开发中,通常推荐直接使用 IDEA 的 MyBatisX 插件,图形化界面,鼠标点两下即可生成整套模板代码。只需在 IDEA 插件市场搜索 MyBatisX 安装,然后在 Database 工具窗口中右键表名,选择 MybatisX-Generator 即可。
7.2 使用代码生成器依赖
如果你希望通过代码来灵活控制生成逻辑,可以引入官方的代码生成器依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
<!-- 模板引擎,推荐 freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
快速生成代码示例:
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/mydb", "root", "123456")
.globalConfig(builder -> {
builder.author("YourName") // 设置作者
.outputDir("D://output"); // 输出目录
})
.packageConfig(builder -> {
builder.parent("com.example.demo") // 设置父包名
.moduleName("system"); // 设置父包模块名
})
.strategyConfig(builder -> {
builder.addInclude("t_user", "t_order") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
第八章 多表查询解决方案
MyBatis-Plus 的核心设计理念是“专注于单表操作”。官方提供的 Wrapper 条件构造器并不直接支持多表联查(如 JOIN)。面对多表查询,通常有以下几种解决方案:
8.1 自定义 XML 编写 SQL(最正宗、最推荐)
对于复杂的报表、多表关联,最稳妥和好维护的方式还是回归 MyBatis 的本源,写 XML。
Mapper 接口:
public interface UserMapper extends BaseMapper<User> {
List<UserVO> getUserAndOrder(@Param("userId") Long userId);
}
XML 映射文件:
<select id="getUserAndOrder" resultType="com.example.demo.vo.UserVO">
SELECT u.id, u.user_name, o.order_no, o.amount
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
8.2 使用注解方式
如果 SQL 相对简单,可以直接写在 Mapper 接口的方法注解上,省去 XML 文件:
@Select("SELECT u.id, u.user_name, o.order_no, o.amount FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id WHERE u.id = #{userId}")
List<UserVO> getUserAndOrder(@Param("userId") Long userId);
8.3 使用 MyBatis-Plus-Join (MPJ) 插件
如果你非常喜欢 MP 的 Wrapper 风格,并且不想写 SQL,可以引入社区维护的扩展包 MyBatis-Plus-Join。
引入依赖:
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join</artifactId>
<version>1.4.5</version> <!-- 请使用最新版 -->
</dependency>
让 Mapper 继承 MPJBaseMapper:
public interface UserMapper extends MPJBaseMapper<User> {
}
使用连表 Wrapper:
MPJLambdaWrapper<User> wrapper = new MPJLambdaWrapper<User>()
.selectAll(User.class)
.select(Order::getOrderNo, Order::getAmount)
.leftJoin(Order.class, Order::getUserId, User::getId)
.eq(User::getId, 1L);
List<UserVO> list = userMapper.selectJoinList(UserVO.class, wrapper);
这种方式适合不太复杂的 JOIN 查询,能够保持代码的全 Java 化和类型安全。
第九章 总结
MyBatis-Plus 将我们从无聊的单表 CRUD 和冗长的 XML 映射中解放了出来:
- 用
BaseMapper搞定单表增删改查。 - 用
LambdaQueryWrapper优雅、安全地拼接查询条件。 - 用
IService轻松实现批量操作。 - 借助 分页插件、自动填充、逻辑删除 解决高频通用需求。
建议: 虽然 MP 强大,但它的定位是辅助。对于多表联查、极其复杂的报表 SQL,不要强行用 Wrapper 拼接,那会导致代码可读性极差。复杂的 SQL,老老实实写在 Mapper 的 XML 文件里,这才是最正宗、最容易维护的做法!
配合阅读: MyBatis-Plus 的批量操作、多表写入同样依赖 Spring 事务。请参阅 《Java 数据库事务实战》,了解 @Transactional 与 BaseMapper / ServiceImpl 如何共用同一事务。