MyBatis-Plus 的 select 方法是 QueryWrapper 和 LambdaQueryWrapper 中用于指定 SQL 查询中 SELECT 子句要返回的字段的关键功能。它允许你精确控制从数据库中检索哪些列,避免不必要的数据传输和内存消耗。
1. 核心概念
- 目的:控制 SQL
SELECT语句中要查询的列,实现字段过滤或字段投影。 - 优势:
- 减少网络传输:只传输需要的字段,降低网络带宽消耗。
- 减少内存占用:实体对象只包含查询的字段,节省 JVM 内存。
- 提升性能:减少数据库 I/O 和数据处理开销。
- 增强安全性:避免将敏感字段(如密码、密钥)暴露给上层应用。
- 核心方法:
select(String... sqlSelect):接受可变数量的字段名字符串。select(Class<T> entityClass, Predicate<TableFieldInfo> predicate):接受一个实体类和一个Predicate函数,根据条件动态决定是否包含某个字段。lambda():在LambdaQueryWrapper中使用select时,通常配合lambda()方法来引用实体类的字段(实际上是引用SFunction)。
- 作用域:仅影响
SELECT子句,不影响WHERE,ORDER BY等其他子句中的字段使用。
2. 操作步骤 (非常详细)
步骤 1: 准备实体类和 Mapper
确保你已经按照 MyBatis-Plus 的标准配置好了实体类 (Entity) 和 Mapper 接口。
// User.java (实体类)
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data
@TableName("user") // 映射到数据库表 user
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("name")
private String name;
@TableField("age")
private Integer age;
@TableField("email")
private String email;
@TableField("password") // 敏感字段
private String password;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_time")
private LocalDateTime updateTime;
}
// UserMapper.java (Mapper 接口)
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper,拥有基本 CRUD 方法
}
步骤 2: 创建 QueryWrapper 实例
选择使用 QueryWrapper 或更推荐的 LambdaQueryWrapper。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
// 方式一:普通 QueryWrapper (使用字符串字段名)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 方式二:LambdaQueryWrapper (推荐,类型安全)
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
步骤 3: 使用 select 方法指定查询字段
这是核心步骤,有多种方式。
3.1 指定具体字段名 (字符串方式)
// 创建 QueryWrapper
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 指定要查询的字段:id, name, age
// 生成的 SQL: SELECT id, name, age FROM user WHERE ...
wrapper.select("id", "name", "age");
// 执行查询
List<User> users = userMapper.selectList(wrapper);
// 返回的 User 对象中,只有 id, name, age 字段有值,其他字段 (email, password 等) 为 null。
3.2 使用 Lambda 表达式指定字段 (推荐方式)
// 创建 LambdaQueryWrapper
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// 使用 Lambda 表达式指定字段
// User::getId, User::getName, User::getAge 是 SFunction (MyBatis-Plus 特有)
// 生成的 SQL: SELECT id, name, age FROM user WHERE ...
lambdaWrapper.select(User::getId, User::getName, User::getAge);
// 执行查询
List<User> users = userMapper.selectList(lambdaWrapper);
// 返回的 User 对象中,只有 id, name, age 字段有值。
3.3 动态条件选择字段
使用 select(Class<T> entityClass, Predicate<TableFieldInfo> predicate) 方法,根据条件动态决定包含哪些字段。
// 创建 QueryWrapper
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 场景:根据一个布尔标志位,决定是否包含 email 字段
boolean includeEmail = true; // 或 false,根据业务逻辑
// Predicate<TableFieldInfo> 会遍历 User 类的所有 @TableField 字段
// tableFieldInfo 包含字段的元信息 (如数据库列名、Java 属性名、是否主键等)
wrapper.select(User.class, tableFieldInfo -> {
// 包含 id, name, age 字段 (总是包含)
if (Arrays.asList("id", "name", "age").contains(tableFieldInfo.getColumn())) {
return true;
}
// 根据条件决定是否包含 email 字段
if ("email".equals(tableFieldInfo.getColumn()) && includeEmail) {
return true;
}
// 其他字段 (如 password, create_time) 不包含
return false;
});
// 执行查询
List<User> users = userMapper.selectList(wrapper);
// 如果 includeEmail 为 true,则返回 id, name, age, email;否则只返回 id, name, age。
3.4 排除特定字段
虽然没有直接的 exclude 方法,但可以通过 Predicate 实现排除。
// 创建 QueryWrapper
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 排除 password 字段 (假设它是敏感字段)
// 注意:这种方式会包含所有其他字段,包括 createTime, updateTime 等
// 通常更推荐明确指定需要的字段 (方式1, 2)
wrapper.select(User.class, tableFieldInfo -> !"password".equals(tableFieldInfo.getColumn()));
// 执行查询
List<User> users = userMapper.selectList(wrapper);
// 返回除 password 外的所有字段。
步骤 4: 结合其他条件执行查询
select 可以与其他查询条件(eq, like, orderBy 等)结合使用。
// 使用 LambdaQueryWrapper
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
// 1. 指定查询字段
lambdaWrapper.select(User::getId, User::getName, User::getAge);
// 2. 添加 WHERE 条件
lambdaWrapper.gt(User::getAge, 18); // WHERE age > 18
// 3. 添加 ORDER BY
lambdaWrapper.orderByDesc(User::getCreateTime); // ORDER BY create_time DESC
// 4. 执行查询
List<User> adultUsers = userMapper.selectList(lambdaWrapper);
// SQL: SELECT id, name, age FROM user WHERE age > 18 ORDER BY create_time DESC
3. 常见错误
字段名拼写错误:
- 错误:
wrapper.select("nmae", "age");(字段名nmae错误)。 - 后果:SQL 语法错误或查询不到数据。
- 解决:仔细检查字段名,或使用
LambdaQueryWrapper避免此问题。
- 错误:
在
select中使用非数据库字段:- 错误:实体类中有一个
@TableField(exist = false)的字段fullName,然后wrapper.select("fullName", "age");。 - 后果:SQL 语法错误,因为
fullName不是数据库表中的列。 - 解决:
select方法只能指定数据库表中存在的列名。
- 错误:实体类中有一个
Predicate条件逻辑错误:- 错误:在
select(User.class, predicate)中,predicate返回false给了所有字段,导致SELECT子句为空。 - 后果:SQL 语法错误 (
SELECT FROM ...)。 - 解决:确保
predicate至少为一个字段返回true。
- 错误:在
混淆
select和WHERE条件:- 错误:认为
wrapper.select("id").eq("name", "张三");会只返回id列且name列等于 '张三' 的记录,但忘记了eq条件中的name字段在WHERE子句中是必需的,即使它不在SELECT列表中。这通常不是错误,但需理解SELECT只控制返回列。 - 澄清:
WHERE子句可以引用不在SELECT列表中的字段。上述 SQL 是有效的:SELECT id FROM user WHERE name = '张三'。
- 错误:认为
期望未查询的字段自动填充:
- 错误:执行
select("id", "name")后,期望返回的User对象的age字段能通过某种方式(如缓存、其他查询)自动有值。 - 后果:
age字段为null。 - 解决:明确
select只返回指定字段的值,其他字段在返回的实体对象中为null。需要其他字段时,应在select中包含它们。
- 错误:执行
4. 注意事项
select与@TableField(exist = false):select方法只针对数据库中存在的字段。@TableField(exist = false)标记的字段是 Java 逻辑字段,不能在select中使用。select与主键:即使不显式在select中包含主键(id),BaseMapper的selectById方法依然能正常工作,因为它内部处理了主键。但在selectList/selectPage中使用QueryWrapper时,如果select列表不包含主键,返回的对象主键字段也会是null。select与更新/删除:UpdateWrapper也有select方法,但它不是用于UPDATE语句的SELECT子句(UPDATE没有SELECT子句)。UpdateWrapper.select通常用于指定UPDATE语句中SET子句要更新的字段(结合set方法),但这不是标准用法,标准更新字段应使用set方法。QueryWrapper.select用于SELECT查询。select与groupBy/having:在GROUP BY查询中,SELECT子句中除了聚合函数外的字段,必须出现在GROUP BY子句中。MyBatis-Plus 不会自动检查这一点,需要开发者自己保证 SQL 语法正确。select与性能:虽然select减少了数据量,但极端情况下(如只select主键),如果后续需要其他字段,可能会导致 N+1 查询问题。需要权衡。
5. 使用技巧
- 明确指定字段:优先使用
select(field1, field2, ...)明确列出需要的字段,而不是依赖select(User.class, predicate)排除字段,这样意图更清晰。 LambdaQueryWrapper+select:这是最安全、最推荐的方式,避免了字符串硬编码。- 创建常量或方法:如果某些字段组合经常一起查询,可以定义常量或静态方法来复用
select配置。public class UserQueryWrappers { public static LambdaQueryWrapper<User> selectBasicInfo() { return new LambdaQueryWrapper<User>().select(User::getId, User::getName, User::getAge); } } // 使用 userMapper.selectList(UserQueryWrappers.selectBasicInfo().eq(User::getAge, 25)); - 结合
exists/notExists:在EXISTS子查询中,SELECT列表通常无关紧要(常用SELECT 1),MyBatis-Plus 的exists方法允许你自定义子查询的SELECT部分。 last方法:虽然不推荐,但wrapper.last("LIMIT 1")可以在 SQL 末尾追加,有时与select结合用于特殊优化(注意不支持分页插件)。
6. 最佳实践与性能优化
最佳实践:
- Always
select:除非明确需要所有字段,否则始终使用select指定需要的字段。 - 优先
Lambda:在select中使用LambdaQueryWrapper和SFunction(如User::getName)。 - **避免 `SELECT *: 这是最重要的性能和安全实践。
- 保护敏感数据:在查询 DTO 或 VO 时,确保
select排除了密码、密钥、身份证号等敏感信息。 - DTO/VO 映射:对于复杂查询或需要特定字段组合,考虑创建专门的 DTO (Data Transfer Object) 或 VO (View Object),并使用
select配合@Results/@ResultMap或 MyBatis-Plus 的@TableField(在 DTO 上) 进行映射,而不是直接返回完整的 Entity。 - 文档化:在复杂查询的
select逻辑旁添加注释,说明为什么选择这些字段。
- Always
性能优化:
- 最小化数据集:
select是减少数据传输最直接有效的方法。 - 利用覆盖索引 (Covering Index):如果
select的字段和WHERE/ORDER BY的字段都能被同一个索引覆盖,数据库可以直接从索引中获取所有数据,无需回表查询聚簇索引,极大提升性能。设计索引时考虑查询模式。 - 批量查询优化:在批量查询场景下,精确的
select能显著减少单次查询的数据量。 - 缓存友好:返回更小的数据包,使得缓存(如 Redis)能存储更多数据,或减少缓存序列化/反序列化的开销。
- 监控:通过 SQL 日志监控实际生成的
SELECT语句,确认select配置生效且符合预期。
- 最小化数据集:
遵循这些步骤、技巧和最佳实践,你可以高效、安全地使用 MyBatis-Plus 的 select 方法进行字段过滤,显著提升应用的性能和数据安全性。记住,明确指定需要的字段是核心原则。