DEV Community

loading...
Cover image for How to build custom queries with Spring Data Reactive MongoDB

How to build custom queries with Spring Data Reactive MongoDB

iuriimednikov profile image Yuri Mednikov ・7 min read

This post guides readers to implementation of update methods with Reactive MongoDB for Spring Data. It focuses on core principles of extending ReactiveMongoRepository with custom functionality and gives four concrete examples.

Getting started with ReactiveMongo for Spring Data

Out of the box, Spring supplies us with repositories, that offer basic CRUD functionality, e.g. insertion, deletion, update and retrieving data. In case of reactive MongoDB access, we talk about ReactiveMongoRepository interface, which captures the domain type to manage as well as the domain type's id type. There are two ways to extend it functionality:

  • Use derived query methods, which are generated from custom method signatures, you declare in the interface - like findByFirstName - this is out of the scope of this post.
  • Create a custom implementation and combine it with ReactiveMongoRepository - this is what we will do in this article.

To start, you need to have a reactive MongoDB starter defined with your dependencies management tool. If you use Maven, add following dependency in your pom.:

<dependencies>
    <!-- Your app dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

Let create a custom entity to use in our example. Let do a traditional example - user management. This code declares a User object that we will use in this post:

@Value @AllArgsConstructor
@Document(collection = "users")
public class User {

    @NonFinal @Id String userId;
    String email;
    String password;
    List<Role> roles;
}
Enter fullscreen mode Exit fullscreen mode

Next step is to declare a repository which extends ReactiveMongoRepository For the purpose of tutorial, let create an ordinary user repository:

@Repository
public interface UserRepository extends ReactiveMongoRepository<User, String> {}
Enter fullscreen mode Exit fullscreen mode

This interface will provide us with basic CRUD functionality for User entities, including:

  • Save
  • Retrieve by id
  • Update
  • Retrieve all
  • Remove

This, however does not include custom features we may need to do with User objects. For instance, we may need to update not a full record record, but only specific fields, like password. Same way, we may need to handle changes for nested objects/arrays of the entity. To do this, we can't use derived query methods, but we have to declare a custom repository. Let see how to do this.

How to extend ReactiveMongoRepository

The process to extend Spring's reactive Mongo repository with custom functionality takes 3 steps:

  1. Create an interface that defines custom data management methods, like CustomUserRepository
  2. Provide an implementation using ReactiveMongoTemplate to access data source. The name of the implementation interface, should have Impl postfix, like CustomUsersRepositoryImpl
  3. Extend the core repository with custom repository interface. For example, UserRepository extends ReactiveMongoRepository, CustomUserRepository

Let have a practical example. We already mentioned UsersRepository as an entry point for data access operations. We will use this repository in services to handle data source access. However, Spring will create only implementations for CRUD operations, and for custom method updates we will need to provide own implementations. But this is not a rocket science with Spring.

The first thing is to create a custom interface to declare our own update methods. Let call it CustomUserRepository:

public interface CustomUserRepository {

    Mono<User> changePassword (String userId, String newPassword);

    Mono<User> addNewRole (String userId, Role role);

    Mono<User> removeRole (String userId, String permission);

    Mono<Boolean> hasPermission (String userId, String permission);
}
Enter fullscreen mode Exit fullscreen mode

Then, in the step 2, we need to provide a custom implementation. This is the step, where we will need to do most work. We will look on each method separately later, but for now we will provide an essential configuration. What do we need to do:

  • Provide a dependency of type ReactiveMongoTemplate. We will use it to access an underlaying data source.
  • Add a constructor, where Spring will inject the required bean. Also, annotate the constructor using @Autowired

Take a look on the code snippet below:

public class CustomUserRepositoryImpl implements CustomUserRepository {

    private final ReactiveMongoTemplate mongoTemplate;

    @Autowired
    public CustomUserRepositoryImpl(ReactiveMongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @Override
    public Mono<User> changePassword(String userId, String newPassword) {
        return null;
    }

    @Override
    public Mono<User> addNewRole(String userId, Role role) {
        return null;
    }

    @Override
    public Mono<User> removeRole(String userId, String permission) {
        return null;
    }

    @Override
    public Mono<Boolean> hasPermission(String userId, String permission) {
        return null;
    }

}
Enter fullscreen mode Exit fullscreen mode

Finally, we need to make Spring to know, that we want to use this repository alongside with the built-in one. For this, extend the UserRepository with the CustomUserRepository interface, like this:

public interface UserRepository extends ReactiveMongoRepository<User, String>, CustomUserRepository {
    //...
}
Enter fullscreen mode Exit fullscreen mode

So, now we are ready to do implementations of custom update methods.

Implement custom update functionality

In this section we will look how to implement custom updates methods with Reactive MongoDB in Spring. Each method is described separately.

Update an entity's field

The one of the most common requirements is to change a value of an entity's field. In our example we use confirmed field to store if the user confirmed her/his account or not. Of course, in our example we concentrate only on database logic, and will not handle a confirmation workflow, so please assume that our UserService did everything correctly and now uses a repository to update user's status.

Take a look on the code snippet below:

@Override
public Mono<User> changePassword(String userId, String newPassword) {
    // step 1
    Query query = new Query(Criteria.where("userId").is(userId)); 

    // step 2
    Update update = new Update().set("password", newPassword);

    // step 3
    return mongoTemplate.findAndModify(query, update, User.class);
}
Enter fullscreen mode Exit fullscreen mode

Let examine what we do here:

