Skip to main content

Reactive Spring - CRUD with MongoDB

Greetings!

When we learn something, it is good to have something to play with. Now is the time to connect with a database.

Working with Reactive Programming is fun but it brings extra challenges. Connecting to a database is such a challenge as we can't use blocking drivers. Lucky for us, Spring comes with Data Repository for MongoDB.

Assumptions:
You are comfortable with Spring Boot REST
You have installed or have access to the MongoDB database

The completed project is here - reactive-super-heroes

What to expect?

We will follow Spring's usual layers. The main difference is we are using a Reactive Repository instead of the usual blocking repository. We will build a simple REST application with CRUD operations to store superheroes.
  • Create the project
  • Add database connection
  • Create entity (document), dtos
  • Create repository
  • Create service
  • Create controller
  • Demo

Create the project

Go to start.spring.io and create a project with the below dependencies.
  • Spring Reactive Web
  • Spring Data Reactive MongoDB
  • Lombok

Setup MongoDB

You can install MongoDB on your computer or use a docker container.
version: '3'
services:
  mongo:
    image: mongo
    ports:
      - '27017:27017'
    volumes:
      - './mongo:/data/db'
  mongo-express:
    image: mongo-express
    ports:
      - '8081:8081'
$ docker-compose -f super-hero-mongodb.yml up

Add database connection

Connecting to MongoDB is very simple with Spring Boot as it does all the heavy work for us. Set below in your application.yml (if you have a username/password you need to add that too)
spring:
  data:
    mongodb:
      uri: mongodb://127.0.0.1:27017
      database: super-heroes

Create entity

We don't have much here except a simple POJO.
@Data @AllArgsConstructor @NoArgsConstructor @ToString
@Document("heroes")
public class Hero {

  @Id
  private String id;
  private String name;
  private List<String> powers = new ArrayList<>();

}
@Data @AllArgsConstructor @NoArgsConstructor @ToString
public class HeroDto {

  private String id;
  private String name;
  private List<String> powers = new ArrayList<>();

}
Let's create a data mapper to convert between these two types.
public class HeroMapper {

  public static HeroDto toDto(Hero hero) {
    return new HeroDto(hero.getId(), hero.getName(), hero.getPowers());
  }

  public static Hero toEntity(HeroDto heroDto) {
    return new Hero(heroDto.getId(), heroDto.getName(), heroDto.getPowers());
  }

}

Create repository

This is as simple as creating an interface. The main thing to note here is, instead of the usual methods with Optional, List here we have Mono and Flux.
Make a note that we are extending ReactiveMongoRepository, not the blocking repository.
@Repository
public interface HeroRepository extends ReactiveMongoRepository<Hero, String> {
}

Create service

We can define our service skeleton as below.
public interface HeroService {

  Flux<HeroDto> findAll();

  Mono<HeroDto> findById(String id);

  Mono<HeroDto> save(Mono<HeroDto> heroDtoMono);

  Mono<HeroDto> update(String id, Mono<HeroDto> heroDtoMono);

  Mono<Void> delete(String id);

}
Note that we are exposing our methods with Flux and Mono to main the reactive pipeline.

Find methods

These are easy. I do not think we need an explanation here.
public Flux<HeroDto> findAll() {
  return heroRepository.findAll()
      .map(HeroMapper::toDto);
}

public Mono<HeroDto> findById(String id) {
  return heroRepository.findById(id)
      .map(HeroMapper::toDto);
}

Save method

As we mentioned earlier, the reactive repository returns a Mono. Like in Java Streams we need to flatten the Mono using flatMap.
public Mono<HeroDto> save(Mono<HeroDto> heroDtoMono) {
  return heroDtoMono.map(HeroMapper::toEntity)
      .flatMap(heroRepository::insert)
      .map(HeroMapper::toDto);
}

Update method

