1. MyBatis-Plus Mapper接口select操作深度解析
作为Java开发者最常用的ORM框架之一,MyBatis-Plus在传统MyBatis基础上进行了大量增强,其中BaseMapper提供的CRUD方法极大简化了单表操作。今天我将结合多年实战经验,详细拆解Mapper接口中的select系列方法,包括基础查询、条件构造器使用、返回结果处理等核心知识点。
1.1 MyBatis-Plus与BaseMapper定位
MyBatis-Plus的核心价值在于"简化开发"四个字。相比原生MyBatis需要手动编写SQL和ResultMap,MP通过BaseMapper接口提供了开箱即用的通用方法。只需让自定义Mapper继承BaseMapper并指定泛型类型,就能获得18种基础CRUD方法:
java复制public interface UserMapper extends BaseMapper<User> {
// 无需手动编写基础方法
}
这种设计符合"约定优于配置"原则,开发者只需关注特殊查询的编写,80%的常规操作都可以直接使用内置方法。BaseMapper中的select方法占比超过60%,是日常使用最频繁的部分。
1.2 select方法分类与适用场景
BaseMapper中的select方法可分为四大类:
-
基础查询:
selectById(Serializable id):根据主键查询selectBatchIds(Collection<? extends Serializable> idList):批量ID查询selectByMap(Map<String, Object> columnMap):字段条件查询
-
全量查询:
selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper):条件列表查询selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper):返回Map列表selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper):返回首字段值集合
-
分页查询:
selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper):物理分页selectMapsPage(IPage<Map<String, Object>> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper):Map分页
-
特殊查询:
selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper):单条记录查询selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper):计数查询
提示:实际开发中90%的单表查询需求都可以用这些方法覆盖,应优先使用它们而非自定义SQL
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础select方法实战详解
2.1 主键查询的陷阱与优化
selectById是最常用的方法之一,但有些细节需要注意:
java复制User user = userMapper.selectById(1L);
表面简单的操作背后有几个关键点:
- 主键类型必须实现Serializable接口(基本包装类型都满足)
- 如果使用雪花ID等长整型,建议参数用Long而非long,避免NPE风险
- 返回值为null时表示记录不存在,调用方应做好判空处理
批量查询selectBatchIds的性能优化技巧:
java复制List<Long> ids = Arrays.asList(1L, 2L, 3L);
List<User> users = userMapper.selectBatchIds(ids);
- 当ID数量超过1000时,应考虑分批查询避免SQL过长
- 可配合
@Transactional实现批量操作的原子性 - MyBatis-Plus 3.4.0+版本优化了IN查询的SQL生成方式
2.2 字段条件查询的灵活运用
selectByMap提供了一种简单的字段匹配查询方式:
java复制Map<String, Object> condition = new HashMap<>();
condition.put("age", 25);
condition.put("gender", 1);
List<User> users = userMapper.selectByMap(condition);
但需要注意:
- Map的key必须严格对应数据库字段名(注意大小写和下划线转换)
- 条件关系只能是AND,无法实现OR或复杂条件
- 值为null时会被忽略(这与update操作不同)
实际项目中,复杂条件建议使用Wrapper构建,而非Map方式
3. QueryWrapper条件构造器高级用法
3.1 条件构建基础语法
QueryWrapper是MyBatis-Plus条件查询的核心,支持链式调用:
java复制QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("department_id", 2)
.ge("age", 25)
.like("name", "张")
.orderByDesc("create_time");
List<User> users = userMapper.selectList(wrapper);
常用条件方法:
eq/ne:等于/不等于gt/ge/lt/le:大于/大于等于/小于/小于等于between/notBetween:区间查询like/notLike/likeLeft/likeRight:模糊查询in/notIn:集合查询isNull/isNotNull:空值判断groupBy/orderBy:分组和排序
3.2 Lambda表达式写法
为避免字段名硬编码,推荐使用Lambda方式:
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getDepartmentId, 2)
.ge(User::getAge, 25)
.like(User::getName, "张");
优势:
- 编译时检查字段名正确性
- 重构友好,字段修改会自动同步
- 避免手写字段名的拼写错误
3.3 复杂条件组合
通过and和or方法可以构建复杂逻辑:
java复制wrapper.and(qw -> qw.gt("salary", 5000).or().isNotNull("bonus"))
.or(qw -> qw.eq("status", 1).le("age", 30));
对应的SQL:
sql复制WHERE (salary > 5000 OR bonus IS NOT NULL)
OR (status = 1 AND age <= 30)
注意:嵌套条件要特别注意括号的位置,建议通过格式化SQL日志验证
4. 返回结果处理技巧
4.1 不同返回类型对比
| 方法 | 返回类型 | 适用场景 | 性能影响 |
|---|---|---|---|
| selectList | List |
需要完整实体 | 需要反射构建对象 |
| selectMaps | List<Map<String, Object>> | 只需部分字段 | 直接使用ResultSet |
| selectObjs | List | 获取单个字段 | 最佳性能 |
| selectCount | Integer | 统计数量 | 聚合查询 |
4.2 字段筛选与性能优化
默认查询所有字段时,可使用select方法指定返回字段:
java复制wrapper.select("id", "name", "email"); // 字符串方式
wrapper.select(User::getId, User::getName); // Lambda方式
这对于大表查询(含BLOB/TEXT字段)性能提升明显。实测在包含10个字段的表中,指定3个关键字段可使查询速度提升40%。
4.3 结果集自定义处理
对于复杂结果处理,可以结合Java8 Stream API:
java复制List<UserDTO> userDTOs = userMapper.selectList(wrapper)
.stream()
.map(user -> {
UserDTO dto = new UserDTO();
BeanUtils.copyProperties(user, dto);
dto.setAgeGroup(getAgeGroup(user.getAge()));
return dto;
})
.collect(Collectors.toList());
5. 分页查询实现方案
5.1 基础分页实现
MyBatis-Plus的分页需要先配置分页插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
然后使用Page对象进行查询:
java复制Page<User> page = new Page<>(1, 10); // 当前页,每页大小
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1);
IPage<User> result = userMapper.selectPage(page, wrapper);
// 获取分页信息
long total = result.getTotal();
List<User> records = result.getRecords();
5.2 分页性能优化
-
禁止
count(1)查询:当不需要总数时java复制Page<User> page = new Page<>(1, 10, false); -
优化count语句:对于多表关联查询
java复制page.setOptimizeCountSql(false); -
内存分页:小数据量时先查全量再分页
java复制List<User> allUsers = userMapper.selectList(wrapper); List<User> pageList = allUsers.stream() .skip((pageNum-1)*pageSize) .limit(pageSize) .collect(Collectors.toList());
6. 常见问题与解决方案
6.1 查询结果不符合预期
现象:条件设置正确但返回空列表
- 检查1:确认数据库连接正确,表名映射无误
- 检查2:开启SQL日志,核对生成的最终SQL
yaml复制mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl - 检查3:字段名是否遵循MP的命名转换规则(默认下划线转驼峰)
6.2 大数据量查询OOM
解决方案:
- 使用游标查询(3.4.0+版本)
java复制try (Cursor<User> cursor = userMapper.selectCursor(wrapper)) { cursor.forEach(user -> { // 处理单条记录 }); } - 分批查询(固定大小分页)
java复制int batchSize = 1000; int page = 1; while (true) { Page<User> page = new Page<>(page++, batchSize); IPage<User> result = userMapper.selectPage(page, wrapper); process(result.getRecords()); if (!result.hasNext()) break; }
6.3 多租户环境下查询隔离
如果配置了多租户插件,所有select查询会自动加上租户条件。需要忽略租户条件时:
java复制try {
TenantIgnoreHelper.setIgnore(true);
// 你的查询代码
} finally {
TenantIgnoreHelper.setIgnore(false);
}
7. 高级特性与最佳实践
7.1 自动结果映射
MyBatis-Plus支持复杂的嵌套结果映射。假设User中包含Department属性:
java复制@Data
public class User {
private Long id;
private String name;
private Department department;
}
可以通过@TableField注解配置关联查询:
java复制@TableField(el = "department.id, department.name")
private Department department;
然后在查询时自动填充:
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getId, 1);
User user = userMapper.selectOne(wrapper); // department信息自动填充
7.2 动态表名支持
对于分表场景,可以实现动态表名解析器:
java复制public class DynamicTableNameParser implements ITableNameHandler {
@Override
public String dynamicTableName(String sql, String tableName) {
return tableName + "_" + getCurrentYear();
}
}
然后在Interceptor中配置:
java复制interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor(new DynamicTableNameParser()));
7.3 逻辑删除集成
配置逻辑删除后,所有select查询会自动过滤已删除记录:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
需要查询包含已删除记录时:
java复制wrapper.withDeleted(); // 3.3.0+版本支持
8. 性能监控与调优
8.1 SQL执行监控
通过自定义Interceptor记录慢查询:
java复制public class SlowSqlInterceptor implements Interceptor {
private static final long SLOW_THRESHOLD = 1000; // 1秒
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long time = System.currentTimeMillis() - start;
if (time > SLOW_THRESHOLD) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
log.warn("Slow SQL detected: {} - {}ms", ms.getId(), time);
}
return result;
}
}
8.2 索引使用分析
通过EXPLAIN分析查询性能:
java复制@Select("EXPLAIN ${sql}")
List<Map<String, Object>> explain(@Param("sql") String sql);
// 使用方式
String originalSql = queryWrapper.getCustomSqlSegment();
List<Map<String, Object>> explains = userMapper.explain("SELECT * FROM user " + originalSql);
8.3 二级缓存配置
启用MyBatis二级缓存提升查询性能:
java复制@CacheNamespace
public interface UserMapper extends BaseMapper<User> {
// ...
}
然后在实体类上实现Serializable:
java复制@Data
public class User implements Serializable {
// 字段定义
}
注意二级缓存是跨Session的,适合读多写少的场景,更新频繁的数据不建议使用。
