MongoDB with Spring Data: Complete Guide

This comprehensive guide covers MongoDB integration with Spring Data, including repositories, queries, aggregation, transactions, and best practices.

1. Basic Setup and Configuration

Dependencies (pom.xml)

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Application Configuration (application.yml)

spring:
data:
mongodb:
uri: mongodb://localhost:27017/ecommerce
auto-index-creation: true
application:
name: mongo-service
logging:
level:
org.springframework.data.mongodb.core: DEBUG
com.example.mongodemo: DEBUG
server:
port: 8080

MongoDB Configuration Class

@Configuration
@EnableMongoAuditing
public class MongoConfig {
@Bean
public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory,
MongoMappingContext mongoMappingContext) {
MappingMongoConverter converter = new MappingMongoConverter(
new DefaultDbRefResolver(mongoDatabaseFactory), mongoMappingContext);
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return new MongoTemplate(mongoDatabaseFactory, converter);
}
@Bean
public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}

2. Document Models and Entities

Basic Document Entity

@Document(collection = "users")
public class User {
@Id
private String id;
@Indexed(unique = true)
@Field("username")
@NotBlank
private String username;
@NotBlank
@Email
private String email;
@Field("full_name")
@NotBlank
private String fullName;
private Integer age;
@DBRef
private List<Address> addresses = new ArrayList<>();
private UserStatus status;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@Version
private Long version;
// Constructors
public User() {}
public User(String username, String email, String fullName, Integer age) {
this.username = username;
this.email = email;
this.fullName = fullName;
this.age = age;
this.status = UserStatus.ACTIVE;
}
// Business methods
public void addAddress(Address address) {
this.addresses.add(address);
}
public void deactivate() {
this.status = UserStatus.INACTIVE;
}
public void activate() {
this.status = UserStatus.ACTIVE;
}
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public List<Address> getAddresses() { return addresses; }
public void setAddresses(List<Address> addresses) { this.addresses = addresses; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public Long getVersion() { return version; }
}
enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED
}

Embedded Document

@Document(collection = "addresses")
public class Address {
@Id
private String id;
private String street;
private String city;
private String state;
private String zipCode;
private String country;
@Field("address_type")
private AddressType addressType;
private Boolean isPrimary = false;
// Constructors, getters, and setters
public Address() {}
public Address(String street, String city, String state, String zipCode, 
String country, AddressType addressType) {
this.street = street;
this.city = city;
this.state = state;
this.zipCode = zipCode;
this.country = country;
this.addressType = addressType;
}
// Getters and setters...
}
enum AddressType {
HOME, WORK, BILLING, SHIPPING
}

Complex Document with Embedded Objects

@Document(collection = "products")
public class Product {
@Id
private String id;
@Indexed(unique = true)
private String sku;
private String name;
private String description;
@TextIndexed
private String category;
private Money price;
private Inventory inventory;
private List<ProductAttribute> attributes = new ArrayList<>();
private Map<String, Object> metadata = new HashMap<>();
private List<Review> reviews = new ArrayList<>();
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
// Constructors
public Product() {}
public Product(String sku, String name, String description, String category, Money price) {
this.sku = sku;
this.name = name;
this.description = description;
this.category = category;
this.price = price;
this.inventory = new Inventory(0, 0);
}
// Business methods
public void addAttribute(String name, String value) {
this.attributes.add(new ProductAttribute(name, value));
}
public void addReview(Review review) {
this.reviews.add(review);
}
public void updateInventory(int quantity, int reserved) {
this.inventory = new Inventory(quantity, reserved);
}
public boolean isInStock() {
return this.inventory.getAvailableQuantity() > 0;
}
public int getAvailableQuantity() {
return this.inventory.getAvailableQuantity();
}
// Getters and setters...
}
// Embedded objects
class Money {
private BigDecimal amount;
private String currency;
public Money() {}
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// Getters and setters...
}
class Inventory {
private Integer quantity;
private Integer reserved;
public Inventory() {}
public Inventory(Integer quantity, Integer reserved) {
this.quantity = quantity;
this.reserved = reserved;
}
public Integer getAvailableQuantity() {
return quantity - reserved;
}
// Getters and setters...
}
class ProductAttribute {
private String name;
private String value;
public ProductAttribute() {}
public ProductAttribute(String name, String value) {
this.name = name;
this.value = value;
}
// Getters and setters...
}
class Review {
private String userId;
private String username;
private Integer rating;
private String comment;
private LocalDateTime reviewDate;
public Review() {}
public Review(String userId, String username, Integer rating, String comment) {
this.userId = userId;
this.username = username;
this.rating = rating;
this.comment = comment;
this.reviewDate = LocalDateTime.now();
}
// Getters and setters...
}

3. Repository Layer

Basic Repository Interface

@Repository
public interface UserRepository extends MongoRepository<User, String> {
// Query methods
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByStatus(UserStatus status);
List<User> findByAgeBetween(Integer minAge, Integer maxAge);
List<User> findByFullNameContainingIgnoreCase(String name);
List<User> findByAddressesCity(String city);
// Exists methods
boolean existsByUsername(String username);
boolean existsByEmail(String email);
// Count methods
long countByStatus(UserStatus status);
long countByAgeGreaterThan(Integer age);
// Delete methods
void deleteByStatus(UserStatus status);
// Pageable queries
Page<User> findByStatus(UserStatus status, Pageable pageable);
// Custom sort
List<User> findByStatusOrderByCreatedAtDesc(UserStatus status);
}
@Repository
public interface ProductRepository extends MongoRepository<Product, String> {
// Basic queries
Optional<Product> findBySku(String sku);
List<Product> findByCategory(String category);
List<Product> findByPriceAmountBetween(BigDecimal minPrice, BigDecimal maxPrice);
List<Product> findByInventoryQuantityGreaterThan(Integer quantity);
// Text search
List<Product> findByCategoryContainingIgnoreCase(String category);
// Embedded object queries
List<Product> findByAttributesNameAndAttributesValue(String name, String value);
List<Product> findByReviewsRatingGreaterThanEqual(Integer minRating);
// Complex queries
@Query("{ 'price.amount': { $gte: ?0, $lte: ?1 } }")
List<Product> findProductsInPriceRange(BigDecimal minPrice, BigDecimal maxPrice);
@Query("{ 'reviews': { $elemMatch: { 'rating': { $gte: ?0 }, 'userId': ?1 } } }")
List<Product> findProductsWithUserRatingAbove(Integer minRating, String userId);
@Query(value = "{ 'category': ?0 }", fields = "{ 'name': 1, 'price': 1, 'sku': 1 }")
List<Product> findProductProjectionsByCategory(String category);
// Aggregation queries
@Aggregation(pipeline = {
"{ '$match': { 'category': ?0 } }",
"{ '$group': { '_id': '$category', 'averagePrice': { '$avg': '$price.amount' }, 'count': { '$sum': 1 } } }"
})
CategoryStats getCategoryStats(String category);
}

Custom Repository Implementation

