Spring Data MongoDB
Introduction
Spring Data MongoDB is a part of the larger Spring Data family that makes it easy to integrate MongoDB — a popular NoSQL database — with your Spring applications. MongoDB stores data in flexible, JSON-like documents, which means fields can vary from document to document and data structure can change over time. This flexibility makes MongoDB a great choice for certain types of applications.
Spring Data MongoDB provides familiar Spring abstractions and idioms over MongoDB, eliminating much of the boilerplate code required for data access layers. Whether you're building web applications, microservices, or enterprise applications, Spring Data MongoDB offers convenient tools to simplify your development process.
In this guide, you'll learn how to:
- Configure Spring Data MongoDB in your application
- Create document models and repositories
- Perform CRUD operations and queries
- Work with MongoDB-specific features
Getting Started with Spring Data MongoDB
Adding Dependencies
To use Spring Data MongoDB in your Spring Boot project, you need to add the appropriate dependency to your build file.
For Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
For Gradle:
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
Basic Configuration
Spring Boot provides auto-configuration for MongoDB. The simplest way to configure MongoDB is through properties in your application.properties
or application.yml
file:
# MongoDB connection properties
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mydatabase
# Optional authentication
# spring.data.mongodb.username=user
# spring.data.mongodb.password=secret
Alternatively, you can use a MongoDB connection URI:
spring.data.mongodb.uri=mongodb://localhost:27017/mydatabase
Working with MongoDB Documents
Creating Document Models
Documents in MongoDB are represented as POJOs (Plain Old Java Objects) in your Spring application. These classes are annotated with MongoDB-specific annotations:
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.util.Date;
@Document(collection = "products")
public class Product {
@Id
private String id;
private String name;
@Field("product_price")
private Double price;
private String category;
private Date createdAt;
// Constructors, getters, and setters
public Product() {
}
public Product(String name, Double price, String category) {
this.name = name;
this.price = price;
this.category = category;
this.createdAt = new Date();
}
// Getters and setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
@Override
public String toString() {
return "Product{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", price=" + price +
", category='" + category + '\'' +
", createdAt=" + createdAt +
'}';
}
}
Key annotations used:
@Document
: Marks the class as a MongoDB document and specifies the collection name@Id
: Designates the field as the document's ID@Field
: Maps the field to a specific name in the MongoDB document
Creating MongoDB Repositories
Spring Data MongoDB provides a repository abstraction that reduces boilerplate code. Create a repository interface that extends MongoRepository
:
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
public interface ProductRepository extends MongoRepository<Product, String> {
// Spring Data will automatically implement these methods based on the method names
List<Product> findByCategory(String category);
List<Product> findByPriceLessThan(Double price);
List<Product> findByCategoryAndPriceBetween(String category, Double minPrice, Double maxPrice);
}
By extending MongoRepository
, you automatically get methods for CRUD operations and other common data access patterns. The interface is typed with your document class and the ID type.
Performing MongoDB Operations
Basic CRUD Operations
Once you have your repository, you can perform CRUD operations:
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// Create
public Product saveProduct(Product product) {
return productRepository.save(product);
}
// Read
public List<Product> getAllProducts() {
return productRepository.findAll();
}
public Optional<Product> getProductById(String id) {
return productRepository.findById(id);
}
// Update (save method is used for both create and update)
public Product updateProduct(Product product) {
return productRepository.save(product);
}
// Delete
public void deleteProduct(String id) {
productRepository.deleteById(id);
}
}
Testing the Repository
Here's an example of how you might use your repository in a test or application:
@SpringBootApplication
public class MongoDbDemoApplication implements CommandLineRunner {
@Autowired
private ProductRepository productRepository;
public static void main(String[] args) {
SpringApplication.run(MongoDbDemoApplication.class, args);
}
@Override
public void run(String... args) {
// Clear previous test data
productRepository.deleteAll();
// Create a few products
productRepository.save(new Product("Laptop", 1299.99, "Electronics"));
productRepository.save(new Product("Phone", 899.99, "Electronics"));
productRepository.save(new Product("Desk", 349.99, "Furniture"));
// Display all products
System.out.println("Products found with findAll():");
System.out.println("-----------------------------");
productRepository.findAll().forEach(System.out::println);
System.out.println();
// Find by category
System.out.println("Electronics products:");
System.out.println("--------------------");
productRepository.findByCategory("Electronics").forEach(System.out::println);
System.out.println();
// Find by price
System.out.println("Affordable products (under $500):");
System.out.println("-------------------------------");
productRepository.findByPriceLessThan(500.0).forEach(System.out::println);
}
}
Output:
Products found with findAll():
-----------------------------
Product{id='64a1e8f7a2b3c41d3f9e8b72', name='Laptop', price=1299.99, category='Electronics', createdAt=Sat Jul 02 15:23:19 UTC 2023}
Product{id='64a1e8f7a2b3c41d3f9e8b73', name='Phone', price=899.99, category='Electronics', createdAt=Sat Jul 02 15:23:19 UTC 2023}
Product{id='64a1e8f7a2b3c41d3f9e8b74', name='Desk', price=349.99, category='Furniture', createdAt=Sat Jul 02 15:23:19 UTC 2023}
Electronics products:
--------------------
Product{id='64a1e8f7a2b3c41d3f9e8b72', name='Laptop', price=1299.99, category='Electronics', createdAt=Sat Jul 02 15:23:19 UTC 2023}
Product{id='64a1e8f7a2b3c41d3f9e8b73', name='Phone', price=899.99, category='Electronics', createdAt=Sat Jul 02 15:23:19 UTC 2023}
Affordable products (under $500):
-------------------------------
Product{id='64a1e8f7a2b3c41d3f9e8b74', name='Desk', price=349.99, category='Furniture', createdAt=Sat Jul 02 15:23:19 UTC 2023}
Advanced MongoDB Features
Custom Queries with @Query Annotation
For more complex queries, you can use the @Query
annotation:
public interface ProductRepository extends MongoRepository<Product, String> {
@Query("{ 'category': ?0, 'price': { $lt: ?1 } }")
List<Product> findByCategoryAndPriceLower(String category, Double price);
@Query("{ 'name': { $regex: ?0, $options: 'i' } }")
List<Product> findByNameContainingIgnoreCase(String nameFragment);
}
Using MongoTemplate for Complex Operations
When repositories aren't enough, you can use MongoTemplate
for more complex operations:
@Service
public class AdvancedProductService {
private final MongoTemplate mongoTemplate;
@Autowired
public AdvancedProductService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public List<Product> findProductsByMultipleCriteria(String nameFragment, List<String> categories, Double maxPrice) {
Query query = new Query();
List<Criteria> criteria = new ArrayList<>();
if (nameFragment != null && !nameFragment.isEmpty()) {
criteria.add(Criteria.where("name").regex(nameFragment, "i"));
}
if (categories != null && !categories.isEmpty()) {
criteria.add(Criteria.where("category").in(categories));
}
if (maxPrice != null) {
criteria.add(Criteria.where("price").lte(maxPrice));
}
if (!criteria.isEmpty()) {
query.addCriteria(new Criteria().andOperator(criteria.toArray(new Criteria[0])));
}
return mongoTemplate.find(query, Product.class);
}
public void updatePriceForCategory(String category, double multiplier) {
Query query = Query.query(Criteria.where("category").is(category));
Update update = new Update().multiply("price", multiplier);
mongoTemplate.updateMulti(query, update, Product.class);
}
}
Working with Aggregations
MongoDB's aggregation framework is powerful for data analysis. Here's an example using Spring Data:
@Service
public class ProductAnalyticsService {
private final MongoTemplate mongoTemplate;
@Autowired
public ProductAnalyticsService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public Map<String, Double> getAveragePriceByCategory() {
TypedAggregation<Product> aggregation = Aggregation.newAggregation(
Product.class,
Aggregation.group("category").avg("price").as("averagePrice")
);
AggregationResults<CategoryPrice> results = mongoTemplate.aggregate(
aggregation,
CategoryPrice.class
);
Map<String, Double> categoryPriceMap = new HashMap<>();
for (CategoryPrice result : results.getMappedResults()) {
categoryPriceMap.put(result.getCategory(), result.getAveragePrice());
}
return categoryPriceMap;
}
// Helper class for aggregation results
private static class CategoryPrice {
private String category;
private Double averagePrice;
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public Double getAveragePrice() {
return averagePrice;
}
public void setAveragePrice(Double averagePrice) {
this.averagePrice = averagePrice;
}
}
}
Real-world Application: Building a Product Catalog API
Let's combine what we've learned to build a simple REST API for a product catalog:
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
private final AdvancedProductService advancedProductService;
@Autowired
public ProductController(ProductService productService, AdvancedProductService advancedProductService) {
this.productService = productService;
this.advancedProductService = advancedProductService;
}
@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable String id) {
return productService.getProductById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
// Set creation date
product.setCreatedAt(new Date());
return productService.saveProduct(product);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable String id, @RequestBody Product product) {
return productService.getProductById(id)
.map(existingProduct -> {
product.setId(id);
// Preserve creation date
product.setCreatedAt(existingProduct.getCreatedAt());
return ResponseEntity.ok(productService.updateProduct(product));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable String id) {
return productService.getProductById(id)
.map(product -> {
productService.deleteProduct(id);
return ResponseEntity.ok().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/search")
public List<Product> searchProducts(
@RequestParam(required = false) String name,
@RequestParam(required = false) List<String> categories,
@RequestParam(required = false) Double maxPrice) {
return advancedProductService.findProductsByMultipleCriteria(name, categories, maxPrice);
}
@PatchMapping("/category/{category}/price")
public ResponseEntity<Void> updateCategoryPrices(
@PathVariable String category,
@RequestParam double multiplier) {
advancedProductService.updatePriceForCategory(category, multiplier);
return ResponseEntity.ok().build();
}
}
Best Practices for Spring Data MongoDB
-
Document Design: Design your documents carefully. Embedded documents are great for data that's always accessed together.
-
Indexing: Add indexes to frequently queried fields to improve performance:
java@Document(collection = "products")
@CompoundIndex(name = "category_price_idx", def = "{'category': 1, 'price': -1}")
public class Product {
// fields
} -
Pagination: Use pagination for large result sets:
javaPage<Product> findByCategory(String category, Pageable pageable);
// Usage
Page<Product> productPage = productRepository.findByCategory("Electronics",
PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "price"))); -
Use DTOs: Separate your MongoDB documents from your API responses:
javapublic class ProductDTO {
private String id;
private String name;
private Double price;
private String category;
// Constructor, getters, setters
}
// Convert document to DTO
private ProductDTO convertToDTO(Product product) {
return new ProductDTO(
product.getId(),
product.getName(),
product.getPrice(),
product.getCategory()
);
} -
Proper Error Handling: Catch and handle MongoDB-specific exceptions:
javatry {
return productRepository.save(product);
} catch (MongoWriteException e) {
// Handle duplicate key or other MongoDB-specific errors
throw new RuntimeException("Failed to save product: " + e.getMessage(), e);
}
Summary
Spring Data MongoDB provides a powerful and convenient way to integrate MongoDB with your Spring applications. We've covered:
- Configuring Spring Data MongoDB in a Spring Boot application
- Creating document models with appropriate annotations
- Building repositories that extend MongoRepository
- Performing CRUD operations and implementing custom queries
- Using advanced features like MongoTemplate and aggregations
- Building a real-world REST API for a product catalog
Spring Data MongoDB abstracts away much of the complexity of working directly with the MongoDB driver while still providing access to MongoDB's powerful features when needed. This combination makes it an excellent choice for applications that require the flexibility of a document database with the convenience of Spring's ecosystem.
Additional Resources and Exercises
Resources
Exercises
-
Basic Repository Exercise: Create a
Customer
document model and corresponding repository with methods to find customers by name and creation date. -
Relationship Exercise: Extend the product catalog with a
Review
document that has a reference to aProduct
. Implement methods to add reviews to products and query products by their average rating. -
Aggregation Exercise: Implement an aggregation to find the top 5 most expensive products in each category.
-
Geospatial Exercise: Create a
Store
document with geospatial coordinates and implement queries to find stores within a certain distance of a location. -
Transaction Exercise: Implement a service that uses MongoDB transactions to transfer inventory between stores, ensuring that inventory counts remain valid.
By working through these exercises, you'll gain practical experience with Spring Data MongoDB and be well on your way to building robust applications with MongoDB.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)