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
方法进行字段过滤,显著提升应用的性能和数据安全性。记住,明确指定需要的字段是核心原则。