// Custom repository interface
public interface CustomUserRepository {
List<User> findActiveUsersWithAddressInCity(String city);
void updateUserStatus(String userId, UserStatus status);
List<User> findUsersByComplexCriteria(UserSearchCriteria criteria);
UserStats getUserStats();
}
// Implementation
public class CustomUserRepositoryImpl implements CustomUserRepository {
private final MongoTemplate mongoTemplate;
public CustomUserRepositoryImpl(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@Override
public List<User> findActiveUsersWithAddressInCity(String city) {
Query query = new Query();
query.addCriteria(Criteria.where("status").is(UserStatus.ACTIVE)
.and("addresses.city").is(city));
return mongoTemplate.find(query, User.class);
}
@Override
public void updateUserStatus(String userId, UserStatus status) {
Query query = new Query(Criteria.where("id").is(userId));
Update update = new Update().set("status", status).set("updatedAt", LocalDateTime.now());
mongoTemplate.updateFirst(query, update, User.class);
}
@Override
public List<User> findUsersByComplexCriteria(UserSearchCriteria criteria) {
Criteria criteriaBuilder = new Criteria();
if (criteria.getMinAge() != null && criteria.getMaxAge() != null) {
criteriaBuilder.and("age").gte(criteria.getMinAge()).lte(criteria.getMaxAge());
}
if (criteria.getStatus() != null) {
criteriaBuilder.and("status").is(criteria.getStatus());
}
if (StringUtils.hasText(criteria.getCity())) {
criteriaBuilder.and("addresses.city").is(criteria.getCity());
}
if (StringUtils.hasText(criteria.getSearchTerm())) {
criteriaBuilder.orOperator(
Criteria.where("fullName").regex(criteria.getSearchTerm(), "i"),
Criteria.where("email").regex(criteria.getSearchTerm(), "i")
);
}
Query query = new Query(criteriaBuilder);
if (criteria.getPageable() != null) {
query.with(criteria.getPageable());
}
return mongoTemplate.find(query, User.class);
}
@Override
public UserStats getUserStats() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.group()
.count().as("totalUsers")
.sum(ConditionalOperators.when(Criteria.where("status").is(UserStatus.ACTIVE)).then(1).otherwise(0)).as("activeUsers")
.avg("age").as("averageAge")
.max("age").as("maxAge")
.min("age").as("minAge")
);
AggregationResults<UserStats> results = mongoTemplate.aggregate(aggregation, "users", UserStats.class);
return results.getUniqueMappedResult();
}
}
// Extended repository interface
public interface UserRepository extends MongoRepository<User, String>, CustomUserRepository {
// Inherits all methods from both interfaces
}
// Supporting classes
public class UserSearchCriteria {
private Integer minAge;
private Integer maxAge;
private UserStatus status;
private String city;
private String searchTerm;
private Pageable pageable;
// Constructors, getters, and setters...
}
public class UserStats {
private Long totalUsers;
private Long activeUsers;
private Double averageAge;
private Integer maxAge;
private Integer minAge;
// Constructors, getters, and setters...
}
public class CategoryStats {
private String category;
private Double averagePrice;
private Long count;
// Constructors, getters, and setters...
}

4. Service Layer

User Service Implementation

@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final MongoTemplate mongoTemplate;
public UserService(UserRepository userRepository, MongoTemplate mongoTemplate) {
this.userRepository = userRepository;
this.mongoTemplate = mongoTemplate;
}
public User createUser(CreateUserRequest request) {
// Check if username or email already exists
if (userRepository.existsByUsername(request.getUsername())) {
throw new UserAlreadyExistsException("Username already exists: " + request.getUsername());
}
if (userRepository.existsByEmail(request.getEmail())) {
throw new UserAlreadyExistsException("Email already exists: " + request.getEmail());
}
User user = new User(
request.getUsername(),
request.getEmail(),
request.getFullName(),
request.getAge()
);
// Add addresses if provided
if (request.getAddresses() != null) {
request.getAddresses().forEach(user::addAddress);
}
return userRepository.save(user);
}
public User getUserById(String id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
public User getUserByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UserNotFoundException("User not found with username: " + username));
}
public List<User> getUsersByStatus(UserStatus status) {
return userRepository.findByStatus(status);
}
public List<User> searchUsers(UserSearchCriteria criteria) {
return userRepository.findUsersByComplexCriteria(criteria);
}
public User updateUser(String userId, UpdateUserRequest request) {
User user = getUserById(userId);
if (StringUtils.hasText(request.getFullName())) {
user.setFullName(request.getFullName());
}
if (request.getAge() != null) {
user.setAge(request.getAge());
}
return userRepository.save(user);
}
public void deactivateUser(String userId) {
userRepository.updateUserStatus(userId, UserStatus.INACTIVE);
}
public void activateUser(String userId) {
userRepository.updateUserStatus(userId, UserStatus.ACTIVE);
}
public void deleteUser(String userId) {
if (!userRepository.existsById(userId)) {
throw new UserNotFoundException("User not found with id: " + userId);
}
userRepository.deleteById(userId);
}
public User addAddressToUser(String userId, Address address) {
User user = getUserById(userId);
user.addAddress(address);
return userRepository.save(user);
}
public UserStats getUserStatistics() {
return userRepository.getUserStats();
}
// Bulk operations
@Transactional
public void bulkUpdateUserStatus(List<String> userIds, UserStatus status) {
Query query = new Query(Criteria.where("id").in(userIds));
Update update = new Update().set("status", status).set("updatedAt", LocalDateTime.now());
mongoTemplate.updateMulti(query, update, User.class);
}
}
// Custom exceptions
class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
class UserAlreadyExistsException extends RuntimeException {
public UserAlreadyExistsException(String message) {
super(message);
}
}