  1. Create a query object that finds the concrete user with specific userId
  2. Create an update object to change the confirmed field's value to true. For this, use set() method that accepts as a first argument field's name (key) and as a second one new value
  3. Finally execute an update using mongoTemplate object. The method findAndModify is used to apply provided update on documents that match defined criteria.

Note, that basically, the flow is same if we use "traditional" synchronous Spring MongoDB. The only difference is the return type - instead of plain User object, reacitve repository returns a Mono.

Add to a nested array

An another widespread operation you may need to handle in your applications is to add a new entity in a nested array. Let say, that we want to add a new role for the specific user. In our example we store permssions in the nested list roles.

Again, we don't focus on how actually permission management is done, and assume that UserService does everything as needed. Here is an implementation:

@Override
public Mono<User> addNewRole(String userId, Role role) {
    // step 1
    Query query = new Query(Criteria.where("userId").is(userId));

    // step 2
    Update update = new Update().addToSet("roles", role);

    // step 3
    return mongoTemplate.findAndModify(query, update, User.class);
}
Enter fullscreen mode Exit fullscreen mode

Basically, steps are same. The only change is an Update object. We use here addToSet method which accepts a name of a nested array - roles and an object to insert in it.

There is an another approach to add an object to a nested collection - push:

// ..
Update updateWithPush = new Update().push("roles", role);
Enter fullscreen mode Exit fullscreen mode

The difference between these two methods is following:

  • addToSet method does not allow duplicates and inserts an object only if collection does not contain it already.
  • push method allows duplicates and can insert the same entity several times

Remove from a nested array

Like we add something inside a nested collection, we may need to remove it. As we do in our examples role management, we may want to revoke given access rights from the user. To do this we actually need to remove an item from a nested collection. Take a look on the code snippet below:

@Override
public Mono<User> removeRole(String userId, String permission) {
    // step 1
    Query query = new Query(Criteria.where("userId").is(userId));

    // step 2
    Update update = new Update().pull("roles",
         new BasicDBObject("permission", permission));

    // step 3
    return mongoTemplate.findAndModify(query, update, User.class);
}
Enter fullscreen mode Exit fullscreen mode

The update object uses pull method to remove a role with a given permission value. We assume that permissions are unique, but in a real life it is better to use some internal unique IDs for the nested object management. We can divide this step into two parts:

  1. pull operator accepts as a first argument a name of a nested collection
  2. Second argument is a BasicDBObject which specifies a criteria of which role we want to delete. In our case we use permission field to specify it

Query by a nested object's field

We already mentioned, that Spring helps us to do querying with derived query methods, like findByName. Although, this will work with object's fields, but what if we want to query by a nested object?

For the illustration we declared a method hasPermission which returns a boolean value that indicates if a user has a specific access right, or in terms of our example - does a nested list roles contains an entity with a specific permission value.

Let see how we can solve this task:

@Override
// 1
public Mono<Boolean> hasPermission(String userId, String permission) {

    // 2
    Query query = new Query(Criteria.where("userId").is(userId))
            .addCriteria(Criteria.where("roles.permission").is(permission)); //3

    // 4
    return mongoTemplate.exists(query, User.class);
}
Enter fullscreen mode Exit fullscreen mode

Let go step by step:

  1. Unlike previous methods, this method returns Mono result, which wraps a boolean value that indicates a presence/absense of the desired permission.
  2. We need to combine two criterias, because we are looking for the specific user with specific permssion. Query object offers fluent interface and we can chain criterias with addCriteria method.
  3. We build a query object which looks for a role entity inside roles array with the needed permission value.
  4. Finally, we call exists method, which accepts a query and checks for an existance of the queried data

Source code

That is how you can create custom update queries using Reactive MongoDB with Spring. You can find a complete source code in this Github repository.

Discussion (4)

pic
Editor guide
Collapse
jendorski profile image
Jendorski

So this is really cool, the article.

I am working on a little project and i discovered that in Generated Query Methods, i cannot have Logical Operators like AND, NOT, OR.

For example,

private Mono> findByUserAndUserAgeNotNull(int userID);

Is it compulsory, i use the Query and Criteria methods, or are there Generated Logical Methods that are possible to use.

Thanks.

Collapse
felipegutierrez profile image
Felipe Oliveira Gutierrez

very good. It worked perfectly for me. thank you

Collapse
rawas94 profile image
AbdulRaHman elRawas

That was very clean and helpful thank you.

Collapse
marttp profile image
Thanaphoom Babparn

That was very helpful! Thank you.