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.