Spring Data MongoDB 构建多个条件的查询

1、简介

本文将带你了解如何使用 Spring Data JPA 在 MongoDB 中创建具有多个 Criteria(条件)的查询。

2、项目设置

首先,在 pom.xml 文件中添加 Spring Data MongoDB Starter 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
    <version>3.3.1</version>
</dependency>

通过该依赖,我们可以在 Spring Boot 项目中使用 Spring Data MongoDB 的功能。

2.1、定义 MongoDB Document 和 Repository

接下来,定义一个 MongoDB Document(文档),它是一个用 @Document 注解的 Java 类。该类映射到 MongoDB 中的一个 Collection(集合)。

例如,创建一个 Product Document:

@Document(collection = "products")
public class Product {
    @Id
    private String id;
    private String name;
    private String category;
    private double price;
    private boolean available;

    // Getter 和 Setter 方法
}

在 Spring Data MongoDB 中,我们可以创建 Repository 来定义自己的查询方法。通过注入 MongoTemplate,可以对 MongoDB 数据库执行高级操作。该类为执行查询、聚合数据和有效处理 CRUD 操作提供了丰富的方法集:

@Repository
public class CustomProductRepositoryImpl implements CustomProductRepository {
    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public List find(Query query, Class entityClass) {
        return mongoTemplate.find(query, entityClass);
    }
}

2.2、MongoDB 中的数据示例

假设我们的 MongoDB products collection 中有以下示例数据:

[
    {
        "name": "MacBook Pro M3",
        "price": 1500,
        "category": "Laptop",
        "available": true
    },
    {
        "name": "MacBook Air M2",
        "price": 1000,
        "category": "Laptop",
        "available": false
    },
    {
        "name": "iPhone 13",
        "price": 800,
        "category": "Phone",
        "available": true
    }
]

3、构建 MongoDB 查询

在 Spring Data MongoDB 中构建复杂查询时,我们利用诸如 andOperator()orOperator() 等方法来有效地组合多个条件。这些方法对于创建需要文档同时或交替满足多个条件的查询至关重要。

3.1、使用 addOperator()

andOperator() 方法用于用 AND 运算符组合多个条件。这意味着所有条件都必须为 true,文档才能与查询匹配。当我们需要强制满足多个条件时,这个方法非常有用。

假设我们要检索一款名为 MacBook Pro M3、价格超过 1000 美元的笔记本电脑,并确保它有库存。

使用 andOperator() 构建查询:

List<Product> findProductsUsingAndOperator(String name, int minPrice, String category, boolean available) {
    Query query = new Query();
    query.addCriteria(new Criteria().andOperator(Criteria.where("name")
      .is(name), Criteria.where("price")
      .gt(minPrice), Criteria.where("category")
      .is(category), Criteria.where("available")
      .is(available)));
   return customProductRepository.find(query, Product.class);
}

测试查询方法:

List<Product> actualProducts = productService.findProductsUsingAndOperator("MacBook Pro M3", 1000, "Laptop", true);

assertThat(actualProducts).hasSize(1);
assertThat(actualProducts.get(0).getName()).isEqualTo("MacBook Pro M3");

3.2、使用 orOperator()

相反,orOperator() 方法将多个条件与 OR 运算符结合起来。这意味着任何一个指定的条件必须为 true,文档才能与查询匹配。这在检索至少符合多个条件之一的文档时非常有用。

例如,要检索属于 Laptop 类别或价格超过 1000 美元的产品。

使用 orOperator() 构造该查询:

List<Product> findProductsUsingOrOperator(String category, int minPrice) {
    Query query = new Query();
    query.addCriteria(new Criteria().orOperator(Criteria.where("category")
      .is(category), Criteria.where("price")
      .gt(minPrice)));

    return customProductRepository.find(query, Product.class);
}

测试查询方法:

actualProducts = productService.findProductsUsingOrOperator("Laptop", 1000);
assertThat(actualProducts).hasSize(2);

3.3、结合 andOperator() 和 orOperator()

我们可以结合 andOperator()orOperator() 方法来创建更复杂的查询:

List<Product> findProductsUsingAndOperatorAndOrOperator(String category1, int price1, String name1, boolean available1) {
    Query query = new Query();
    query.addCriteria(new Criteria().orOperator(
      new Criteria().andOperator(
        Criteria.where("category").is(category1),
        Criteria.where("price").gt(price1)),
      new Criteria().andOperator(
        Criteria.where("name").is(name1),
        Criteria.where("available").is(available1)
      )
    ));

    return customProductRepository.find(query, Product.class);
}

如上,创建一个 Query 对象,并使用 orOperator() 定义条件的主要结构。其中,使用 andOperator() 指定了两个条件。检索属于 “Laptop” 类别且价格超过 1000 美元或名为 “MacBook Pro M3” 且有库存的产品:

测试如下:

actualProducts = productService.findProductsUsingAndOperatorAndOrOperator("Laptop", 1000, "MacBook Pro M3", true);

assertThat(actualProducts).hasSize(1);
assertThat(actualProducts.get(0).getName()).isEqualTo("MacBook Pro M3");

3.4、链式调用

此外,我们还可以利用 Criteria 类,通过使用 and() 方法将多个条件串联起来,构建 Fluent 风格的查询。这种方法提供了一种简洁明了的方式来定义复杂的查询,同时又不失可读性。

例如,我们要检索名为 “MacBook Pro M3” 的笔记本电脑,其价格超过 1000 美元,并且有库存:

List<Product> findProductsUsingChainMethod(String name1, int price1, String category1, boolean available1) {
    Criteria criteria = Criteria.where("name").is(name1)
      .and("price").gt(price1)
      .and("category").is(category1)
      .and("available").is(available1);
    return customProductRepository.find(new Query(criteria), Product.class);
}

测试如下:

actualProducts = productService.findProductsUsingChainMethod("MacBook Pro M3", 1000, "Laptop", true);

assertThat(actualProducts).hasSize(1);
assertThat(actualProducts.get(0).getName()).isEqualTo("MacBook Pro M3");

4、使用 @Query 注解进行多条件查询

除了使用 MongoTemplate 自定义 Repository 外,我们还可以创建一个新的 Repository 接口,继承 MongoRepository,利用 @Query 注解进行多条件查询。通过这种方法,我们可以直接在 Repository 中定义复杂的查询,而无需以编程方式构建查询。

ProductRepository 接口中定义一个自定义方法:

public interface ProductRepository extends MongoRepository<Product, String> {
    @Query("{ 'name': ?0, 'price': { $gt: ?1 }, 'category': ?2, 'available': ?3 }")
    List<Product> findProductsByNamePriceCategoryAndAvailability(String name, double minPrice, String category, boolean available);
    
    @Query("{ $or: [{ 'category': ?0, 'available': ?1 }, { 'price': { $gt: ?2 } } ] }")
    List<Product> findProductsByCategoryAndAvailabilityOrPrice(String category, boolean available, double minPrice);
}

第一个方法是 findProductsByNamePriceCategoryAndAvailability(),用于检索符合所有指定条件的产品。这包括产品的准确名称、大于指定最小值的价格、产品所属类别以及产品是否有库存。

测试如下:

actualProducts = productRepository.findProductsByNamePriceCategoryAndAvailability("MacBook Pro M3", 1000, "Laptop",  true);

assertThat(actualProducts).hasSize(1);
assertThat(actualProducts.get(0).getName()).isEqualTo("MacBook Pro M3");

第二个方法 findProductsByCategoryAndAvailabilityOrPrice() 提供了一种更灵活的方式。它能检索到属于特定类别、可用或价格高于指定最低值的产品。

测试如下:

actualProducts = productRepository.findProductsByCategoryAndAvailabilityOrPrice("Laptop", false, 600);

assertThat(actualProducts).hasSize(3);

5、使用 QueryDSL

QueryDSL 是一个允许我们以编程方式构建类型安全查询的框架。

5.1、添加 QueryDSL 依赖

首先,在 pom.xml 文件中添加 QueryDSL 依赖:

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-mongodb</artifactId>
    <version>5.1.0</version>
</dependency>

5.2、生成 “Q” 查询类

在 QueryDSL 中,我们需要为 Domain 对象生成辅助类。这些类通常以 “Q” 前缀命名(如 QProduct),提供对实体字段的类型安全的访问。

我们可以使用 Maven 插件自动化生成这些类的过程:

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

