mongoTempalte 什么是投影 (Projection)?如何只选择需要的字段返回?

在 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 的元素。

基本规则:

  1. _id 字段默认总是被包含,除非你显式地使用 fields().exclude("_id") 排除它。
  2. 不能混合使用 includeexclude 针对顶级字段 (除了 _id 的特殊情况)。
    • 如果你使用 include("fieldA", "fieldB"),则只有 fieldAfieldB_id (除非排除) 会被返回。
    • 如果你使用 exclude("fieldC", "fieldD"),则除了 fieldCfieldD 之外的所有字段都会被返回。

示例:

假设我们有以下 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

  • DTO 类的字段名需要与投影中 包含 的字段名一致 (考虑 @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 默认包含,除非显式排除。
  • 不能混合顶级字段的 includeexclude (除了 _id)。
  • 投影结果通常映射回原始实体类,未包含的字段为 null 或默认值。
  • 为了更好的类型安全和灵活性,特别是当使用 Repository 时,接口投影是一个非常好的选择。
  • 对于复杂的转换、计算字段或重塑文档结构,应使用聚合框架的 $project 阶段

选择哪种投影方式取决于你的具体需求、查询的复杂性以及你是否在使用 Spring Data Repositories。投影是优化 MongoDB 查询性能和减少数据传输的重要手段。

你可能感兴趣的:(MongoDB实战系列,投影,mongodb)