Product Service with Advanced Features

@Service
@Transactional
public class ProductService {
private final ProductRepository productRepository;
private final MongoTemplate mongoTemplate;
public ProductService(ProductRepository productRepository, MongoTemplate mongoTemplate) {
this.productRepository = productRepository;
this.mongoTemplate = mongoTemplate;
}
public Product createProduct(CreateProductRequest request) {
if (productRepository.findBySku(request.getSku()).isPresent()) {
throw new ProductAlreadyExistsException("Product with SKU already exists: " + request.getSku());
}
Product product = new Product(
request.getSku(),
request.getName(),
request.getDescription(),
request.getCategory(),
new Money(request.getPrice(), "USD")
);
// Set initial inventory
product.updateInventory(request.getInitialQuantity(), 0);
// Add attributes
if (request.getAttributes() != null) {
request.getAttributes().forEach(product::addAttribute);
}
return productRepository.save(product);
}
public Product getProductById(String id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + id));
}
public Product getProductBySku(String sku) {
return productRepository.findBySku(sku)
.orElseThrow(() -> new ProductNotFoundException("Product not found with SKU: " + sku));
}
public List<Product> getProductsByCategory(String category) {
return productRepository.findByCategory(category);
}
public List<Product> searchProducts(String searchTerm) {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matching(searchTerm);
Query query = TextQuery.queryText(criteria).sortByScore();
return mongoTemplate.find(query, Product.class);
}
public List<Product> getProductsInPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findProductsInPriceRange(minPrice, maxPrice);
}
public Product updateProductInventory(String productId, int quantity, int reserved) {
Product product = getProductById(productId);
product.updateInventory(quantity, reserved);
return productRepository.save(product);
}
public Product addProductReview(String productId, AddReviewRequest request) {
Product product = getProductById(productId);
Review review = new Review(
request.getUserId(),
request.getUsername(),
request.getRating(),
request.getComment()
);
product.addReview(review);
return productRepository.save(product);
}
public List<Product> getTopRatedProducts(int limit) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.unwind("reviews"),
Aggregation.group("id")
.first("name").as("name")
.first("sku").as("sku")
.first("category").as("category")
.first("price").as("price")
.avg("reviews.rating").as("averageRating")
.count().as("reviewCount"),
Aggregation.match(Criteria.where("reviewCount").gte(5)),
Aggregation.sort(Sort.Direction.DESC, "averageRating"),
Aggregation.limit(limit)
);
return mongoTemplate.aggregate(aggregation, "products", Product.class)
.getMappedResults();
}
public Map<String, CategoryStats> getCategoryStatistics() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.group("category")
.avg("price.amount").as("averagePrice")
.count().as("productCount")
.sum("inventory.quantity").as("totalStock")
.sum("inventory.reserved").as("totalReserved"),
Aggregation.project()
.and("_id").as("category")
.and("averagePrice").as("averagePrice")
.and("productCount").as("count")
.andExpression("totalStock - totalReserved").as("availableStock")
);
List<CategoryStats> results = mongoTemplate.aggregate(aggregation, "products", CategoryStats.class)
.getMappedResults();
return results.stream()
.collect(Collectors.toMap(CategoryStats::getCategory, Function.identity()));
}
// Transactional operation
@Transactional
public void processOrder(OrderRequest order) {
for (OrderItem item : order.getItems()) {
Product product = getProductBySku(item.getSku());
if (product.getAvailableQuantity() < item.getQuantity()) {
throw new InsufficientStockException(
"Insufficient stock for product: " + product.getName() + 
". Available: " + product.getAvailableQuantity() + 
", Requested: " + item.getQuantity()
);
}
// Reserve the items
product.updateInventory(
product.getInventory().getQuantity(),
product.getInventory().getReserved() + item.getQuantity()
);
productRepository.save(product);
}
}
}

5. REST Controllers

