1. 为什么选择MyBatis-Plus?
作为一个常年与数据库打交道的Java开发者,我经历过JDBC的繁琐、Hibernate的"太重"、MyBatis的XML地狱。直到三年前项目组强制推行MyBatis-Plus(以下简称MP),我才发现原来ORM工具可以既保持MyBatis的灵活性,又能享受JPA式的开发效率。
MP的核心价值在于:它是对MyBatis的增强而非替换。这意味着:
- 你仍然可以继续使用原有的MyBatis功能
- 所有MyBatis的配置和插件仍然有效
- 在需要快速开发时,MP的CRUD接口能节省80%的基础代码量
- 在复杂查询场景下,你随时可以退回到原生MyBatis模式
实际项目中,我通常用MP处理90%的单表操作,剩下10%的复杂联表查询仍然使用XML映射文件。这种"双模式"开发体验,是其他ORM框架难以提供的。
2. 环境搭建与基础配置
2.1 依赖引入的坑点
以Maven项目为例,最常见的依赖配置如下:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
但这里有几个新手容易踩的坑:
- 版本冲突:如果项目已有MyBatis依赖,需要确保版本兼容。我建议使用MP的starter,它会自动管理MyBatis相关依赖
- 分页插件缺失:MP的分页功能需要额外配置插件(后文会详述)
- 多数据源问题:在Spring Boot中,默认数据源配置可能不生效,需要特别处理
2.2 配置文件的正确姿势
典型的application.yml配置示例:
yaml复制mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志
map-underscore-to-camel-case: true # 自动驼峰转换
global-config:
db-config:
id-type: auto # 主键自增
logic-delete-field: deleted # 逻辑删除字段
logic-not-delete-value: 0
logic-delete-value: 1
特别提醒几个关键配置:
map-underscore-to-camel-case:建议始终开启,避免字段映射问题log-impl:开发环境建议使用StdOutImpl,生产环境可改为Slf4jImpl- 逻辑删除配置:这是MP的特色功能,后文会详细演示
3. 实体类与Mapper的魔法
3.1 注解驱动的实体类
MP通过注解极大简化了实体类定义。一个典型的用户实体类示例:
java复制@Data
@TableName("sys_user") // 指定表名
public class User {
@TableId(type = IdType.AUTO) // 自增主键
private Long id;
private String username;
@TableField("real_name") // 字段映射
private String realName;
@TableField(fill = FieldFill.INSERT) // 自动填充
private LocalDateTime createTime;
@TableLogic // 逻辑删除标记
private Integer deleted;
}
关键注解解析:
@TableName:解决Java类名与表名的映射问题@TableId:主键策略配置(自增、UUID、雪花ID等)@TableField:处理字段特殊需求(如映射、自动填充)@TableLogic:实现逻辑删除而非物理删除
3.2 Mapper接口的智能继承
MP最强大的特性之一就是BaseMapper:
java复制public interface UserMapper extends BaseMapper<User> {
// 无需任何方法,就已具备基础CRUD能力
}
这个简单的接口通过继承,自动拥有了以下方法:
- insert/updateById/deleteById 等基础操作
- selectBatchIds 批量查询
- selectByMap 条件查询
- selectCount 统计查询
在我的电商项目中,仅凭BaseMapper就覆盖了60%以上的数据库操作需求。对于简单业务,甚至可以不写任何SQL。
4. 条件构造器的实战技巧
4.1 QueryWrapper的基本用法
条件构造器是MP的核心竞争力。看一个复杂查询示例:
java复制QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "username", "create_time")
.like("username", "张")
.between("age", 18, 30)
.orderByDesc("create_time")
.last("limit 10");
List<User> users = userMapper.selectList(wrapper);
这段代码生成的SQL是:
sql复制SELECT id,username,create_time
FROM sys_user
WHERE username LIKE '%张%'
AND age BETWEEN 18 AND 30
ORDER BY create_time DESC
LIMIT 10
4.2 Lambda表达式的类型安全写法
更推荐使用LambdaQueryWrapper避免字段名硬编码:
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getUsername, User::getCreateTime)
.like(User::getUsername, "张")
.between(User::getAge, 18, 30)
.orderByDesc(User::getCreateTime)
.last("limit 10");
这种写法的优势:
- 编译时检查字段名正确性
- IDE支持代码自动补全
- 重构时字段名修改会自动同步
4.3 复杂查询的链式调用
MP支持流畅的链式调用,可以这样构建复杂查询:
java复制List<User> users = userMapper.selectList(new QueryWrapper<User>()
.select("id", "username")
.nested(w -> w.like("username", "王").or().like("username", "张"))
.eq("status", 1)
.apply("date_format(create_time,'%Y-%m')={0}", "2023-08")
.inSql("dept_id", "select id from sys_dept where status=1")
.having("count(*)>0"));
5. 分页查询的完整方案
5.1 分页插件配置
MP的分页需要先配置插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
5.2 分页查询实践
基本分页操作:
java复制Page<User> page = new Page<>(1, 10); // 当前页,每页大小
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("status", 1);
IPage<User> userPage = userMapper.selectPage(page, wrapper);
System.out.println("总记录数:" + userPage.getTotal());
System.out.println("总页数:" + userPage.getPages());
userPage.getRecords().forEach(System.out::println);
5.3 自定义SQL分页
对于复杂SQL,可以在Mapper中定义:
java复制@Select("select * from sys_user where status=#{status}")
IPage<User> selectByStatus(Page<User> page, @Param("status") Integer status);
6. 高级特性实战
6.1 自动填充功能
实现MetaObjectHandler处理自动填充:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUsername());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUsername());
}
}
6.2 逻辑删除实现
配置逻辑删除后,所有删除操作自动变为更新:
java复制userMapper.deleteById(1L);
// 实际执行:UPDATE sys_user SET deleted=1 WHERE id=1 AND deleted=0
查询时会自动过滤已删除数据:
java复制userMapper.selectList(null);
// 实际执行:SELECT * FROM sys_user WHERE deleted=0
6.3 乐观锁实现
添加@Version注解:
java复制@Version
private Integer version;
更新时自动带上版本条件:
java复制User user = userMapper.selectById(1L);
user.setUsername("newname");
userMapper.updateById(user);
// 执行:UPDATE sys_user SET username='newname', version=version+1
// WHERE id=1 AND version=原始version
7. 性能优化与生产建议
7.1 SQL注入防护
虽然MP的QueryWrapper能有效防止SQL注入,但仍需注意:
- 避免直接拼接SQL片段
- 谨慎使用
last()方法 - 对用户输入参数做好校验
7.2 批量操作优化
使用MP的批量操作方法:
java复制List<User> userList = new ArrayList<>();
// 添加数据...
// 批量插入
userService.saveBatch(userList);
// 批量更新
userService.updateBatchById(userList);
实测对比:批量插入1万条数据,saveBatch比循环insert快15倍以上
7.3 多数据源配置
Spring Boot多数据源配置要点:
java复制@Configuration
@MapperScan(basePackages = "com.xxx.mapper.db1", sqlSessionFactoryRef = "db1SqlSessionFactory")
public class Db1Config {
@Bean
@ConfigurationProperties("spring.datasource.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/db1/*.xml"));
return sessionFactory.getObject();
}
}
8. 常见问题排查指南
8.1 字段映射失败问题
症状:查询返回null值
排查步骤:
- 检查
@TableField注解配置 - 确认数据库字段名与实体类属性名对应关系
- 查看是否开启
map-underscore-to-camel-case - 检查SQL日志确认实际查询字段
8.2 分页不生效问题
可能原因:
- 未配置分页插件
- 自定义SQL未使用IPage参数
- Page对象构造参数错误
8.3 乐观锁冲突处理
标准处理流程:
java复制User user = userService.getById(1L);
user.setName("new name");
int retry = 3;
while (retry-- > 0) {
try {
userService.updateById(user);
break;
} catch (OptimisticLockingFailureException e) {
user = userService.getById(1L); // 重新获取最新数据
// 合并业务修改...
}
}
经过三年在生产环境中的实践验证,MyBatis-Plus确实大幅提升了我们的开发效率。特别是在快速迭代的业务系统中,它能减少大量重复CRUD代码。但也要注意:对于超复杂的查询场景,不要强行使用Wrapper,该写XML映射文件时就写XML,保持灵活性才是MyBatis系列工具的核心优势。
