MyBatis-Plus 常用特性
候选人小陈在面试滴滴时,简历上写着"熟练使用 MyBatis-Plus",面试官顺着问了一句:
"MyBatis-Plus 的 LambdaQueryWrapper 和 XML 里写 SQL 相比,有什么优势?"
小陈说:"不用写 SQL 了,更方便。"
面试官追问:"那你知道 LambdaQueryWrapper 是怎么做到不用写字段名的字符串的吗?"
小陈说:"呃...它会自动映射?"
面试官继续追问:"如果表里的字段名是下划线命名,实体类是驼峰命名,MyBatis-Plus 怎么处理的?"
小陈支支吾吾答不上来。
【面试官心理】
MyBatis-Plus 是目前国内使用最广泛的 MyBatis 增强框架。这道题我用来测试候选人对"无 SQL 化"的理解深度——知道 CRUD 无 SQL 是基本操作,知道 LambdaQueryWrapper 通过泛型擦除 + 反射获取字段名的是理解原理的,知道驼峰映射配置的才是真正用过生产环境的。
一、CRUD 无 SQL 🔴
1.1 BaseMapper 的核心方法
// 定义 Mapper 接口,只需继承 BaseMapper
public interface UserMapper extends BaseMapper<User> {
// 无需定义任何方法!MyBatis-Plus 自动提供以下 CRUD 方法
// insert(T entity)
// deleteById(Serializable id)
// deleteBatchIds(Collection<? extends Serializable> ids)
// deleteByMap(Map<String, Object> columnMap)
// selectById(Serializable id)
// selectBatchIds(Collection<? extends Serializable> ids)
// selectByMap(Map<String, Object> columnMap)
// selectPage(IPage<T> page, Wrapper<T> queryWrapper)
// selectMapsPage(IPage<T> page, Wrapper<T> queryWrapper)
// updateById(T entity)
// update(T entity, Wrapper<T> updateWrapper)
// selectCount(Wrapper<T> queryWrapper)
// selectList(Wrapper<T> queryWrapper)
// ...
}
1.2 基本 CRUD 操作
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void testCrud() {
// 插入
User user = new User();
user.setName("张三");
user.setAge(28);
user.setEmail("zhangsan@example.com");
userMapper.insert(user); // 自动生成 INSERT 语句,主键回填
// 根据 ID 查询
User found = userMapper.selectById(1L);
// 更新
User update = new User();
update.setId(1L);
update.setAge(30);
userMapper.updateById(update); // 只更新非 null 字段
// 删除
userMapper.deleteById(1L);
userMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));
}
}
1.3 ❌ 错误示范
候选人原话:"MyBatis-Plus 不用写 SQL,很方便,所有查询都可以用它。"
问题诊断:
- 不知道复杂查询(多表关联、子查询)仍然需要手写 SQL
- 不知道 updateById 只更新非 null 字段的坑
- 不知道主键策略和自增的配置
【面试官心理】
MyBatis-Plus 不是银弹。对于简单的单表 CRUD,它能极大提升效率;但对于复杂的多表查询、动态 SQL、批量操作,仍然需要手写 XML 或注解。知道边界在哪里,才算真正会用这个框架。
二、Lambda 条件构造器 🟡
2.1 LambdaQueryWrapper
public void testQueryWrapper() {
// 查询:名字包含"张",年龄在 20-30 之间,按年龄降序
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "张")
.between(User::getAge, 20, 30)
.orderByDesc(User::getAge)
.last("LIMIT 10");
List<User> users = userMapper.selectList(wrapper);
}
LambdaQueryWrapper 的优势:通过 Lambda 表达式引用实体类的字段方法(User::getName),而不是写字符串("name"),享受编译期检查,字段改名时编译器报错。
2.2 LambdaUpdateWrapper
public void testUpdateWrapper() {
// 更新:所有年龄 < 18 的用户,年龄设为 18
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(User::getAge, 18)
.lt(User::getAge, 18);
userMapper.update(null, wrapper);
// 第一个参数是实体(设为 null 表示只使用 wrapper 条件)
}
2.3 常用条件方法
三、IService + ServiceImpl 双层架构 🟡
3.1 架构设计
MyBatis-Plus 提供 IService 接口和 ServiceImpl 实现类:
// Mapper 层
public interface UserMapper extends BaseMapper<User> {
}
public class UserMapperImpl extends ServiceImpl<UserMapper, User> implements UserMapper {
// MyBatis-Plus 通过 ServiceImpl 注入了 BaseMapper
// 自动拥有所有 CRUD 方法
}
// Service 层
public interface UserService extends IService<User> {
// 自定义业务方法
User getVipUser(Long id);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Override
public User getVipUser(Long id) {
return getById(id);
}
// 通过 getBaseMapper() 访问 Mapper 层
public List<User> getTop10() {
return getBaseMapper().selectList(
new LambdaQueryWrapper<User>()
.orderByDesc(User::getAge)
.last("LIMIT 10")
);
}
}
为什么这么设计:
BaseMapper:单表 CRUD 的原子操作
IService:业务层接口,定义业务方法
ServiceImpl:实现类,同时持有 BaseMapper 和 IService,可以在实现中组合使用
3.2 链式调用
// IService 支持链式调用
userService.lambdaQuery()
.eq(User::getStatus, 1)
.like(User::getName, "张")
.orderByDesc(User::getCreateTime)
.list();
userService.lambdaUpdate()
.eq(User::getId, 1L)
.set(User::getStatus, 0)
.update();
四、自动填充 🟡
// 配置字段自动填充:创建时间、更新时间
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 插入时自动填充
StrictFill<String> createTime = StrictFill
.forMetaObject("createTime")
.fillType(() -> LocalDateTime.now());
StrictFill<String> updateTime = StrictFill
.forMetaObject("updateTime")
.fillType(() -> LocalDateTime.now());
StrictFill<Long> createUser = StrictFill
.forMetaObject("createUser")
.fillType(() -> SecurityContextHolder.getUserId());
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时自动填充
strictFill(LocalDateTime.class, metaObject, "updateTime", LocalDateTime::now);
strictFill(Long.class, metaObject, "updateUser", SecurityContextHolder::getUserId);
}
}
4.2 实体类配置
@TableName("sys_user")
public class User {
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
💡
FieldFill.INSERT 表示插入时填充,FieldFill.INSERT_UPDATE 表示插入和更新时都填充。这个功能避免了每次插入/更新时手动设置时间的重复代码。
五、逻辑删除 🟡
5.1 配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局配置逻辑删除字段
logic-delete-value: 1 # 删除值
logic-not-delete-value: 0 # 未删除值
5.2 实体类
@TableName("sys_user")
public class User {
@TableLogic // 标记为逻辑删除字段
private Integer deleted; // 0=未删除, 1=已删除
}
5.3 效果
// 调用 deleteById 时
userMapper.deleteById(1L);
// 实际执行的 SQL:
// UPDATE sys_user SET deleted = 1 WHERE id = 1 AND deleted = 0
// 调用 selectById 时
userMapper.selectById(1L);
// 实际执行的 SQL:
// SELECT * FROM sys_user WHERE id = 1 AND deleted = 0
// 已删除的记录自动被过滤
⚠️
逻辑删除有一个隐藏坑:如果在 XML 中手写了 SELECT 语句,逻辑删除不会自动生效。需要在 SQL 中手动加上 WHERE deleted = 0。
六、分页插件 🔴
6.1 配置
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
6.2 使用
public IPage<User> getUserPage(int current, int size) {
Page<User> page = new Page<>(current, size);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "张")
.orderByDesc(User::getCreateTime);
// selectPage 自动添加 LIMIT 和 OFFSET
return userMapper.selectPage(page, wrapper);
}
生成的 SQL:
SELECT * FROM sys_user
WHERE deleted = 0 AND name LIKE '%张%'
ORDER BY create_time DESC
LIMIT 10 OFFSET 0;
-- 总数查询(自动执行)
SELECT COUNT(*) FROM sys_user
WHERE deleted = 0 AND name LIKE '%张%';
6.3 分页超时的坑
// 错误:Page 对象被复用
Page<User> page = new Page<>(1, 10);
userMapper.selectPage(page, wrapper);
// page 中有总数,但后续的 page 对象可能污染
// 正确:每次查询创建新的 Page
public IPage<User> getPage(int current, int size) {
return userMapper.selectPage(new Page<>(current, size), wrapper);
}
七、Wrapper 高级用法 🟡
7.1 select 指定字段
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getName, User::getEmail)
.eq(User::getStatus, 1);
// 生成的 SQL:
// SELECT id, name, email FROM sys_user WHERE status = 1
7.2 嵌套查询
// 查询有订单的用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.inSql(User::getId,
"SELECT user_id FROM sys_order WHERE amount > 1000");
// 生成的 SQL:
// SELECT * FROM sys_user WHERE id IN (
// SELECT user_id FROM sys_order WHERE amount > 1000
// )
7.3 动态 SQL
public List<User> search(String name, Integer age, String email) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), User::getName, name)
.eq(age != null, User::getAge, age)
.like(StringUtils.isNotBlank(email), User::getEmail, email);
// 第一个参数为 true 时条件才生效
return userMapper.selectList(wrapper);
}
八、生产避坑 🟡
8.1 批量插入性能问题
// 错误:循环单条插入,1000条数据需要1000次数据库交互
for (User user : userList) {
userMapper.insert(user);
}
// 正确:使用 MyBatis-Plus 的批量插入
// 需要配置 SQL 批处理模式
userService.saveBatch(userList); // IService 提供的批量方法
8.2 updateById 的 null 字段问题
// updateById 默认只更新非 null 字段
User update = new User();
update.setId(1L);
update.setName("新名字");
// age 字段不会被更新(即使数据库中有值)
userMapper.updateById(update);
// UPDATE sys_user SET name = '新名字' WHERE id = 1
// 如果需要强制更新为 null,需要用 UpdateWrapper
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, 1L)
.set(User::getName, null); // 强制设为 null
userMapper.update(null, wrapper);
8.3 字段映射问题
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰,默认开启
// 表字段:user_name (下划线)
// 实体字段:userName (驼峰)
// MyBatis-Plus 默认开启自动映射,无需额外配置
// 如果字段名不一致,用 @TableField 指定
@TableField("nick_name")
private String nickname;
【面试官心理】
这道追问"下划线和驼峰的映射"是测试候选人有没有配置过 MyBatis-Plus 的经验。知道默认开启的占50%,知道可以关闭的占30%,知道可以自定义映射策略的不到10%。
九、工程选型 🟢
9.1 MyBatis-Plus vs MyBatis vs JPA
【面试官心理】
我通常会问:"既然 MyBatis-Plus 这么方便,为什么还有项目坚持用原生 MyBatis?" 能答出"原生 MyBatis 对复杂 SQL 的控制更精细,MyBatis-Plus 的 Wrapper 在复杂场景下反而不如手写 SQL 清晰"的,通常是有实际踩坑经验的工程师。
十、面试追问链 🔴
第一层:基本使用
面试官问:"MyBatis-Plus 怎么实现无 SQL 查询?"
候选人答:"继承 BaseMapper..."
考察点:基本用法
第二层:原理追问
面试官追问:"LambdaQueryWrapper 为什么不需要写字段名字符串?"
候选人答:...(泛型擦除 + Lambda)
考察点:底层原理
第三层:配置细节
面试官追问:"分页插件怎么配置?分页 SQL 怎么生成的?"
候选人答:...(PaginationInnerInterceptor)
考察点:插件机制
第四层:生产问题
面试官追问:"分页查询很慢怎么排查?"
候选人答:...(索引、COUNT 优化)
考察点:性能优化