1. MyBatis-Plus 核心定位与基础环境搭建
MyBatis-Plus(简称MP)是国内开发者基于MyBatis封装的一套增强工具包,它在保留MyBatis所有特性的基础上,通过内置通用Mapper和Service,显著简化了单表CRUD操作。根据官方统计,使用MP后基础数据操作代码量可减少70%以上,这使得它成为Java后端开发中的效率利器。
要开始使用MP的CRUD功能,首先需要完成环境配置。以Spring Boot项目为例,在pom.xml中添加最新稳定版依赖(当前推荐3.5.3版本):
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
数据库配置采用yml格式更为清晰,以下是MySQL的典型配置:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mp_demo?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志
实体类映射需要配合注解使用,这是与数据库表关联的关键。例如用户表的映射示例:
java复制@Data
@TableName("sys_user") // 表名注解
public class User {
@TableId(type = IdType.AUTO) // 主键策略
private Long id;
private String username;
private Integer age;
@TableField("email") // 字段映射
private String mail;
@TableLogic // 逻辑删除标记
private Integer deleted;
}
特别注意:当数据库表名与实体类名不一致时,必须使用@TableName显式指定。字段映射规则遵循驼峰转下划线自动匹配,特殊字段需用@TableField单独标注。
2. 基础CRUD操作全解析
2.1 Mapper层基础方法
MyBatis-Plus通过BaseMapper接口提供了最基础的CRUD方法,这些方法开箱即用,无需编写XML。以下是核心方法及其SQL等效形式:
java复制public interface UserMapper extends BaseMapper<User> {
// 内置方法无需实现
}
// 示例用法
userMapper.insert(user);
// INSERT INTO sys_user (username, age, email) VALUES (?, ?, ?)
userMapper.deleteById(1L);
// DELETE FROM sys_user WHERE id=1
userMapper.updateById(user);
// UPDATE sys_user SET username=?, age=?, email=? WHERE id=?
User user = userMapper.selectById(1L);
// SELECT * FROM sys_user WHERE id=1
批量操作方法在实际开发中尤为重要,它们能显著减少数据库交互次数:
java复制List<User> users = Arrays.asList(new User(...), new User(...));
userMapper.insertBatchSomeColumn(users); // 批量插入(自选字段)
userMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));
// DELETE FROM sys_user WHERE id IN (1, 2, 3)
List<User> userList = userMapper.selectBatchIds(Arrays.asList(1L, 2L));
// SELECT * FROM sys_user WHERE id IN (1, 2)
2.2 Service层增强接口
IService接口进一步封装了更丰富的业务场景方法,典型用法如下:
java复制public interface UserService extends IService<User> {}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {}
// 使用示例
userService.saveOrUpdate(user); // 存在则更新,否则插入
userService.lambdaUpdate()
.eq(User::getAge, 20)
.set(User::getEmail, "update@mp.com")
.update(); // 条件更新
long count = userService.lambdaQuery()
.gt(User::getAge, 18)
.like(User::getUsername, "张")
.count(); // 复杂条件统计
Service层特别适合处理带有业务逻辑的批量操作,比如下面这个事务性的批量保存示例:
java复制@Transactional
public void batchSaveWithCheck(List<User> users) {
for (User user : users) {
if (user.getAge() < 18) {
throw new RuntimeException("未成年人禁止注册");
}
}
saveBatch(users, 1000); // 每1000条提交一次
}
2.3 条件构造器详解
QueryWrapper和LambdaQueryWrapper是构建查询条件的两种主要方式,后者具有类型安全优势:
java复制// 传统方式
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "username", "age")
.like("username", "张")
.between("age", 20, 30)
.orderByDesc("create_time");
// Lambda方式(推荐)
LambdaQueryWrapper<User> lambdaWrapper = Wrappers.lambdaQuery();
lambdaWrapper.select(User::getId, User::getUsername, User::getAge)
.like(User::getUsername, "张")
.between(User::getAge, 20, 30)
.orderByDesc(User::getCreateTime);
UpdateWrapper则专注于更新操作,支持动态字段更新:
java复制UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("email", "new@mp.com")
.setSql("balance = balance - 100")
.eq("status", 1)
.apply("date_format(create_time,'%Y-%m')={0}", "2023-08");
经验之谈:复杂条件构造时,建议将固定条件放在前面,动态条件通过
if语句追加,这样能保持SQL可读性。例如先写.eq("deleted", 0)再动态追加其他条件。
3. 高级特性与实战技巧
3.1 分页查询实现方案
MyBatis-Plus的分页需要先配置拦截器,然后通过Page对象实现物理分页:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
// 使用示例
Page<User> page = new Page<>(1, 10); // 当前页,每页条数
Page<User> result = userMapper.selectPage(page,
Wrappers.<User>lambdaQuery()
.gt(User::getAge, 18)
.orderByAsc(User::getCreateTime)
);
System.out.println("总记录数:" + result.getTotal());
System.out.println("总页数:" + result.getPages());
result.getRecords().forEach(System.out::println);
对于自定义SQL的分页查询,需要在Mapper方法中正确接收Page参数:
xml复制<!-- XML中 -->
<select id="selectUserPage" resultType="User">
SELECT * FROM sys_user ${ew.customSqlSegment}
</select>
java复制// Mapper接口
Page<User> selectUserPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
3.2 逻辑删除与自动填充
逻辑删除是现代系统的标配功能,配置后删除操作自动转为更新:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段名
logic-delete-value: 1 # 删除值
logic-not-delete-value: 0 # 未删除值
自动填充常用于创建时间、更新时间等字段,通过实现MetaObjectHandler接口:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
3.3 多租户与乐观锁
多租户方案通过TenantLineInnerInterceptor实现数据隔离:
java复制public class MyTenantLineHandler implements TenantLineHandler {
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public Expression getTenantId() {
return new LongValue(1L); // 实际应从上下文中获取
}
@Override
public boolean ignoreTable(String tableName) {
return !Arrays.asList("sys_user", "sys_role").contains(tableName);
}
}
乐观锁通过@Version注解实现,确保并发更新的安全性:
java复制@Version
private Integer version;
配置乐观锁拦截器后,更新时会自动带上版本条件:
sql复制UPDATE sys_user SET ..., version=version+1 WHERE id=? AND version=?
4. 性能优化与常见问题
4.1 批量操作优化
批量插入建议使用executeBatch模式,配合rewriteBatchedStatements参数:
java复制// JDBC连接字符串需添加参数
jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
// 批量插入示例
List<User> users = generateUsers(10000);
userService.saveBatch(users, 1000); // 每1000条提交一次
对于大数据量更新,建议使用分批处理+并行流:
java复制List<Long> ids = getAllIds(); // 获取所有待处理ID
Lists.partition(ids, 1000).parallelStream().forEach(batch -> {
userMapper.update(
Wrappers.<User>lambdaUpdate()
.set(User::getStatus, 1)
.in(User::getId, batch)
);
});
4.2 SQL注入防御
虽然MP的Wrapper能有效防止SQL注入,但在自定义SQL时仍需注意:
java复制// 错误示范(存在注入风险)
wrapper.apply("date_format(create_time,'%Y-%m')='"+month+"'");
// 正确做法
wrapper.apply("date_format(create_time,'%Y-%m')={0}", month);
XML中同样需要使用预编译方式:
xml复制<select id="selectByMonth" resultType="User">
SELECT * FROM user
WHERE date_format(create_time,'%Y-%m')=#{month}
<!-- 不要使用 ${month} -->
</select>
4.3 典型问题排查
问题1:字段值为null不更新
解决方案:在配置中开启null值更新
yaml复制mybatis-plus:
global-config:
db-config:
update-strategy: not_empty
问题2:Long类型主键精度丢失
原因:JavaScript处理Long类型有精度限制
解决方案:返回JSON时主键转为String
java复制@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
问题3:自动填充不生效
检查点:
- 实现类是否被Spring管理
- 字段类型是否匹配
- 是否使用了strictInsertFill/strictUpdateFill
在实际项目中,我建议为所有Mapper方法编写单元测试,特别是复杂条件查询。可以使用H2内存数据库快速验证SQL正确性,这能节省大量调试时间。另外,对于高频查询字段,务必添加数据库索引,MP的@TableField注解支持通过condition属性指定索引使用方式。
