Getting Started with Spring Boot Data and MongoDB

MongoDB is the most popular NoSQL document database that stores data in JSON-like format. MongoDB database provides a flexible data model which allows us to store unstructured data. Combining Spring Boot and MongoDB results in applications that are fast, secure, and reliable.

This tutorial demonstrates how to integrate Spring Boot with MongoDB using the Spring Data MongoDB API. We will use docker to run a MongoDB server on our local system.

1. Prerequisites

Before we start make sure docker is installed in our system. To verify the same, we can use below commands –

 $ docker --version

Then use the below command to run a MongoDB server locally, and our application will interact with the same. If docker is not installed, refer to 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

This command will download a MongoDB docker image and run it on the system on port 27017.

2. Setting Up Spring Data Mongo

2.1. Maven

We start by defining the dependency information in the pom.xml

<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 specify the connection properties required to authenticate and connect with the database in the application.properties file. Spring Boot auto-configuration takes care of creating one for us. We are also specifying the database name testdb – if it doesn’t exist, MongoDB will create it.

The admin database stores the system users, and the authentication process uses it to authenticate the user credentials.

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 Java config is rather simple, and an equivalent configuration is as follows:

@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");
  }
}

To set the connection pool settings, we can configure 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

Let us look at some annotations provided by Spring Data Mongo used to drive API operations.

  • @Document – identifies a domain object, which is persisted to MongoDB and represents a MongoDB collection. It is like Entity annotation of Java persistence API.
  • @Field – configure the name of a field MongoDB uses while persisting the document.
  • @Id – marks a field as the primary key. It is a Spring data-provided generic annotation.

MongoDB requires to have an _id field for all documents. If we do not provide one, the driver assigns an ObjectId with a generated value.

In Mongo @Document annotated class, the id field should be of type:

  • java.lang.String
  • java.lang.BigInteger
  • org.bson.types.ObjectId

A property or field without an annotation, too, but named id maps to the _id field. If no id field or property is specified, an implicit _id file is generated by the driver but 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 will trigger scanning of the package of annotated class. To scan the source code of the complete project, use it along with @SpringBootApplication in the main application class.

@SpringBootApplication
@EnableMongoRepositories
public class App {

  public static void main(String[] args) {
    
    SpringApplication.run(App.class, args);
  }
}

To customize the package to scan for repository classes, we can use the basePackage attribute as follows:

@Configuration
@EnableMongoRepositories(basePackageClasses = ItemRepository.class)
public class MongoConfig {

  //...
}

4.2. Extending MongoRepository Interface

MongoRepository extends CrudRepository and exposes the methods to work with MongoDB in addition to the generic persistence methods. Out of the box, we get methods for document creation, viewing, and more.

In addition, by using MongoRepository, we can write queries just by creating a specific method. In runtime, Spring data parses the method names and generates appropriate queries to execute.

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 that increases productivity when performing common Mongo operations. It offers convenient operations to create, update, delete, and query MongoDB documents and provides a mapping between the domain objects and MongoDB documents.

Another important feature of MongoTemplate is the translation of exceptions thrown by the MongoDB Java driver into Spring’s exception hierarchy. Note that an instance of MongoTemplate is thread-safe and can be reused without introducing synchronization issues.

@Bean
MongoTemplate mongoTemplate(MongoClient mongoClient) {

      return new MongoTemplate(mongoClient, "testdb");
}

6. Performing CRUD Operations

The APIs to perform the query or crud operations on mongo documents are the same for MongoRepository or MongoTemplate, so we are taking the example of MongoTemplate which is the preferred approach among both.

6.1. Insert

To create the new documents that have their _id field NULL, we can use the following methods provided by 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) 

For inserting multiple documents in a batch, we can use the following 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)

Let us see a demo of inserting a document.

Item item = new Item(null, "Red Chili", 3, "Spices");
Item savedItem = mongoTemplate.insert(item);

Assertions.assertNotNull(savedItem.getId());

If the document with the specified id is already present, then the insert() operation will throw 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 does an insert.

We can update an existing document using the following 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);

Let us see a demo.

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 is used to find the eligible documents for the update. Additionally, we provide an update definition that contains either the updated object to persist in the database or $ operators to update the existing record in place.

  • The updateFirst() updates the first object that is found using the query.
  • The updateMulti() updates all the matching objects found using the query.
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)

Let us take an example.

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 works on the find and modify-else-create semantics. If the document is found, update it, or else create a new document by combining the query and 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)

Let us take a demo.

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

Many methods can help us delete the documents from MongoDB using id and criteria queries etc.

// 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

In this section, we will be looking at querying documents with in-built APIs, Query and Criteria classes, auto-generated query methods, and JSON queries.

7.1. findById

The findById() method retrieves a document based on its unique identifier, which is usually the _id field. The method returns an instance of the document if a document with the specified ID exists, otherwise, it returns null.


findById(Object id, Class<T> entityClass)
findById(Object id, Class<T> entityClass, String collectionName)

Let us see a demo.

Item item = mongoTemplate.findById("1", Item.class);

Assertions.assertEquals("1", item.getId());

7.2. findAll

We can also query for a list of all document objects using the findAll() method for the specified document type.

findAll(Class<T> entityClass)
findAll(Class<T> entityClass, String collectionName)

Note that if the collection is empty, it returns an empty list. If the collectionName does not exist, it throws the 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 helps to request and display only a chunk of data based on the page number and page-size parameters. Sort is used to specify a sort order for the results.

We’ll use the Pageable interface provided by Spring Data to simplify pagination. Let’s look at a quick example using pagination and sorting:

@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 we can’t represent a query with the help of a method name or criteria, we can do something lower level, using the @Query annotation. With this annotation, we can specify a raw query as a Mongo JSON query string.

9.1. Query Params

The following method, findByName, returns an item by name; it requires a parameter for the query, i.e., the field to filter the query by. We specify this with the annotation @Query. The placeholder ?0 references the first parameter of the method.

@Query("{name:'?0'}")
Item findByName(String name);

9.2. Projections

MongoDB supports projecting fields returned by a query. A projection can include and exclude fields (the _id field is always included unless explicitly excluded) based on their name. =

The following method uses the category field to get all items of a particular category. We only want to return the field’s name and quantity in the query response, so we set those fields to 1.

@Query(value = "{category:'?0'}", fields = "{'name' : 1, 'quantity' : 1}")
List<Item> findByCategory(String category);

Let’s write a test case to verify field projection, we will see that only the item name and item quantity are returned, 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

In this section, we will use the Query and Criteria classes to express our queries. They have method names that mirror the native MongoDB operator names, such as lt (less than), lte (less than equals), is (equals), and others. The Query and Criteria classes follow a fluent API style so that we can chain together multiple method criteria and queries while having easy-to-understand code.

Let’s take a quick example where we’re looking for all Items named Cheese Crackers and quantities between 2 and 5 and categories named 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 above method checks what data is specified to filter the records – if only the name is specified, use it as a filter criterion. If quantity or category is specified, then they are used. If all three data are present, all three act as filter criteria.

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);

Check the logs to verify the search document 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

This tutorial discussed how to connect to MongoDB from a Spring Boot Application. We also learned ways to perform simple and complex operations on MongoDB with Spring Data via the MongoTemplate API and MongoRepository.

To learn more about different operations supported in MongoTemplate, you can refer to the official documentation.

Sourcecode on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode