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 常用条件方法

方法说明示例
eq等于 =.eq(User::getName, "张三")
ne不等于 <>.ne(User::getName, "张三")
gt大于 >.gt(User::getAge, 18)
ge大于等于 >=.ge(User::getAge, 18)
lt小于 <.lt(User::getAge, 65)
le小于等于 <=.le(User::getAge, 65)
betweenBETWEEN ... AND ....between(User::getAge, 18, 65)
likeLIKE %xxx%.like(User::getName, "张")
likeLeftLIKE %xxx.likeLeft(User::getName, "张")
likeRightLIKE xxx%.likeRight(User::getName, "张")
inIN.in(User::getId, 1, 2, 3)
isNullIS NULL.isNull(User::getEmail)
isNotNullIS NOT NULL.isNotNull(User::getEmail)
inSqlIN (子查询).inSql(User::getId, "SELECT user_id FROM order")
select指定查询字段.select(User::getId, User::getName)

三、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:实现类,同时持有 BaseMapperIService,可以在实现中组合使用

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();

四、自动填充 🟡

4.1 MetaObjectHandler

// 配置字段自动填充:创建时间、更新时间
@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-PlusMyBatisJPA
SQL 控制单表无 SQL,多表手写完全手写自动生成,可能不精准
学习成本
灵活度最高
国内生态一般
适用场景快速开发、单表为主复杂 SQL、精细控制领域驱动设计

【面试官心理】 我通常会问:"既然 MyBatis-Plus 这么方便,为什么还有项目坚持用原生 MyBatis?" 能答出"原生 MyBatis 对复杂 SQL 的控制更精细,MyBatis-Plus 的 Wrapper 在复杂场景下反而不如手写 SQL 清晰"的,通常是有实际踩坑经验的工程师。

十、面试追问链 🔴

第一层:基本使用 面试官问:"MyBatis-Plus 怎么实现无 SQL 查询?" 候选人答:"继承 BaseMapper..." 考察点:基本用法

第二层:原理追问 面试官追问:"LambdaQueryWrapper 为什么不需要写字段名字符串?" 候选人答:...(泛型擦除 + Lambda) 考察点:底层原理

第三层:配置细节 面试官追问:"分页插件怎么配置?分页 SQL 怎么生成的?" 候选人答:...(PaginationInnerInterceptor) 考察点:插件机制

第四层:生产问题 面试官追问:"分页查询很慢怎么排查?" 候选人答:...(索引、COUNT 优化) 考察点:性能优化