当构建过程运行此配置时,注解处理器会为每个 MongoDB 文档生成相应的 Q 类。例如,如果我们有一个 Product 类,它会生成一个 QProduct 类。这个 QProduct 类提供了对 Product 实体字段的类型安全的访问,允许我们使用 QueryDSL 以更结构化和无差错的方式构建查询:

接下来,修改 Repository,使其继承 QuerydslPredicateExecutor

public interface ProductRepository extends MongoRepository<Product, String>, QuerydslPredicateExecutor<Product> {
}

5.3、QueryDSL 使用 AND 查询

在 QueryDSL 中,我们可以使用 Predicate 接口构建复杂的查询,该接口表示一个布尔表达式。and() 方法允许我们组合多个条件,确保所有指定的条件都满足才能使文档与查询匹配:

List<Product> findProductsUsingQueryDSLWithAndCondition(String category, boolean available, String name, double minPrice) {
    QProduct qProduct = QProduct.product;
    Predicate predicate = qProduct.category.eq(category)
      .and(qProduct.available.eq(available))
      .and(qProduct.name.eq(name))
      .and(qProduct.price.gt(minPrice));

    return StreamSupport.stream(productRepository.findAll(predicate).spliterator(), false)
      .collect(Collectors.toList());
}

如上,首先创建一个 QProduct 实例。然后,使用 and() 方法构建一个结合了多个条件的 Predicate。最后,使用 productRepository.findAll(predicate) 执行查询,根据构建的 Predicate 检索所有匹配的产品。

测试如下:

actualProducts = productService.findProductsUsingQueryDSLWithAndCondition("Laptop", true, "MacBook Pro M3", 1000);

assertThat(actualProducts).hasSize(1);
assertThat(actualProducts.get(0).getName()).isEqualTo("MacBook Pro M3");

5.4、QueryDSL 使用 OR 查询

我们还可以使用 or() 方法构建查询,该方法允许我们使用逻辑 OR 运算符组合多个条件。这意味着,如果满足指定条件中的任何一个,则文档符合查询条件。

创建一个方法,使用 OR 条件来查找产品:

List<Product> findProductsUsingQueryDSLWithOrCondition(String category, String name, double minPrice) {
    QProduct qProduct = QProduct.product;
    Predicate predicate = qProduct.category.eq(category)
      .or(qProduct.name.eq(name))
      .or(qProduct.price.gt(minPrice));

    return StreamSupport.stream(productRepository.findAll(predicate).spliterator(), false)
      .collect(Collectors.toList());
}

or() 方法确保如果任何一个条件为 true,产品就与查询匹配:

actualProducts = productService.findProductsUsingQueryDSLWithOrCondition("Laptop", "MacBook", 800);

assertThat(actualProducts).hasSize(2);

5.4、组合 AND 和 OR 查询

我们还可以在 Predicate 中同时使用 and()or() 方法。这种灵活性允许我们指定一些条件必须为 true,而其他条件可以是备选条件。

示例如下:

List<Product> findProductsUsingQueryDSLWithAndOrCondition(String category, boolean available, String name, double minPrice) {
    QProduct qProduct = QProduct.product;
    Predicate predicate = qProduct.category.eq(category)
      .and(qProduct.available.eq(available))
      .or(qProduct.name.eq(name).and(qProduct.price.gt(minPrice)));

    return StreamSupport.stream(productRepository.findAll(predicate).spliterator(), false)
      .collect(Collectors.toList());
}

如上,使用 and()or() 组合条件来构建查询。以匹配价格大于指定金额的特定类别中的产品,或具有特定名称的可用产品。

测试如下:

actualProducts = productService.findProductsUsingQueryDSLWithAndOrCondition("Laptop", true, "MacBook Pro M3", 1000);
assertThat(actualProducts).hasSize(3);

6、总结

本文介绍了在 Spring Data MongoDB 中使用多个条件构建查询的方法,对于简单的查询和少量条件,使用 Criteria 或链式方法可能已经足够简单明了。然而,如果查询涉及复杂的逻辑、多个条件和嵌套,通常建议使用 @Query 注解或 QueryDSL,因为它们具有更好的可读性和可维护性。


Ref:https://www.baeldung.com/spring-data-mongo-several-criteria