This is a little tricky as we need to check the existence of the given id. However, still we need to maintain the pipeline. First, we use findById to find our entry in the database and if present converts the update request into an entity. As this update request may not contain the id, we set it in doOnNext.
What if it doesn't find an entry? This will return an empty Mono which we should handle in our Controller.
public Mono<HeroDto> update(String id, Mono<HeroDto> heroDtoMono) {
  return heroRepository.findById(id)
      .flatMap(hero -> heroDtoMono.map(HeroMapper::toEntity))
      .doOnNext(hero -> hero.setId(id))
      .flatMap(heroRepository::save)
      .map(HeroMapper::toDto);
}

Delete method

This is straightforward.
public Mono<Void> delete(String id) {
  return heroRepository.deleteById(id);
}

Create controller

This is just a thin layer that delegates the request to the Service. However, there are two things to note.
Handle empty - we use defaultIfEmpty
Use Mono with RequestBody
@PutMapping("super-heroes/{id}")
public Mono<ResponseEntity<HeroDto>> update(@PathVariable("id") String id, @RequestBody Mono<HeroDto> heroDtoMono) {
  return heroService.update(id, heroDtoMono)
      .map(ResponseEntity::ok)
      .defaultIfEmpty(ResponseEntity.badRequest().build());
}
All other methods are normal Controller methods.
@AllArgsConstructor
@RestController
public class SuperHeroController {

  private HeroService heroService;

  @GetMapping("super-heroes")
  public Flux<HeroDto> index() {
    return heroService.findAll();
  }

  @GetMapping("super-heroes/{id}")
  public Mono<ResponseEntity<HeroDto>> findById(@PathVariable("id") String id) {
    return heroService.findById(id)
        .map(ResponseEntity::ok)
        .defaultIfEmpty(ResponseEntity.notFound().build());
  }

  @PostMapping("super-heroes")
  public Mono<ResponseEntity<HeroDto>> save(@RequestBody Mono<HeroDto> heroDtoMono) {
    return heroService.save(heroDtoMono).map(ResponseEntity::ok);
  }

  @PutMapping("super-heroes/{id}")
  public Mono<ResponseEntity<HeroDto>> update(@PathVariable("id") String id, @RequestBody Mono<HeroDto> heroDtoMono) {
    return heroService.update(id, heroDtoMono)
        .map(ResponseEntity::ok)
        .defaultIfEmpty(ResponseEntity.badRequest().build());
  }

  @DeleteMapping("super-heroes/{id}")
  public Mono<ResponseEntity<Object>> delete(@PathVariable("id") String id) {
    return heroService.delete(id)
        .map(hero -> ResponseEntity.noContent().build())
        .defaultIfEmpty(ResponseEntity.noContent().build());
  }

}

Demo

Run the Spring Boot application. By default, it will listen to port 8080. I'm using curl here.
// create using POST
curl -d '{"name":"Batman", "powers":[ "Rich", "Science" ]}' -H "Content-Type: application/json" -X POST localhost:8080/super-heroes
curl -d '{"name":"Wonder Woman", "powers":[ "Pretty" ]}' -H "Content-Type: application/json" -X POST localhost:8080/super-heroes

// get, note that I use json_pp to format the output
curl localhost:8080/super-heroes
curl localhost:8080/super-heroes | json_pp
curl localhost:8080/super-heroes/62f0e4aa5d5efe47589d155a

// update using PUT
curl -d '{"name":"Batman", "powers":[ "Rich" ]}' -H "Content-Type: application/json" -X PUT localhost:8080/super-heroes/62f0e4aa5d5efe47589d155a

// finally, DELETE
curl -i -H "Content-Type: application/json" -X DELETE localhost:8080/super-heroes/62f0e4aa5d5efe47589d155a
Meanwhile, you can check the database as well.
$ mongo
> show dbs 
> use super-heroes
> show collections
> db.heroes.find()

What did we achieve?

We have created a CRUD application in reactive passion in no time. Spring WebFlux is that good. However, with complex logic, things may become more complicated. Use this as your playground. Learn!

Happy learning :)