eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

1. Introduction

In this tutorial, we’ll explore how to create queries with multiple criteria in MongoDB using Spring Data JPA.

2. Setting up the Project

To start, we need to include the necessary dependencies in our project. We’ll add the Spring Data MongoDB starter dependency to our pom.xml file:

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

This dependency allows us to use Spring Data MongoDB functionalities in our Spring Boot project.

2.1. Defining the MongoDB Document and Repository

Next, we define a MongoDB document, which is a Java class annotated with @Document. This class maps to a collection in MongoDB. For example, let’s create a Product document:

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

    // Getters and setters
}

In Spring Data MongoDB, we can create a custom repository to define our own query methods. By injecting MongoTemplate, we can perform advanced operations on the MongoDB database. This class provides a rich set of methods for executing queries, aggregating data, and handling CRUD operations effectively:

@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. Sample Data in MongoDB

Before we begin writing queries, let’s assume we have the following sample data in our 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
    }
]

This data will help us test our queries effectively.

3. Building MongoDB Queries

When constructing complex queries in Spring Data MongoDB, we leverage methods like andOperator() and orOperator() to combine multiple conditions effectively. These methods are crucial for creating queries that require documents to satisfy multiple conditions simultaneously or alternatively.

3.1. Using addOperator()

The andOperator() method is used to combine multiple criteria with an AND operator. This means that all the criteria must be true for a document to match the query. This is useful when we need to enforce that multiple conditions are met.

Here’s how we can construct this query using the 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);
}

Suppose we want to retrieve a laptop named “MacBook Pro M3” with a price greater than $1000 and ensure it’s available in stock:

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. Using orOperator()

Conversely, the orOperator() method combines multiple criteria with an OR operator. This means that any one of the specified criteria must be true for a document to match the query. This is useful when retrieving documents that match at least one of several conditions.

Here’s how we can construct this query using the 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);
}

If we want to retrieve products that either belong to the “Laptop” category or have a price greater than $1000, we can invoke the method:

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

3.3. Combining andOperator() and orOperator()

We can create complex queries by combining both andOperator() and orOperator() methods:

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);
}

In this method, we create a Query object and use the orOperator() to define the main structure of our criteria. Within this, we specify two conditions using andOperator(). For instance, we can retrieve products that either belong to the “Laptop” category with a price greater than $1000 or are named “MacBook Pro M3″ and are available in stock:

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

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

3.4. Using Chain Methods

Moreover, we can utilize the Criteria class to construct queries in a fluent style by chaining multiple conditions together using and() method. This approach provides a clear and concise way to define complex queries without losing readability:

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);
}

When invoking this method, we expect to find one product named “MacBook Pro M3” that costs more than $1000 and is available in stock:

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

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

4. @Query Annotation for Multiple Criteria

In addition to our custom repository using MongoTemplate, we can create a new repository interface that extends MongoRepository to utilize the @Query annotation for multiple criteria queries. This approach allows us to define complex queries directly in our repository without needing to build them programmatically.

We can define a custom method in our ProductRepository interface:

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);
}

The first method, findProductsByNamePriceCategoryAndAvailability(), retrieves products that match all specified criteria. This includes the exact name of the product, a price greater than a specified minimum, the category the product belongs to, and whether the product is available in stock:

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

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

On the other hand, the second method, findProductsByCategoryAndAvailabilityOrPrice(), offers a more flexible approach. It finds products that either belong to a specific category and are available or have a price greater than the specified minimum:

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

assertThat(actualProducts).hasSize(3);

5. Using QueryDSL

QueryDSL is a framework that allows us to construct type-safe queries programmatically. Let’s walk through setting up and using QueryDSL for handling multiple criteria queries in our Spring Data MongoDB project.

5.1. Adding QueryDSL Dependency

First, we need to include QueryDSL in our project. We can do this by adding the following dependency to our pom.xml file:

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

5.2. Generating Q Classes

QueryDSL requires generating helper classes for our domain objects. These classes, typically named with a “Q” prefix (e.g., QProduct), provide type-safe access to our entity fields. We can automate this generation process using the Maven plugin:

<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>

When the build process runs this configuration, the annotation processor generates Q classes for each of our MongoDB documents. For instance, if we have a Product class, it generates a QProduct class. This QProduct class provides type-safe access to the fields of the Product entity, allowing us to construct queries in a more structured and error-free way using QueryDSL.

Next, we need to modify our repository to extend QuerydslPredicateExecutor:

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

5.3. Using AND with QueryDSL

In QueryDSL, we can construct complex queries using the Predicate interface, which represents a boolean expression. The and() method allows us to combine multiple conditions, ensuring that all specified criteria are satisfied for a document to match the query:

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());
}

In this method, we first create an instance of QProduct. We then construct a Predicate that combines several conditions using the and() method. Finally, we execute the query using productRepository.findAll(predicate), which retrieves all matching products based on the constructed 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. Using OR With QueryDSL

We can also construct queries using the or() method, which allows us to combine multiple conditions with a logical OR operator. This means that a document matches the query if any of the specified criteria are satisfied.

Let’s create a method that finds products using QueryDSL with an OR condition:

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());
}

The or() method ensures that a product matches the query if any of these conditions are true:

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

assertThat(actualProducts).hasSize(2);

5.4. Combining AND and OR With QueryDSL

We can also combine both and() and or() methods within our predicates. This flexibility allows us to specify conditions where some criteria must be true while others can be alternative conditions. Here’s an example of how to combine and() and or() in a single query:

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());
}

In this method, we construct the query by combining conditions with and() and or(). This allows us to build a query that matches products either in a specific category with a price greater than a specified amount or products with a specific name that are available:

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

6. Conclusion

In this article, we’ve explored various approaches for constructing queries with multiple criteria in Spring Data MongoDB. For straightforward queries with a few criteria, Criteria or chain methods might be sufficient due to their simplicity. However, if the queries involve complex logic with multiple conditions and nesting, using @Query annotations or QueryDSL is generally recommended due to their improved readability and maintainability.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)