Here we go again for a new post šŸ˜ After publishing my Quarkus book last September, I got many requests about creating reactive applications with the Quarkus Framework.

Today, I will show you how to make a Reactive CRUD Application with the Quarkus Framework and backed by a PostgreSQL Database.

Building our the Reactive Quarkus Application

The full source code is available on my Github.

We will generate our project using the code.quarkus.io. Our application will need 8 extensions from the portal:

  • RESTEasy Reactive
  • RESTEasy Reactive Jackson
  • Hibernate Reactive with Panache
  • Hibernate Validator
  • Reactive PostgreSQL client
  • JDBC Driver - PostgreSQL
  • Flyway
  • SmallRye OpenAPI

Reactive Quarkus - Generate skull

Other than these dependencies, we will need some more:

  • Lombok for simplifying the code and avoiding all boilerplate stuff like getters/setter, constructors, etc.
  • Mapstruct is used to implement a simplified mapping mechanism between JPA entities and DTOs.
  • Testcontainers is used to provide lightweight database instances for JUnit Tests.

We will need manually to the pom.xml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.4.2.Final</version>
</dependency>

We will need also Test scope dependencies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>

In this tutorial, we will use a great feature available in Quarkus called DevServices,Ā which helps you to provision sevices (like DB, Keycloak, Cache Engines, Kafka, etc.) for Dev and Test purposes.

Based on the Quarkus Guides:

Quarkus supports the automatic provisioning of unconfigured services in development and test mode. We refer to this capability as Dev Services. From a developerā€™s perspective this means that if you include an extension and donā€™t configure it then Quarkus will automatically start the relevant service (usually usingĀ TestcontainersĀ behind the scenes) and wire up your application to use this service.

Quarkus - Dev Services Overview

āš ļø āš ļø You need to have Docker installed in order to enjoy the Quarkus Dev Services capability.

We will start by configuring DevServices in the application.properties :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
quarkus.datasource.db-kind=postgresql
quarkus.datasource.devservices.enabled=true
quarkus.datasource.devservices.image-name=postgres:13
quarkus.datasource.devservices.port=5432
quarkus.datasource.username=postgresx
quarkus.datasource.password=postgresx
quarkus.datasource.jdbc=false
quarkus.flyway.migrate-at-start=false

quarkus.datasource.reactive.url=postgresql://localhost:5432/postgres
quarkus.datasource.reactive.max-size=20

The line(s):

We will be developing a Reactive version of our famous Books CRUD Application. Our Book entity looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@NoArgsConstructor
@Entity
@Table(name = "books")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    @Size(min = 13, max = 20)
    private String isbn;
    private String author;
    @DecimalMin("0.0")
    private BigDecimal price;

    public Book(String title, String isbn, String author, BigDecimal price) {
        this.title = title;
        this.isbn = isbn;
        this.author = author;
        this.price = price;
    }
}

We will start by defining the initial Flyway migration script in the resources/db/migration folder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
CREATE SEQUENCE hibernate_sequence START WITH 1 INCREMENT BY 1;

CREATE TABLE books
(
    id BIGINT NOT NULL PRIMARY KEY DEFAULT nextval('hibernate_sequence'),
    title VARCHAR(100),
    isbn VARCHAR(20) CONSTRAINT isbn_size_between_13_20 CHECK (char_length(isbn) >= 13),
    author VARCHAR(100),
    price NUMERIC(6, 2) CONSTRAINT positive_price CHECK (price >= 0)
);

INSERT INTO books (title, isbn, author, price)
VALUES ('Pairing Apache Shiro and Java EE 7', '978-1-365-12404-4', 'Nebrass Lamouchi', 0);
INSERT INTO books (title, isbn, author, price)
VALUES ('Playing with Java Microservices on Kubernetes and OpenShift', '9782956428510', 'Nebrass Lamouchi', 9.18);
INSERT INTO books (title, isbn, author, price)
VALUES ('Pro Java Microservices with Quarkus and Kubernetes', '9781484271698', 'Nebrass Lamouchi', 65);

Then, we need to create a Flyway migration component that will use the DevServices credentials to load this script to our Database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@ApplicationScoped
public class FlywayMigrationService {

    @ConfigProperty(name = "quarkus.datasource.reactive.url")
    String dbUrl;
    @ConfigProperty(name = "quarkus.datasource.username")
    String dbUsername;
    @ConfigProperty(name = "quarkus.datasource.password")
    String dbPassword;

    public void runFlywayMigration(@Observes StartupEvent event) {
        Flyway flyway = Flyway.configure()
                .dataSource("jdbc:" + dbUrl, dbUsername, dbPassword)
                .load();

        flyway.migrate();
    }
}

The FlywayMigrationService will grab the DevService Database credentials and will load all the migration scripts stored in the Flyway directory.

Next, we will create a Reactive Repository to manage our Books in our DB:

1
2
3
@ApplicationScoped
public class BookRepository implements PanacheRepositoryBase<Book, Long> {
}

ā„¹ļø The PanacheRepositoryBase represents a Repository for a specific type of entity Entity. Implementing this repository will gain you similar useful methods that are on Spring Data JpaRepository like:

  • findAll()
  • findById()
  • persist()
  • update()
  • delete()
  • etc..

Then, we will create the BookService component with an injected BookRepository:

1
2
3
4
5
@ApplicationScoped
@RequiredArgsConstructor
public class BookService {
    private final BookRepository bookRepository;
}

Then, in the Service layer, we will implement the CRUD operations thru the Repository in the Reactive mode.

Implementing the findAll() method

To grab all entities of the Book entity, we will be call the findAll() method of the BookRepository. In the PanacheRepositoryBase interface, the findAll method returns a PanacheQuery<Entity>.

ā„¹ļø A PanacheQuery is an interface representing an entity query, which abstracts the use of paging, getting the number of results, and operating on List or Stream. The PanacheQuery interface will get its implementation based on the shipped Panache dependency that we have. In our case it will be implemented in the PanacheQueryImpl class from the io.quarkus.hibernate.reactive.panache.runtime package from the quarkus-hibernate-reactive-panache extension.

The list() method from the PanacheQueryImpl class returns a Uni<List<T>>.

But, what is the Uni type ?

AĀ Uni<T>Ā is a specialized stream that emits only an item or a failure. Typically,Ā Uni<T>Ā are great to represent asynchronous actions such as a remote procedure call, an HTTP request, or an operation producing a single result.

Uni<T>Ā provides many operators that create, transform, and orchestrateĀ Uni sequences.

SmallRye Mutiny documentation

In the BookService, we can use the list() method from the PanacheQueryImpl class to grab all the books. The findAll() will look like:

1
2
3
public Uni<List<Book>> findAll() {
    return bookRepository.findAll().list();
}

ā„¹ļø Actually, there is a shortcut to the findAll().list() in the PanacheRepositoryBase interface, but I wanted to take you in a tour to show you how things are already made.

Then, we will create the BookResource to expose the findAll() operation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RequiredArgsConstructor
@Produces(MediaType.APPLICATION_JSON)
@Path("/api/books")
@RequestScoped
public class BookResource {

    private final BookService bookService;

    @GET
    public Uni<Response> findAll() {
        return bookService.findAll()
                .map(data -> ok(data).build());
    }
}

We are wrapping the List<Book> inside the Uni<Response>.

Then, if you run the Quarkus application using the mvn quarkus:dev command, then we can test the findAll REST API using the Swagger UI available on http://localhost:8080/q/swagger-ui/:

findAll REST API - Swagger UI findAll REST API - Swagger UI

Implementing the save() method

After implementing the findAll() method, we need to create the one that inserts records in the database šŸ˜…

In our REST API, the save() method is called with JSON Payload holding a new book details. These details can be defined as a DTO Model class that I will call BookDTO:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@lombok.Data
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public class BookDTO {
    private Long id;
    private String title;
    private String isbn;
    private String author;
    private BigDecimal price;
}

In the BookResource class we need to add a new method that is called using the HTTP Post Verb and that consumes as JSON Payload the BookDTO:

1
2
3
4
5
6
7
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Uni<Response> create(BookDTO bookDTO) {
    return bookService.save(bookDTO)                                        <1>
            .onItem().ifNotNull().transform(book -> ok(book).build())       <2>
            .onItem().ifNull().continueWith(status(BAD_REQUEST)::build);    <3>
}

The line(s):

  • 1: will delegate the saving task to the bookService.save() method.
  • 2: if the save() succeeds then we will return the created Book entry in the Response body.
  • 3: if the save() fails then we will return an HTTP Bad Request (400) in the Response body.

Now, we need to create the save() method in the BookService. This is will be obviously calling the BookRepository save method:

1
2
3
4
5
6
7
8
public Uni<Book> save(BookDTO dto) {
    return bookRepository.persistAndFlush(new Book(
            dto.getTitle(),
            dto.getIsbn(),
            dto.getAuthor(),
            dto.getPrice()
    ));
}

You can notice that the persistAndFlush() method returns a Uni<Book> which we already discovered in the previous part of this tutorial.

Hmm.. I have something to modify in my BookService after introducing the BookDTO class. I wonder:

  • Why do I need to return the all the Book entity records in my findAll() method ? Why I dont return instead the entity records wrapped as DTO instances ? Is this will be better or it’s just an overhead ?

The RESPONSE is: THIS IS MUST BE DONE āš”ļøšŸ›”āš”ļø But why ?

The DTO pattern is highly useful in many cases:

  • Returning the plain data directly from the database to the user can be dangerous when the entity records hold sensitive data such as password, payments credentials, adresses, etc. So records mapping to DTOs is a must - as you cannot obfuscate data on the record instance itself šŸ˜
  • The DTO offers the ability to provide the data that is needed in different contexts. For example, the findAll() method in a page that needs only the books titles and prices is not the same as a page that needs the books titles and isbns only. DTOs here will be offering the possibility to create a separate DTO dedicated for each need.

šŸ’” It’s true that in our example none of these needs are present, but I will use the DTO for learning purposes šŸ„¹

My findAll() method with DTOs will look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public Uni<List<BookDTO>> findAll() {
    return bookRepository.findAll()
            .stream()
            .map(bookEntity -> new BookDTO(
                    bookEntity.getId(),
                    bookEntity.getTitle(),
                    bookEntity.getIsbn(),
                    bookEntity.getAuthor(),
                    bookEntity.getPrice()
            ))
            .collect()
            .asList();
}

Hhmmmmmmmm.. in the save() method I instanciated a new Book instance and I passed the attributes to the constructor, and here I passed the attributes from the entity record to the instanciated DTO constructor. Ok this is clear, but headache and it’s not beautiful šŸ¤®

One idea, is to create Mapping methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private java.util.function.Function<Book, BookDTO> mapBookEntityToDTO() {
    return bookEntity -> new BookDTO(
            bookEntity.getId(),
            bookEntity.getTitle(),
            bookEntity.getIsbn(),
            bookEntity.getAuthor(),
            bookEntity.getPrice()
    );
}

private java.util.function.Function<BookDTO, Book> mapBookDtoToEntity() {
    return bookDTO -> new Book(
            bookDTO.getTitle(),
            bookDTO.getIsbn(),
            bookDTO.getAuthor(),
            bookDTO.getPrice()
    );
}

Then my findAll() and save() methods will look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public Uni<List<BookDTO>> findAll() {
    return bookRepository.findAll()
            .stream()
            .map(mapBookEntityToDTO())
            .collect()
            .asList();
}

public Uni<BookDTO> save(BookDTO dto) {
    return bookRepository.persistAndFlush(
                    mapBookDtoToEntity().apply(dto))
            .map(mapBookEntityToDTO());
}

Ouuuf šŸ˜© This is better, but it will be better if there are any cleaner way.. I know it will be crazy to create 2 x N methods if we will have N different DTO for each entity. The good news is that there is an excellent solution to do this: Java bean mappings utility classes !

Hmm šŸ˜’ the name is no so funky ! I know ! But this is what is called. I won’t reinvent the wheel šŸ˜

Implementing the Mapping mechanism

In the Java World, there are many libraries that helps to do dynamic mapping from an entity to its corresponding DTO classes. Since 2016, I was trying many of them like: MapStruct, Dozer, ModelMapper, JMapper, etc. But the best one that suited my needs is MapStruct.

You can read about benchmarking and performance of the different available libraries here @Baeldung: https://www.baeldung.com/java-performance-mapping-frameworks.

To add MapStruct to our application, we start by adding the MapStruct Maven dependency to our pom.xml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<properties>
    ...
    <mapstruct.version>1.4.2.Final</mapstruct.version>
    <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
    ...
</properties>

...

<dependencies>
    ...
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${mapstruct.version}</version>
    </dependency>
    ...
</dependencies>    

Then, in the Maven POM build section, we need to configure the compiler to take into consideration the MapStruct processor along with the Lombok processor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<build>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${compiler-plugin.version}</version>
            <configuration>
                <compilerArgs>
                    <arg>-parameters</arg>
                </compilerArgs>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <dependency>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>${lombok-mapstruct-binding.version}</version>
                    </dependency>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    ...
</build>    

Now, we need to create the MapStruct Mapper class that will reference the Entity and the DTO:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Mapper(componentModel = "cdi")
public interface BookMapper {
    BookDTO toBookDto(Book book);

    Book toBookEntity(BookDTO dto);

    List<BookDTO> toBookDtoList(List<Book> book);

    void updateBookEntityFromDto(BookDTO dto, @MappingTarget Book book);
}

This interface defines:

  • the generated mapper as an application-scoped CDI bean and can be retrieved via @Inject annotation.
  • different methods that maps:
    • an entity to a DTO
    • a DTO to an entity
    • an entities list to a DTOs list
    • an update Entity from a DTO definition

Excellent ! Now, we can add the BookMapper reference for injection in the BookService class:

1
2
3
4
5
6
7
8
9
@ApplicationScoped
@RequiredArgsConstructor
public class BookService {

    private final BookRepository bookRepository;
    private final BookMapper bookMapper;

    ...
}

Then we can use the BookMapper in our findAll() and save() methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public Uni<List<BookDTO>> findAll() {
    return bookRepository.findAll()
            .stream()
            .map(bookMapper::toBookDto)
            .collect()
            .asList();
}

public Uni<BookDTO> save(BookDTO book) {
    return bookRepository.persistAndFlush(
                bookMapper.toBookEntity(book))
            .map(bookMapper::toBookDto);
}

šŸ’” In the save() method, we used the mapper to create a Book instance from the DTO, and then convert the stored Book record to a DTO instance.

That’s all tale! The MapStruct will do all the magic!

Behind the scenes, the Maven Compiler will use the MapStruct Processor to implement the MapStruct Mapper via the BookMapperImpl.java class, available after compilation, in the /target/generated-sources/annotations/ directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-05-27T11:20:38+0200",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 17.0.3 (GraalVM Community)"
)
@ApplicationScoped
public class BookMapperImpl implements BookMapper {

    @Override
    public BookDTO toBookDto(Book book) {
        if ( book == null ) {
            return null;
        }

        BookDTO bookDTO = new BookDTO();

        bookDTO.setId( book.getId() );
        bookDTO.setTitle( book.getTitle() );
        bookDTO.setIsbn( book.getIsbn() );
        bookDTO.setAuthor( book.getAuthor() );
        bookDTO.setPrice( book.getPrice() );

        return bookDTO;
    }

    @Override
    public Book toBookEntity(BookDTO dto) {
        if ( dto == null ) {
            return null;
        }

        Book book = new Book();

        book.setId( dto.getId() );
        book.setTitle( dto.getTitle() );
        book.setIsbn( dto.getIsbn() );
        book.setAuthor( dto.getAuthor() );
        book.setPrice( dto.getPrice() );

        return book;
    }

    @Override
    public List<BookDTO> toBookDtoList(List<Book> book) {
        if ( book == null ) {
            return null;
        }

        List<BookDTO> list = new ArrayList<BookDTO>( book.size() );
        for ( Book book1 : book ) {
            list.add( toBookDto( book1 ) );
        }

        return list;
    }

    @Override
    public void updateBookEntityFromDto(BookDTO dto, Book book) {
        if ( dto == null ) {
            return;
        }

        book.setId( dto.getId() );
        book.setTitle( dto.getTitle() );
        book.setIsbn( dto.getIsbn() );
        book.setAuthor( dto.getAuthor() );
        book.setPrice( dto.getPrice() );
    }
}

Great! I feel very comfortable for delegating the DTO Mapping load to MapStruct šŸ˜ As I always say: “A Good Developer is a Lazy Developer” šŸ˜‚

Implementing the findById, findByAuthor and deleteById methods

Now, we will continue to implement our CRUD methods - like:

  • findById(): find a book using a given book ID.
  • findByAuthor(): find books using a given author name.
  • deleteById(): delete a book using a given book ID.

The implementation looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public Uni<BookDTO> findById(Long id) {
    return bookRepository.findById(id)
            .map(bookMapper::toBookDto);
}

public Uni<List<BookDTO>> findByAuthor(String author) {
    return bookRepository
            .find("lower(author) like lower(CONCAT('%', '" + author + "', '%')) ") <1>
            .stream()
            .map(bookMapper::toBookDto)
            .collect()
            .asList();
}

@ReactiveTransactional <2>
public Uni<Void> deleteById(Long id) {
    return bookRepository.deleteById(id).replaceWithVoid();
}

The line:

  • 1: defines the Hibernate Query that is used to search by author name with ignore case.
  • 2: runs the method into a reactive Mutiny.Session.Transation.

Then, their REST APIs look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RequiredArgsConstructor
@Produces(MediaType.APPLICATION_JSON)
@Path("/api/books")
@RequestScoped
public class BookResource {

    private final BookService bookService;

    ...

    @GET
    @Path("/{id}")
    public Uni<Response> findById(@PathParam("id") Long id) {
        return bookService.findById(id)
                .onItem().ifNotNull().transform(book -> ok(book).build())
                .onItem().ifNull().continueWith(status(NOT_FOUND)::build);
    }

    @GET
    @Path("/author/{name}")
    public Uni<Response> findByAuthor(@PathParam("name") String name) {
        return bookService.findByAuthor(name)
                .onItem().ifNotNull().transform(book -> ok(book).build())
                .onItem().ifNull().continueWith(status(NOT_FOUND)::build);
    }

    @DELETE
    @Path("/{id}")
    public Uni<Void> deleteById(@PathParam("id") Long id) {
        return bookService.deleteById(id);
    }
}

That’s all tale ! šŸ˜ We made our first Quarkus Reactive application ! You can now write some integration tests using RestAssured to be sure that all is working as expected šŸ¤©

If you have questions, please feel free to get in touch with me šŸ˜Ž