User Controller

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable String id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@GetMapping("/username/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
User user = userService.getUserByUsername(username);
return ResponseEntity.ok(user);
}
@GetMapping
public ResponseEntity<List<User>> getUsers(
@RequestParam(required = false) UserStatus status,
@RequestParam(required = false) String city,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
@RequestParam(required = false) String search,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
UserSearchCriteria criteria = new UserSearchCriteria();
criteria.setStatus(status);
criteria.setCity(city);
criteria.setMinAge(minAge);
criteria.setMaxAge(maxAge);
criteria.setSearchTerm(search);
criteria.setPageable(PageRequest.of(page, size, Sort.by("createdAt").descending()));
List<User> users = userService.searchUsers(criteria);
return ResponseEntity.ok(users);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable String id, 
@Valid @RequestBody UpdateUserRequest request) {
User user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
}
@PatchMapping("/{id}/deactivate")
public ResponseEntity<Void> deactivateUser(@PathVariable String id) {
userService.deactivateUser(id);
return ResponseEntity.ok().build();
}
@PatchMapping("/{id}/activate")
public ResponseEntity<Void> activateUser(@PathVariable String id) {
userService.activateUser(id);
return ResponseEntity.ok().build();
}
@PostMapping("/{id}/addresses")
public ResponseEntity<User> addAddress(@PathVariable String id, 
@Valid @RequestBody Address address) {
User user = userService.addAddressToUser(id, address);
return ResponseEntity.ok(user);
}
@GetMapping("/stats")
public ResponseEntity<UserStats> getUserStats() {
UserStats stats = userService.getUserStatistics();
return ResponseEntity.ok(stats);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable String id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}

Product Controller

@RestController
@RequestMapping("/api/products")
@Validated
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody CreateProductRequest request) {
Product product = productService.createProduct(request);
return ResponseEntity.status(HttpStatus.CREATED).body(product);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable String id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
@GetMapping("/sku/{sku}")
public ResponseEntity<Product> getProductBySku(@PathVariable String sku) {
Product product = productService.getProductBySku(sku);
return ResponseEntity.ok(product);
}
@GetMapping
public ResponseEntity<List<Product>> getProducts(
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(required = false) String search) {
List<Product> products;
if (search != null) {
products = productService.searchProducts(search);
} else if (minPrice != null && maxPrice != null) {
products = productService.getProductsInPriceRange(minPrice, maxPrice);
} else if (category != null) {
products = productService.getProductsByCategory(category);
} else {
products = productService.getProductsByCategory("electronics"); // Default
}
return ResponseEntity.ok(products);
}
@GetMapping("/top-rated")
public ResponseEntity<List<Product>> getTopRatedProducts(
@RequestParam(defaultValue = "10") int limit) {
List<Product> products = productService.getTopRatedProducts(limit);
return ResponseEntity.ok(products);
}
@PostMapping("/{id}/reviews")
public ResponseEntity<Product> addReview(@PathVariable String id,
@Valid @RequestBody AddReviewRequest request) {
Product product = productService.addProductReview(id, request);
return ResponseEntity.ok(product);
}
@GetMapping("/stats/categories")
public ResponseEntity<Map<String, CategoryStats>> getCategoryStats() {
Map<String, CategoryStats> stats = productService.getCategoryStatistics();
return ResponseEntity.ok(stats);
}
@PatchMapping("/{id}/inventory")
public ResponseEntity<Product> updateInventory(@PathVariable String id,
@RequestBody UpdateInventoryRequest request) {
Product product = productService.updateProductInventory(
id, request.getQuantity(), request.getReserved());
return ResponseEntity.ok(product);
}
}

6. Advanced MongoDB Features

Aggregation Framework Examples

@Component
public class ProductAnalyticsService {
private final MongoTemplate mongoTemplate;
public ProductAnalyticsService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public List<ProductSales> getProductSalesStatistics(LocalDateTime startDate, LocalDateTime endDate) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("orderDate").gte(startDate).lte(endDate)),
Aggregation.unwind("items"),
Aggregation.group("items.productId")
.first("items.productName").as("productName")
.sum("items.quantity").as("totalSold")
.sum("items.totalPrice").as("totalRevenue")
.avg("items.unitPrice").as("averagePrice"),
Aggregation.sort(Sort.Direction.DESC, "totalRevenue"),
Aggregation.limit(50)
);
return mongoTemplate.aggregate(aggregation, "orders", ProductSales.class)
.getMappedResults();
}
public List<CategoryPerformance> getCategoryPerformance() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.group("category")
.count().as("productCount")
.avg("price.amount").as("avgPrice")
.sum("inventory.quantity").as("totalStock")
.avg("reviews.rating").as("avgRating"),
Aggregation.project()
.and("_id").as("category")
.and("productCount").as("productCount")
.and("avgPrice").as("averagePrice")
.and("totalStock").as("totalStock")
.and("avgRating").as("averageRating")
.andExpression("totalStock * avgPrice").as("totalValue"),
Aggregation.sort(Sort.Direction.DESC, "totalValue")
);
return mongoTemplate.aggregate(aggregation, "products", CategoryPerformance.class)
.getMappedResults();
}
public List<UserPurchaseHistory> getUserPurchaseHistory(String userId) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("userId").is(userId)),
Aggregation.unwind("items"),
Aggregation.group("items.productId")
.first("items.productName").as("productName")
.sum("items.quantity").as("totalPurchased")
.sum("items.totalPrice").as("totalSpent")
.count().as("purchaseCount"),
Aggregation.sort(Sort.Direction.DESC, "totalSpent")
);
return mongoTemplate.aggregate(aggregation, "orders", UserPurchaseHistory.class)
.getMappedResults();
}
}
// Aggregation result classes
class ProductSales {
private String productId;
private String productName;
private Integer totalSold;
private BigDecimal totalRevenue;
private BigDecimal averagePrice;
// Constructors, getters, and setters...
}
class CategoryPerformance {
private String category;
private Long productCount;
private Double averagePrice;
private Integer totalStock;
private Double averageRating;
private Double totalValue;
// Constructors, getters, and setters...
}
class UserPurchaseHistory {
private String productId;
private String productName;
private Integer totalPurchased;
private BigDecimal totalSpent;
private Long purchaseCount;
// Constructors, getters, and setters...
}

Text Search and Index Management

@Component
public class SearchService {
private final MongoTemplate mongoTemplate;
public SearchService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public List<Product> fullTextSearch(String searchTerm) {
TextCriteria criteria = TextCriteria.forDefaultLanguage()
.matchingAny(searchTerm.split(" "))
.caseSensitive(false)
.diacriticSensitive(false);
Query query = TextQuery.queryText(criteria)
.sortByScore()
.with(Sort.by("price.amount").ascending());
return mongoTemplate.find(query, Product.class);
}
public List<Product> advancedSearch(ProductSearchCriteria criteria) {
Criteria queryCriteria = new Criteria();
List<Criteria> andCriteria = new ArrayList<>();
List<Criteria> orCriteria = new ArrayList<>();
// Text search
if (StringUtils.hasText(criteria.getSearchTerm())) {
orCriteria.add(Criteria.where("name").regex(criteria.getSearchTerm(), "i"));
orCriteria.add(Criteria.where("description").regex(criteria.getSearchTerm(), "i"));
orCriteria.add(Criteria.where("category").regex(criteria.getSearchTerm(), "i"));
}
// Price range
if (criteria.getMinPrice() != null && criteria.getMaxPrice() != null) {
andCriteria.add(Criteria.where("price.amount")
.gte(criteria.getMinPrice()).lte(criteria.getMaxPrice()));
}
// Category filter
if (StringUtils.hasText(criteria.getCategory())) {
andCriteria.add(Criteria.where("category").is(criteria.getCategory()));
}
// In stock filter
if (criteria.getInStockOnly() != null && criteria.getInStockOnly()) {
andCriteria.add(Criteria.where("inventory.quantity").gt(0));
}
// Minimum rating
if (criteria.getMinRating() != null) {
andCriteria.add(Criteria.where("reviews.rating").gte(criteria.getMinRating()));
}
// Build final query
if (!orCriteria.isEmpty()) {
Criteria textSearchCriteria = new Criteria().orOperator(orCriteria.toArray(new Criteria[0]));
andCriteria.add(textSearchCriteria);
}
if (!andCriteria.isEmpty()) {
queryCriteria.andOperator(andCriteria.toArray(new Criteria[0]));
}
Query query = new Query(queryCriteria);
// Sorting
if (StringUtils.hasText(criteria.getSortBy())) {
Sort.Direction direction = criteria.isSortDesc() ? Sort.Direction.DESC : Sort.Direction.ASC;
query.with(Sort.by(direction, criteria.getSortBy()));
}
// Pagination
if (criteria.getPage() != null && criteria.getSize() != null) {
query.with(PageRequest.of(criteria.getPage(), criteria.getSize()));
}
return mongoTemplate.find(query, Product.class);
}
public void createTextIndex() {
mongoTemplate.indexOps(Product.class).ensureIndex(
new TextIndexDefinitionBuilder()
.onField("name", 2.0f)
.onField("description", 1.0f)
.onField("category", 1.5f)
.build()
);
}
public void createCompoundIndex() {
mongoTemplate.indexOps(Product.class).ensureIndex(
new Index().on("category", Sort.Direction.ASC)
.on("price.amount", Sort.Direction.ASC)
.on("inventory.quantity", Sort.Direction.DESC)
.named("category_price_stock_idx")
);
}
}

