MongoDB is a NoSQL document database that stores data as JSON-like documents, so it can store unstructured data without a fixed schema. Spring Data MongoDB is the Spring Data module that connects a Spring Boot application to MongoDB through the interfaces MongoRepository and MongoTemplate. We use this combination in applications that store unstructured data.
We connect Spring Boot to MongoDB with the Spring Data MongoDB API and run the MongoDB server in docker on our local system. The sections cover the connection properties, the mapping annotations, MongoRepository, MongoTemplate, CRUD operations, pagination and queries.
1. Prerequisites
Docker must be installed on our system, and the following command prints its version.
$ docker --version
The next command runs a MongoDB server locally, and our application connects to it. If docker is not installed, see the installation section of this post.
$ docker run -d
-p 27017:27017 --name mongo-on-docker
-e MONGO_INITDB_ROOT_USERNAME=mongoadmin
-e MONGO_INITDB_ROOT_PASSWORD=secret
-e MONGO_INITDB_DATABASE=testdb mongo
The command downloads the MongoDB docker image and runs it on port 27017.
2. Setting Up Spring Data Mongo
2.1. Maven
We define the dependency in the pom.xml file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>3.0.5</version>
</dependency>
2.2. Connecting to MongoDB
To connect to MongoDB, we set the connection properties for authentication in the application.properties file, and Spring Boot auto-configuration creates the connection from them. The database name is testdb, and MongoDB creates the database when it does not exist.
The admin database stores the system users, and MongoDB checks the user credentials against it during authentication.
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=testdb
spring.data.mongodb.username=mongoadmin
spring.data.mongodb.password=secret
spring.data.mongodb.authentication-database=admin
The equivalent Java configuration looks like this.
@Configuration
@EnableMongoRepositories(basePackageClasses = ItemRepository.class)
public class MongoConfig {
@Bean
MongoClient mongoClient() {
return MongoClients.create("mongodb://mongoadmin:secret@localhost:27017");
}
@Bean
MongoTemplate mongoTemplate(MongoClient mongoClient) {
return new MongoTemplate(mongoClient, "testdb");
}
}
We configure the connection pool through MongoClientSettings.
@Bean
MongoClient mongoClient() {
ConnectionString connectionString
= new ConnectionString("mongodb://mongoadmin:secret@localhost:27017");
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.applyToConnectionPoolSettings(builder -> builder.maxSize(20)
.minSize(10)
.maxWaitTime(2000, TimeUnit.MILLISECONDS)
.build())
.build();
return MongoClients.create(mongoClientSettings);
}
3. Document Mapping Annotations
Spring Data MongoDB provides three main annotations that map a Java class to a MongoDB document.
- @Document identifies a domain object that is persisted to MongoDB and represents a MongoDB collection. It is similar to the @Entity annotation of the Java Persistence API.
- @Field configures the name of the field that MongoDB uses when it persists the document.
- @Id marks a field as the primary key. It is a generic annotation provided by Spring Data.
MongoDB requires an _id field in every document. If we do not provide one, the driver assigns an ObjectId with a generated value.
In a class annotated with @Document, the id field has one of these types.
- java.lang.String
- java.lang.BigInteger
- org.bson.types.ObjectId
A property or field named id maps to the _id field even without an annotation. If no id field or property is specified, the driver generates an implicit _id field that is not mapped to a property or field of the Java class.
@Document("items")
public class Item {
@Id
private String id;
@Field("name")
private String name;
//...
}
4. Configuring MongoRepository
4.1. @EnableMongoRepositories
The @EnableMongoRepositories annotation activates the MongoDB repositories. If no base package is configured, it scans the package of the annotated class. To scan the complete project, we put it on the main application class together with @SpringBootApplication.
@SpringBootApplication
@EnableMongoRepositories
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
To scan a different package for repository classes, we set the basePackage attribute.
@Configuration
@EnableMongoRepositories(basePackageClasses = ItemRepository.class)
public class MongoConfig {
//...
}
4.2. Extending MongoRepository Interface
MongoRepository extends CrudRepository and adds the methods to work with MongoDB to the generic persistence methods. It already provides methods to create, read, update and delete documents.
We can also write queries by declaring a method in the repository interface. At runtime, Spring Data parses the method name and generates the matching query.
public interface ItemRepository extends MongoRepository<Item, Integer> {
@Query("{name:'?0'}")
Item findByName(String name);
@Query(value = "{category:'?0'}")
List<Item> findByCategory(String category);
@Query("{ 'quantity' : { $gt: ?0, $lt: ?1 } }")
List<Item> findAllByQuantityBetween(int qtyGT, int qtyLT);
}
5. Configuring MongoTemplate
MongoTemplate is a helper class for common MongoDB operations. It provides operations to create, update, delete and query MongoDB documents, and it maps between the domain objects and MongoDB documents.
MongoTemplate also translates the exceptions thrown by the MongoDB Java driver into the exception hierarchy of Spring. An instance of MongoTemplate is thread-safe, so we can reuse it without synchronization.
@Bean
MongoTemplate mongoTemplate(MongoClient mongoClient) {
return new MongoTemplate(mongoClient, "testdb");
}
6. Performing CRUD Operations
The query and CRUD APIs are the same for MongoRepository and MongoTemplate. The examples use MongoTemplate, which is the preferred approach of the two.
6.1. Insert
To insert new documents whose _id field is null, we use these methods of MongoTemplate.
// Insert the object into the collection for the entity type of the object to save.
insert(T objectToSave)
// Insert the object into the specified collection.
insert(T objectToSave, String collectionName)
// Insert a mixed Collection of objects into a database collection
// determining the collection name to use based on the class.
insertAll(Collection objectsToSave)
To insert multiple documents in a batch, we use these methods.
// Insert a Collection of objects into a collection in a single batch write to the database.
insert(Collection batchToSave, Class entityClass)
// Insert a Collection of objects into specified collection in a single batch write to the database.
insert(Collection batchToSave, String collectionName)
The demo inserts an item with a null id and checks that the saved item has an id.
Item item = new Item(null, "Red Chili", 3, "Spices");
Item savedItem = mongoTemplate.insert(item);
Assertions.assertNotNull(savedItem.getId());
If a document with the specified id already exists, the insert() operation throws an E11000 duplicate key error.
6.2. Save
The save() operation has save-or-update semantics. If an id is present, it performs an update, and if not, it inserts the document.
We update an existing document with these methods.
// Save the object to the collection for the entity type of the object to save.
save(T objectToSave);
// Save the object to the specified collection.
save(T objectToSave, String collectionName);
The demo saves a new item, changes its quantity and saves it again to update it.
Item savedItem = mongoTemplate.save(new Item(null, "Red Chili", 3, "Spices"));
savedItem.setQuantity(1);
Item updatedItem = mongoTemplate.save(savedItem);
Assertions.assertEquals(1, updatedItem.getQuantity());
6.3. UpdateFirst and UpdateMulti
These methods take a non-null query that finds the documents to update. They also take an update definition that contains either the updated object to persist or the $ operators that update the existing record in place.
- The method updateFirst() updates the first object that the query finds.
- The method updateMulti() updates all the objects that the query finds.
updateFirst(Query query, UpdateDefinition update, Class<?> entityClass)
updateFirst(Query query, UpdateDefinition update, String collectionName)
updateFirst(Query query, UpdateDefinition update, Class<?> entityClass, String collectionName)
updateMulti(Query query, UpdateDefinition update, Class<?> entityClass)
updateMulti(Query query, UpdateDefinition update, String collectionName)
updateMulti(Query query, UpdateDefinition update, Class<?> entityClass, String collectionName)
The example changes the name of all items named Soft Drinks.
Query query = new Query();
query.addCriteria(Criteria.where("name").is("Soft Drinks"));
Update update = new Update();
update.set("name", "Cold Drinks");
mongoTemplate.updateMulti(query, update, Item.class);
6.4. Upsert
The upsert operation has find-and-modify-else-create semantics. If the query finds the document, upsert updates it. Otherwise, it creates a new document by combining the query and the update object.
// Performs an upsert for the specified entity class.
upsert(Query query, UpdateDefinition update, Class<?> entityClass)
// Performs an upsert in the specified collection.
upsert(Query query, UpdateDefinition update, String collectionName)
The demo runs an upsert for the item named Soft Drinks.
Query query = new Query();
query.addCriteria(Criteria.where("name").is("Soft Drinks"));
Update update = new Update();
update.set("name", "Cold Drinks");
mongoTemplate.upsert(query, update, Item.class);
6.5. Remove
We delete documents with the remove() methods, either by passing the document itself or by passing a criteria query.
// Remove using document instance. Returns DeleteResult.
remove(Object object)
remove(Object object, String collectionName)
// Remove all documents matching the criteria query. Returns DeleteResult.
remove(Query query, Class<?> entityClass)
remove(Query query, String collectionName)
// The first document that matches the query is returned and also removed from the database.
findAndRemove(Query query, Class<T> entityClass)
findAndRemove(Query query, Class<T> entityClass, String collectionName)
// All document that matches the query are returned and also removed from the database.
findAllAndRemove(Query query, Class<T> entityClass)
findAllAndRemove(Query query, String collectionName)
DeleteResult result = mongoTemplate.remove(savedItem);
Assertions.assertEquals(result.wasAcknowledged(), true);
7. Fetch Documents
We fetch documents with the built-in methods, the Query and Criteria classes, generated query methods and JSON queries.
7.1. findById
The method findById() retrieves a document by its unique identifier, which is usually the _id field. It returns the document if one with that id exists, and null otherwise.
findById(Object id, Class<T> entityClass)
findById(Object id, Class<T> entityClass, String collectionName)
The demo reads an item by its id.
Item item = mongoTemplate.findById("1", Item.class);
Assertions.assertEquals("1", item.getId());
7.2. findAll
The method findAll() returns a list of all documents of the specified type.
findAll(Class<T> entityClass)
findAll(Class<T> entityClass, String collectionName)
If the collection is empty, findAll() returns an empty list. If the collection name does not exist, it throws IllegalArgumentException.
Item savedItemSpices = mongoTemplate.add(itemSpices);
Item savedItemSnacks = mongoTemplate.add(itemSnacks);
List<Item> items = mongoTemplate.getAll();
Assertions.assertTrue(items.size() == 2);
8. Pagination and Sorting
Pagination requests and displays only one page of the data, based on the page number and the page size. A sort order specifies the order of the results.
We use the Pageable interface of Spring Data for pagination and sorting, as in this method.
@Override
public List<Item> getAll(Integer page, Integer size) {
Sort sort = Sort.by("category").ascending();
Pageable pageable = PageRequest.of(page - 1, size, sort);
Query query = new Query().with(pageable);
Page<Item> pagedResult = PageableExecutionUtils.getPage(
mongoTemplate.find(query, Item.class),
pageable,
() -> mongoTemplate.count(query, Item.class));
return pagedResult.getContent();
}
mongoTemplate.save(new Item(...));
mongoTemplate.save(new Item(...));
List<Item> items = getAll(1, 5);
Assert.assertTrue(items.size() == 2);
9. @Query Annotation in MongoRepository
When a method name or criteria cannot express a query, we use the @Query annotation, which takes a raw MongoDB JSON query string.
9.1. Query Params
The method findByName() returns an item by name and takes the field value to filter by as a parameter. We declare the query with @Query, and the placeholder ?0 references the first method parameter.
@Query("{name:'?0'}")
Item findByName(String name);
9.2. Projections
MongoDB supports projections, which return only some fields of each document. A projection includes or excludes fields by name, and the _id field is always included unless we exclude it explicitly.
The method findByCategory() returns all items of a category. It returns only the fields name and quantity, so we set those fields to 1 in the query.
@Query(value = "{category:'?0'}", fields = "{'name' : 1, 'quantity' : 1}")
List<Item> findByCategory(String category);
The test case verifies the projection. It returns only the item name and the item quantity, and the item category is null.
mongoTemplate.save(new Item(...));
mongoTemplate.save(new Item(...));
List<Item> items = mongoRepository.findAllByCategory(searchCategory);
Assert.assertTrue(items.size() == 2);
Item item = items.get(0);
Assert.assertNotNull(item.getName());
Assert.assertNotNull(item.getQuantity());
Assert.assertNull(item.getCategory());
10. Generic Search with MongoTemplate
The classes Query and Criteria express queries with method names that mirror the native MongoDB operators, such as lt (less than), lte (less than or equal), is (equals) and others. Both classes have a fluent API, so we can chain several criteria in one readable statement.
The search method filters items named Cheese Crackers, with a quantity between 2 and 5, in the category snacks.
@Override
public List<Item> search(String name, Integer minQuantity, Integer maxQuantity, String category) {
Query query = new Query();
List<Criteria> criterias = new ArrayList<>();
if (name != null && !name.isEmpty()) {
criterias.add(Criteria.where("name").is(name));
}
if (minQuantity != null && minQuantity >= 0) {
criterias.add(Criteria.where("quantity").gte(minQuantity));
}
if (maxQuantity != null && maxQuantity >= 0) {
criterias.add(Criteria.where("quantity").lte(maxQuantity));
}
if (category != null && !category.isEmpty()) {
criterias.add(Criteria.where("category").is(category));
}
if (!criterias.isEmpty()) {
Criteria criteria = new Criteria()
.andOperator(criterias.toArray(new Criteria[criterias.size()]));
query.addCriteria(criteria);
}
List<Item> items = mongoTemplate.find(query, Item.class);
return items;
}
The method checks which values are specified. If only the name is specified, it filters by name. If a quantity or category is specified, it adds them as criteria, and if all three values are present, all three filter the records.
Item savedItemSpices = mongoTemplate.add(itemSpices);
Item savedItemSnacks = mongoTemplate.add(itemSnacks);
List<Item> items = mongoTemplateService.search("Dried Red Chilli", 1, 5, searchCategory);
Assert.assertTrue(items.size() > 0);
The logs show the generated query.
o.s.data.mongodb.core.MongoTemplate : find using query:
{ "$and" : [{ "name" : "Dried Red Chilli"}, { "quantity" : { "$gte" : 1}}, { "quantity" : { "$lte" : 5}}, { "category" : "spices"}]}
11. Conclusion
We connected a Spring Boot application to MongoDB and performed simple and complex operations with Spring Data through MongoTemplate and MongoRepository.
For more operations of MongoTemplate, see the official documentation.