在 MongoDB 和 Spring Data MongoDB 中,投影 (Projection) 是一种机制,允许我们在查询时指定只返回文档中特定字段的子集,而不是返回整个文档。这对于优化性能、减少网络传输数据量以及仅获取应用程序真正需要的数据非常有用。
使用 MongoTemplate
时,可以通过 Query
对象的 fields()
方法来实现投影。
如何使用 Query.fields()
进行投影:
Query.fields()
方法返回一个 Field
对象 (内部类 Query.Field
),你可以通过链式调用以下方法来指定包含或排除字段:
include(String fieldname)
: 包含指定的字段。exclude(String fieldname)
: 排除指定的字段。slice(String fieldname, int count)
: 返回数组字段的前 count
个元素。slice(String fieldname, int offset, int count)
: 返回数组字段从 offset
开始的 count
个元素。elemMatch(String fieldname, Criteria criteria)
: 对于数组字段,只返回第一个匹配 criteria
的元素。基本规则:
_id
字段默认总是被包含,除非你显式地使用 fields().exclude("_id")
排除它。include
和 exclude
针对顶级字段 (除了 _id
的特殊情况)。
include("fieldA", "fieldB")
,则只有 fieldA
、fieldB
和 _id
(除非排除) 会被返回。exclude("fieldC", "fieldD")
,则除了 fieldC
和 fieldD
之外的所有字段都会被返回。示例:
假设我们有以下 User
文档结构:
{
"_id": ObjectId("..."),
"username": "john.doe",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"age": 30,
"status": "ACTIVE",
"address": {
"street": "123 Main St",
"city": "Anytown"
},
"tags": ["java", "spring", "mongodb"]
}
对应的 Java POJO User.java
。
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import static org.springframework.data.mongodb.core.query.Criteria.where;
// MongoTemplate mongoTemplate = ...;
// 1. 只包含特定字段 (username 和 email),_id 默认会包含
Query queryInclude = new Query(where("status").is("ACTIVE"));
queryInclude.fields().include("username").include("email");
List<User> usersWithUsernameAndEmail = mongoTemplate.find(queryInclude, User.class);
// 返回的 User 对象中,只有 username, email 和 id 字段会有值,其他字段为 null 或默认值。
// 2. 只包含特定字段,并显式排除 _id
Query queryIncludeExcludeId = new Query(where("age").gt(25));
queryIncludeExcludeId.fields().include("firstName").include("lastName").exclude("_id");
List<User> usersWithNameOnly = mongoTemplate.find(queryIncludeExcludeId, User.class);
// 返回的 User 对象中,只有 firstName 和 lastName 字段会有值。
// 3. 排除特定字段 (返回除 age 和 status 之外的所有字段)
Query queryExclude = new Query(where("username").is("jane.doe"));
queryExclude.fields().exclude("age").exclude("status");
List<User> usersWithoutAgeAndStatus = mongoTemplate.find(queryExclude, User.class);
// 返回的 User 对象中,age 和 status 字段为 null 或默认值,其他字段有值。
// 4. 投影嵌套文档的字段
Query queryNested = new Query(where("address.city").is("Anytown"));
queryNested.fields().include("username").include("address.street"); // 只包含 username 和 address.street
List<User> usersWithStreet = mongoTemplate.find(queryNested, User.class);
// User 对象中,username 有值,address 对象中只有 street 有值,address.city 将为 null。
// 5. 投影数组字段的切片 (返回 tags 数组的前2个元素)
Query querySlice = new Query(where("username").is("john.doe"));
querySlice.fields().include("username").slice("tags", 2);
List<User> userWithSlicedTags = mongoTemplate.find(querySlice, User.class);
// User 对象中,username 有值,tags 列表将只包含 "java", "spring"。
// 6. 投影数组字段的切片 (跳过第一个元素,返回接下来的1个元素)
Query querySliceOffset = new Query(where("username").is("john.doe"));
querySliceOffset.fields().include("username").slice("tags", 1, 1); // 从索引1开始,取1个
List<User> userWithOffsetSlicedTags = mongoTemplate.find(querySliceOffset, User.class);
// User 对象中,username 有值,tags 列表将只包含 "spring"。
// 7. 投影数组字段中匹配的元素 (elemMatch)
// 假设 User 有一个 orders 数组,每个 order 是一个对象: { orderId: "...", amount: Number }
// 只返回 username 和第一个 amount 大于 100 的 order
Query queryElemMatch = new Query(where("username").is("john.doe"));
queryElemMatch.fields().include("username").elemMatch("orders", where("amount").gt(100));
List<User> userWithMatchingOrder = mongoTemplate.find(queryElemMatch, User.class);
// User 对象中,username 有值,orders 列表将只包含第一个 amount > 100 的订单对象,
// 如果没有匹配的,orders 字段可能为 null 或空列表,具体取决于 MongoDB 版本和驱动行为。
投影到不同的类型 (DTOs 或接口):
通常,当你使用投影时,mongoTemplate.find()
方法的第二个参数仍然是你的完整实体类 (如 User.class
)。Spring Data MongoDB 会尝试将投影后的结果映射回这个实体类,未被投影的字段会是 null
(对于对象类型) 或其类型的默认值 (对于基本类型)。
然而,在某些情况下,你可能希望将投影结果直接映射到一个更小的 DTO (Data Transfer Object) 类或接口,特别是当投影的字段集与原始实体差异很大时。
方法一:使用实体类,未投影字段为 null (如上例)
这是最直接的方式。
方法二:查询时指定不同的结果类型 (需要结果文档结构与DTO匹配)
如果投影后的文档结构恰好与某个 DTO 类的字段完全对应,你可以这样做:
public class UserSummaryDTO {
private String username;
private String email;
// Getters and setters
// 构造函数需要匹配MongoDB返回的字段名,或者使用 @PersistenceConstructor
// 或者确保有默认构造函数且Spring能通过setter注入
public UserSummaryDTO(String username, String email) {
this.username = username;
this.email = email;
}
// 或者
// public UserSummaryDTO() {}
}
Query queryDto = new Query(where("status").is("ACTIVE"));
queryDto.fields().include("username").include("email").exclude("_id"); // 确保投影字段与 DTO 匹配
List<UserSummaryDTO> userSummaries = mongoTemplate.find(queryDto, UserSummaryDTO.class);
// 如果 UserSummaryDTO 有一个接受 Document 的构造函数或字段与投影匹配,这可能工作。
// 更可靠的是确保字段名和类型匹配,并且有合适的构造函数或 setter。
注意: 这种方式要求投影后的 BSON 文档能被 Spring Data MongoDB 的 MappingMongoConverter
直接转换成 UserSummaryDTO
。
@Field
注解)。@PersistenceConstructor
标记的构造函数,其参数名与投影字段名匹配) 或默认构造函数加 setter 方法。方法三:使用 Spring Data Repositories 的接口投影 (更推荐的方式)
如果你使用 Spring Data Repositories,接口投影是一个非常优雅和强大的方式:
// 1. 定义一个投影接口
public interface UserProjection {
String getUsername();
String getEmail();
// AddressProjection getAddress(); // 也可以投影嵌套对象
// @Value("#{target.firstName + ' ' + target.lastName}") // SpEL 表达式
// String getFullName();
}
// public interface AddressProjection {
// String getCity();
// }
// 2. 在 Repository 方法中使用该接口作为返回类型
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
public interface UserRepository extends MongoRepository<User, String> {
List<UserProjection> findByStatus(String status); // Spring Data 会自动处理投影
// 也可以结合 @Query 注解指定投影
// @Query(value = "{ 'status': ?0 }", fields = "{ 'username': 1, 'email': 1, '_id': 0 }")
// List findUserSummariesByStatus(String status);
}
// 在 Service 中使用:
// @Autowired
// private UserRepository userRepository;
//
// List projections = userRepository.findByStatus("ACTIVE");
// for (UserProjection p : projections) {
// System.out.println(p.getUsername() + " - " + p.getEmail());
// }
Spring Data 会自动根据接口中的 getter 方法名来确定需要投影哪些字段。这是最灵活且类型安全的方式之一。
方法四:使用聚合框架 (Aggregation Framework) 中的 $project
对于更复杂的投影需求,例如重命名字段、计算新字段、重塑文档结构等,聚合框架的 $project
阶段是更强大的工具。
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.where;
// MongoTemplate mongoTemplate = ...;
ProjectionOperation projectOperation = project("username", "email") // 包含 username 和 email
.andExclude("_id") // 排除 _id
.and("address.city").as("cityOfAddress"); // 将 address.city 投影为 cityOfAddress
// .andExpression("firstName + ' ' + lastName").as("fullName"); // 计算新字段 (需要MongoDB 4.4+)
Aggregation aggregation = newAggregation(
match(where("status").is("ACTIVE")), // 过滤条件
projectOperation
);
// 指定聚合结果的类型 (可以是 DTO)
AggregationResults<UserSummaryDTOWithCity> results = mongoTemplate.aggregate(aggregation, "users", UserSummaryDTOWithCity.class);
List<UserSummaryDTOWithCity> dtos = results.getMappedResults();
// DTO 示例
// public class UserSummaryDTOWithCity {
// private String username;
// private String email;
// private String cityOfAddress; // 对应投影中的 as("cityOfAddress")
// // Getters, setters, constructor
// }
总结:
Query.fields()
是 MongoTemplate
中最直接的方法。_id
默认包含,除非显式排除。include
和 exclude
(除了 _id
)。null
或默认值。$project
阶段。选择哪种投影方式取决于你的具体需求、查询的复杂性以及你是否在使用 Spring Data Repositories。投影是优化 MongoDB 查询性能和减少数据传输的重要手段。