Developing Reactive Applications with Spring Reactive MongoDB
We can use MongoDB with Spring Reactive Mongo to develop reactive applications. NoSQL structures seem to be more suitable for the use of reactive programming in production environments compared to R2DBC structures.
Spring Data Mongo Reactive provides us with a reactive MongoDB connector to develop reactive applications. This allows our operations to be performed asynchronously and non-blocking on MongoDB.
In addition to Reactive MongoDB, I recommend reading about the structures that I have previously mentioned and will be using in this article, starting with the theory , reactive endpoints - Mono and Flux , and finally, writing a reactive web client (for testing purposes), and if you are interested, the topic of Reactive Relational Databases.
In this article, we will develop an end-to-end user service using Spring Data Mongo Reactive. We will use Functional Endpoints on Spring WebFlux to prepare our Controller layer, and in our Service layer, we will implement both our own custom queries using ReactiveMongoTemplate and ready-made methods using ReactiveMongoRepository. You can access all the code on the Github repository.
Project Dependencies and Environment Setup
As we always emphasize, if you are developing a reactive application, all its components should be reactive.
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'implementation 'org.springframework.boot:spring-boot-starter-webflux'testImplementation 'org.springframework.boot:spring-boot-starter-test'testImplementation 'io.projectreactor:reactor-test'
We can create a docker-compose.yaml to prepare MongoDB, so that we can start up a MongoDB instance that we can use.
version: '3.7'services:mongodb_container:image: mongo:latestenvironment:MONGO_INITDB_ROOT_USERNAME: rootMONGO_INITDB_ROOT_PASSWORD: mongopwports:- 27017:27017volumes:- mongodb_data_container:/data/dbvolumes:mongodb_data_container:
To make our project work with MongoDB, running the command "docker-compose up" is enough for our MongoDB to be up and running.
Developing Reactive Mongo Application
To enable our Reactive MongoDB to connect to the database we have set up, we need to write the necessary configurations. We can create a Reactive database connection using the MongoConfiguration class that extends the AbstractReactiveMongoConfiguration class. We use the @EnableMongoRepositories annotation to inform Spring that we will be working with MongoDB. In the configuration, we override two methods, one to provide the database information and the other to provide the MongoClient. To create the MongoClient, we use static factory methods and provide the necessary username:password and ip:port information with the .create(connectionString) method.
package dev.gokhana.userservice.configuration;import com.mongodb.reactivestreams.client.MongoClient;import com.mongodb.reactivestreams.client.MongoClients;import dev.gokhana.userservice.model.User;import dev.gokhana.userservice.repository.UserRepository;import org.springframework.boot.CommandLineRunner;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;import org.springframework.data.mongodb.core.ReactiveMongoTemplate;import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;import reactor.core.publisher.Flux;import java.util.concurrent.ThreadLocalRandom;@Configuration@EnableMongoRepositoriespublic class MongoConfiguration extends AbstractReactiveMongoConfiguration {@Overrideprotected String getDatabaseName() {return "users";}@Override@Beanpublic MongoClient reactiveMongoClient() {return MongoClients.create("mongodb://root:mongopw@localhost:27017");}@Beanpublic ReactiveMongoTemplate reactiveMongoTemplate() {return new ReactiveMongoTemplate(reactiveMongoClient(), getDatabaseName());}@Bean@ConditionalOnProperty(prefix = "job.autorun", name = "enabled", havingValue = "true", matchIfMissing = true)public CommandLineRunner loadData(UserRepository repository) {return (args) -> {// save a couple of usersvar users = Flux.just(new User("Gökhan", ThreadLocalRandom.current().nextInt(1, 100)),new User("Betül", ThreadLocalRandom.current().nextInt(1, 100)),new User("Zühtü", ThreadLocalRandom.current().nextInt(1, 100)));repository.saveAll(users).subscribe();};}}
We have prepared our structure that will establish a connection to the database. Now it's time to create the object that will be stored in our database. In MongoDB, the data we create is stored in structures called Documents. We can follow the same example we used in my previous article on R2DBC.
@Documentpublic class User {@Idprivate String id;@NotBlankprivate String name;private Integer score;//Getter, Setter, hashCode etc..
Now that we have created the document that will be associated with our database, we can create the repository needed to perform operations on this document. We can create our UserRepository by extending ReactiveMongoRepository, which will provide us with CRUD methods such as findAll, findById, save, and delete.
package dev.gokhana.userservice.repository;import dev.gokhana.userservice.model.User;import org.springframework.data.mongodb.repository.ReactiveMongoRepository;public interface UserRepository extends ReactiveMongoRepository<User, String> {}
In the next step, a service that can manage all operations can be created. UserRepository can be injected into this service. Additionally, ReactiveMongoTemplate can also be made available in the service to demonstrate examples. But first, let's look at how UserRepository is used. We can perform all CRUD operations, and each one contains User object(s) as Mono or Flux.
package dev.gokhana.userservice.service;import com.mongodb.client.result.DeleteResult;import dev.gokhana.userservice.model.User;import dev.gokhana.userservice.repository.UserRepository;import org.springframework.data.mongodb.core.ReactiveMongoTemplate;import org.springframework.stereotype.Service;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;import static org.springframework.data.mongodb.core.query.Criteria.where;import static org.springframework.data.mongodb.core.query.Query.query;@Servicepublic class UserServiceImpl implements UserService {private final UserRepository userRepository;private final ReactiveMongoTemplate template;public UserServiceImpl(UserRepository userRepository, ReactiveMongoTemplate template) {this.userRepository = userRepository;this.template = template;}@Overridepublic Mono<User> getUserById(String id) {return userRepository.findById(id);}@Overridepublic Flux<User> getUsers() {return userRepository.findAll();}@Overridepublic Mono<User> saveUser(User userDTO) {return userRepository.save(userDTO);}@Overridepublic Mono<User> updateUser(String id, User userDTO) {return userRepository.findById(id).flatMap(user -> {userDTO.setId(user.getId()); // if there is something else to update do it here.return userRepository.save(userDTO);});}@Overridepublic Mono<Void> deleteUser(String id) {return userRepository.deleteById(id);}@Overridepublic Mono<Long> deleteByName(String name) {return template.remove(query(where("name").is(name)), User.class).map(DeleteResult::getDeletedCount);}
If we want to manage the entire query infrastructure ourselves, then ReactiveMongoTemplate comes into play. The ReactiveMongoTemplate, which we have already made available for use in our service, provides an infrastructure that allows us to manage all operations ourselves.
@Overridepublic Mono<Long> deleteByName(String name) {return template.remove(query(where("name").is(name)), User.class).map(DeleteResult::getDeletedCount);}
In the example, we used the remove method with reactiveTemplate. Thus, we have written a method that deletes the names (the name field) that are equal to the parameter received and returns the number of deleted ones. In this way, we can describe the queries we want.
Now, let's prepare functional endpoints that can handle our requests for all service methods. First, we can develop our UserHandler component to include all request and response objects except routing.
package dev.gokhana.userservice.controller;import dev.gokhana.userservice.controller.validation.ValidatorHandler;import dev.gokhana.userservice.model.User;import dev.gokhana.userservice.service.UserService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.MediaType;import org.springframework.stereotype.Component;import org.springframework.web.reactive.function.server.ServerRequest;import org.springframework.web.reactive.function.server.ServerResponse;import org.springframework.web.util.UriComponentsBuilder;import reactor.core.publisher.Mono;import java.net.URI;import java.util.Map;import java.util.Optional;import static org.springframework.web.reactive.function.BodyInserters.fromValue;@Componentpublic class UserHandler {private static final Logger LOGGER = LoggerFactory.getLogger(UserHandler.class);private final ValidatorHandler validator;private final UserService userService;public UserHandler(ValidatorHandler validator, UserService userService) {this.validator = validator;this.userService = userService;}public Mono<ServerResponse> getAll(ServerRequest request) {return userService.getUsers().collectList().flatMap(users -> {if (users.isEmpty()) {return ServerResponse.noContent().build();}return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromValue(users));});}public Mono<ServerResponse> getUserById(ServerRequest request) {String id = request.pathVariable("id");return userService.getUserById(id).flatMap(contact -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromValue(contact))).switchIfEmpty(ServerResponse.notFound().build());}public Mono<ServerResponse> createUser(ServerRequest request) {return request.bodyToMono(User.class).doOnNext(validator::validate).flatMap(userService::saveUser).doOnSuccess(userSaved -> LOGGER.info("User saved with id: {} ", userSaved.getId())).doOnError(e -> LOGGER.error("Error in saveUser method", e)).flatMap(user -> ServerResponse.created(getToUri(user)).bodyValue(user));}public Mono<ServerResponse> updateUser(ServerRequest request) {Mono<User> userMono = request.bodyToMono(User.class);String id = request.pathVariable("id");return userMono.flatMap(user -> userService.updateUser(id, user)).flatMap(user -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromValue(user))).switchIfEmpty(ServerResponse.notFound().build());}public Mono<ServerResponse> deleteUser(ServerRequest request) {String id = request.pathVariable("id");return userService.deleteUser(id).then(ServerResponse.noContent().build()).switchIfEmpty(ServerResponse.notFound().build());}public Mono<ServerResponse> deleteUserByName(ServerRequest request) {Optional<String> name = request.queryParam("name");if (name.isEmpty())return ServerResponse.badRequest().contentType(MediaType.APPLICATION_JSON).bodyValue(Map.of("error", "There is no name in the field"));return userService.deleteByName(name.get()).then(ServerResponse.noContent().build()).switchIfEmpty(ServerResponse.notFound().build());}private URI getToUri(User userSaved) {return UriComponentsBuilder.fromPath(("/{id}")).buildAndExpand(userSaved.getId()).toUri();}}
As can be seen from the code, our entire structure is ready. Now, we can convert the routing processes into endpoints with RouterFunctions.
package dev.gokhana.userservice.controller;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.MediaType;import org.springframework.util.StringUtils;import org.springframework.web.reactive.function.server.RequestPredicates;import org.springframework.web.reactive.function.server.RouterFunction;import org.springframework.web.reactive.function.server.ServerResponse;import static org.springframework.web.reactive.function.server.RequestPredicates.*;import static org.springframework.web.reactive.function.server.RouterFunctions.route;@Configurationpublic class RoutingHandler {private static final String API = "/api/v1/users";private static final String ID = "/{id}";@Beanpublic RouterFunction<ServerResponse> userRouter(UserHandler userHandler) {return route(GET(API), userHandler::getAll).andRoute(POST(API).and(accept(MediaType.APPLICATION_JSON)), userHandler::createUser).andRoute(GET(API + ID), userHandler::getUserById).andRoute(PUT(API + ID).and(accept(MediaType.APPLICATION_JSON)), userHandler::updateUser).andRoute(DELETE(API + ID), userHandler::deleteUser).andRoute(DELETE(API).and(RequestPredicates.queryParam("name", StringUtils::hasText)), userHandler::deleteUserByName);}}
Now we have a structure where we can route our requests and we have a reactive application that uses MongoDB end-to-end.
If we want to quickly prepare a unit test structure, we can use WebTestClient and mock our database structures.
package dev.gokhana.userservice;import dev.gokhana.userservice.model.User;import dev.gokhana.userservice.repository.UserRepository;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.mock.mockito.MockBean;import org.springframework.test.web.reactive.server.WebTestClient;import reactor.core.publisher.Flux;import java.util.Arrays;import java.util.List;import static org.mockito.Mockito.when;@SpringBootTest(classes = UserServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"job.autorun.enabled=false"})class UserServiceApplicationTests {@Autowiredprivate WebTestClient webTestClient;@MockBeanprivate UserRepository userRepository;@Testpublic void findAllTest() {List<User> users = Arrays.asList(new User("Gokhan", 15),new User("Corç", 25));Flux<User> flux = Flux.fromIterable(users);when(userRepository.findAll()).thenReturn(flux);webTestClient.get().uri("api/v1/users").exchange().expectStatus().isOk().expectBodyList(User.class).isEqualTo(users);}}
Now that we have completed all the processes, we can start our service and send requests and examine their results.
POST http://localhost:8083/api/v1/usersContent-Type: application/json{"name": "Zühtü","score": 52}
When we send a user creation request, the response we receive will be 201 Created along with the information returned with the object's ID.
{"id": "61ed90c1d656605b62e19acd","name": "Zühtü","score": 52}
Conclusion
With reactive programming, we can perform non-blocking and asynchronous operations with fewer threads and less hardware resources. We added a powerful NoSQL database like MongoDB to this and supported it with a reactive connector to obtain an end-to-end reactive application. In my opinion, applications implemented with MongoDB seem to be more ready for production environments compared to R2DBC database structures.