We've come a long way - from basic MongoDB commands to Spring Data repositories, and then to complex relationships. Now it's time I show you how to unleash the full power of MongoDB queries. When repository methods aren't enough, the Criteria API and MongoTemplate can be very helpful.
This article is part of our comprehensive 4-part MongoDB with Spring Boot series.
Part 1: Getting Started with MongoDB: Installation and Basic Commands
Part 2: MongoDB with Spring Boot using Spring-Data-MongoDB
Part 3: MongoDB Relationships in Spring Boot
Part 4 (Current): Advanced MongoDB Queries: Mastering Criteria API and MongoTemplate
Repository methods are great for simple queries, but real-world applications often need:
- Dynamic queries based on user input
- Complex conditions with multiple optional parameters
- Aggregations and data transformations
- Bulk operations and updates
This is where MongoTemplate and Criteria API shine. They give you the flexibility of MongoDB's native query language with the type safety of Java.
MongoTemplate is the core class for interacting with MongoDB. It's automatically configured by Spring Boot:
@Service
public class OrderService {
@Autowired
private MongoTemplate mongoTemplate;
public List<Order> findOrdersWithTemplate() {
return mongoTemplate.findAll(Order.class);
}
}
But that's just scratching the surface. Let's dive into the powerful features.
Criteria API lets you build queries programmatically. Think of it as a query builder that adapts to your needs.
Let's start with simple criteria:
@Test
void basicCriteriaQuery() {
Query query = new Query();
query.addCriteria(Criteria.where("status").is("pending"));
List<Order> orders = mongoTemplate.find(query, Order.class);
}
Real queries often have multiple conditions:
@Test
void multipleCriteria() {
Criteria criteria = Criteria.where("status").is("delivered")
.and("totalPrice").gte(100)
.and("quantity").lt(5);
Query query = new Query(criteria);
List<Order> orders = mongoTemplate.find(query, Order.class);
}
Sometimes you need OR logic:
@Test
void orConditions() {
Criteria criteria = new Criteria().orOperator(
Criteria.where("status").is("pending"),
Criteria.where("status").is("processing")
);
Query query = new Query(criteria);
List<Order> orders = mongoTemplate.find(query, Order.class);
assertTrue(orders.stream()
.allMatch(o -> o.getStatus().equals("pending") ||
o.getStatus().equals("processing"))
);
}
For complex business logic, combine AND and OR:
@Test
void complexNestedQuery() {
Criteria criteria = new Criteria().andOperator(
Criteria.where("totalPrice").gte(50),
new Criteria().orOperator(
Criteria.where("status").is("pending"),
Criteria.where("quantity").gt(10)
)
);
Query query = new Query(criteria);
List<Order> orders = mongoTemplate.find(query, Order.class);
// Orders with price >= 50 AND (status = pending OR quantity > 10)
}
Here's where Criteria API really shines - building queries based on user input:
@Service
public class OrderSearchService {
@Autowired
private MongoTemplate mongoTemplate;
public List<Order> searchOrders(OrderSearchCriteria searchCriteria) {
Query query = new Query();
if (searchCriteria.getStatus() != null) {
query.addCriteria(Criteria.where("status")
.is(searchCriteria.getStatus()));
}
if (searchCriteria.getMinPrice() != null) {
query.addCriteria(Criteria.where("totalPrice")
.gte(searchCriteria.getMinPrice()));
}
if (searchCriteria.getMaxPrice() != null) {
query.addCriteria(Criteria.where("totalPrice")
.lte(searchCriteria.getMaxPrice()));
}
if (searchCriteria.getCity() != null) {
query.addCriteria(Criteria.where("address.city")
.is(searchCriteria.getCity()));
}
if (searchCriteria.getDateFrom() != null) {
query.addCriteria(Criteria.where("createdAt")
.gte(searchCriteria.getDateFrom()));
}
// Add sorting
if (searchCriteria.getSortBy() != null) {
Sort.Direction direction = searchCriteria.isAscending() ?
Sort.Direction.ASC : Sort.Direction.DESC;
query.with(Sort.by(direction, searchCriteria.getSortBy()));
}
// Add pagination
if (searchCriteria.getPage() != null && searchCriteria.getSize() != null) {
Pageable pageable = PageRequest.of(
searchCriteria.getPage(),
searchCriteria.getSize()
);
query.with(pageable);
}
return mongoTemplate.find(query, Order.class);
}
}
@Test
void queryEmbeddedDocument() {
// Find orders in specific cities
Query query = new Query(
Criteria.where("address.city").in("Mumbai", "Delhi", "Bangalore")
);
List<Order> orders = mongoTemplate.find(query, Order.class);
// Find orders with specific zip code pattern
Query zipQuery = new Query(
Criteria.where("address.zipCode").regex("^560.*")
);
List<Order> bangaloreOrders = mongoTemplate.find(zipQuery, Order.class);
}
Querying referenced documents requires special handling:
@Test
void queryWithDBRef() {
// First, find the product
Product product = mongoTemplate.findOne(
Query.query(Criteria.where("name").is("Gaming Laptop")),
Product.class
);
// Then query orders containing this product
Query query = new Query(
Criteria.where("products.$id").is(new ObjectId(product.getId()))
);
List<Order> orders = mongoTemplate.find(query, Order.class);
}
Text Search Capabilities#
MongoDB supports full-text search. First, create a text index:
@Document(collection = "products")
@TextIndexed
public class Product {
@TextIndexed(weight = 2)
private String name;
@TextIndexed
private String description;
// other fields...
}
Then search using text criteria:
@Test
void textSearch() {
TextCriteria criteria = TextCriteria
.forDefaultLanguage()
.matchingAny("wireless", "bluetooth");
Query query = TextQuery.queryText(criteria)
.sortByScore();
List<Product> products = mongoTemplate.find(query, Product.class);
}
Update specific fields without fetching the document:
@Test
void updateSingleDocument() {
Query query = new Query(Criteria.where("id").is(orderId));
Update update = new Update()
.set("status", "shipped")
.inc("quantity", 1)
.currentDate("updatedAt");
UpdateResult result = mongoTemplate.updateFirst(query, update, Order.class);
assertEquals(1, result.getModifiedCount());
}
Update multiple documents at once:
@Test
void bulkUpdate() {
Query query = new Query(
Criteria.where("status").is("pending")
.and("createdAt").lt(LocalDateTime.now().minusDays(7))
);
Update update = new Update()
.set("status", "cancelled")
.set("cancellationReason", "Order timeout");
UpdateResult result = mongoTemplate.updateMulti(query, update, Order.class);
System.out.println("Updated " + result.getModifiedCount() + " orders");
}
Create or update in one operation:
@Test
void upsertOperation() {
Query query = new Query(Criteria.where("id").is("special-order-123"));
Update update = new Update()
.set("status", "vip")
.setOnInsert("createdAt", LocalDateTime.now())
.inc("updateCount", 1);
UpdateResult result = mongoTemplate.upsert(query, update, Order.class);
if (result.getUpsertedId() != null) {
System.out.println("Created new order");
} else {
System.out.println("Updated existing order");
}
}
Optimize queries by fetching only needed fields:
@Test
void projectionExample() {
Query query = new Query();
query.fields()
.include("status")
.include("totalPrice")
.exclude("_id");
List<Order> orders = mongoTemplate.find(query, Order.class);
// Only status and totalPrice are populated
}
For nested fields:
@Test
void nestedProjection() {
Query query = new Query();
query.fields()
.include("status")
.include("address.city")
.include("address.state");
List<Order> orders = mongoTemplate.find(query, Order.class);
}
Sometimes you need the full power of native MongoDB queries:
@Test
void nativeMongoQuery() {
String jsonQuery = "{ 'status': 'pending', " +
"'$or': [ " +
"{ 'totalPrice': { '$gte': 100 } }, " +
"{ 'quantity': { '$gt': 5 } } " +
"] }";
BasicQuery query = new BasicQuery(jsonQuery);
List<Order> orders = mongoTemplate.find(query, Order.class);
}
Congratulations! You've completed my comprehensive MongoDB with Spring Boot series.
You're no longer just a MongoDB user - you're a MongoDB developer capable of building sophisticated, performant applications. The document database that seemed foreign at first is now a powerful tool in your arsenal.
Remember, MongoDB's flexibility is both its strength and responsibility. Design your documents thoughtfully, index strategically, and always consider how your application will query the data. With the skills you've gained, you're ready to tackle real-world projects that demand the scalability and flexibility of NoSQL databases.
Happy coding!
Want to Master Spring Boot and Land Your Dream Job?
Struggling with coding interviews? Learn Data Structures & Algorithms (DSA) with our expert-led course. Build strong problem-solving skills, write optimized code, and crack top tech interviews with ease
Learn moreSpring Boot
MongoDB
MongoTemplate
Criteria API