7. Testing

Integration Tests

@DataMongoTest
@ExtendWith(SpringExtension.class)
@TestPropertySource(properties = "spring.mongodb.embedded.version=4.4.5")
class UserRepositoryIntegrationTest {
@Autowired
private UserRepository userRepository;
@Autowired
private MongoTemplate mongoTemplate;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldSaveAndFindUser() {
// Given
User user = new User("john_doe", "[email protected]", "John Doe", 30);
user.addAddress(new Address("123 Main St", "New York", "NY", "10001", "USA", AddressType.HOME));
// When
User savedUser = userRepository.save(user);
// Then
assertThat(savedUser.getId()).isNotNull();
assertThat(savedUser.getUsername()).isEqualTo("john_doe");
Optional<User> foundUser = userRepository.findByUsername("john_doe");
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getEmail()).isEqualTo("[email protected]");
}
@Test
void shouldFindUsersByCity() {
// Given
User user1 = new User("user1", "[email protected]", "User One", 25);
user1.addAddress(new Address("123 Main St", "New York", "NY", "10001", "USA", AddressType.HOME));
User user2 = new User("user2", "[email protected]", "User Two", 35);
user2.addAddress(new Address("456 Oak St", "Los Angeles", "CA", "90001", "USA", AddressType.HOME));
userRepository.saveAll(List.of(user1, user2));
// When
List<User> nyUsers = userRepository.findByAddressesCity("New York");
// Then
assertThat(nyUsers).hasSize(1);
assertThat(nyUsers.get(0).getUsername()).isEqualTo("user1");
}
}
@SpringBootTest
@Testcontainers
class ProductServiceIntegrationTest {
@Container
static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:5.0");
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
}
@Autowired
private ProductService productService;
@Autowired
private ProductRepository productRepository;
@BeforeEach
void setUp() {
productRepository.deleteAll();
}
@Test
void shouldCreateAndRetrieveProduct() {
// Given
CreateProductRequest request = new CreateProductRequest(
"SKU-001", "Laptop", "High-performance laptop", "electronics", 
new BigDecimal("999.99"), 10, new HashMap<>()
);
// When
Product product = productService.createProduct(request);
// Then
assertThat(product.getId()).isNotNull();
assertThat(product.getSku()).isEqualTo("SKU-001");
assertThat(product.isInStock()).isTrue();
Product foundProduct = productService.getProductBySku("SKU-001");
assertThat(foundProduct.getName()).isEqualTo("Laptop");
}
@Test
void shouldGetTopRatedProducts() {
// Given
Product product1 = new Product("SKU-001", "Product 1", "Description 1", "category1", 
new Money(new BigDecimal("100"), "USD"));
product1.addReview(new Review("user1", "User One", 5, "Excellent!"));
product1.addReview(new Review("user2", "User Two", 4, "Very good"));
Product product2 = new Product("SKU-002", "Product 2", "Description 2", "category1", 
new Money(new BigDecimal("200"), "USD"));
product2.addReview(new Review("user3", "User Three", 3, "Average"));
productRepository.saveAll(List.of(product1, product2));
// When
List<Product> topRated = productService.getTopRatedProducts(10);
// Then
assertThat(topRated).hasSize(1);
assertThat(topRated.get(0).getSku()).isEqualTo("SKU-001");
}
}

This comprehensive guide covers MongoDB integration with Spring Data, including advanced features like aggregation framework, text search, transactions, and testing strategies. The implementation follows best practices for building robust, scalable applications with MongoDB.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper