1

I build a small Rest API to understand more about the Quarkus framework. Now I would like to start using the framework with its reactive API but I'm struggling to understand some concepts. Currently the project is using RESTEasy Reactive with Jackson, Hibernate Reactive with Panache and the Postgresql Reactive Client.

This are my classes

@Table(name = "cat_role")
@Entity
public class Role extends PanacheEntityBase {
    private static final long serialVersionUID = -2246110460374253942L;

    @Id
    @Column(name = "id", nullable = false, updatable = false)
    @GeneratedValue
    public UUID id;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "name", nullable = false, length = 18)
    public UserRole name;

    public enum UserRole {
        Administrador, Asesor_Empresarial, Asesor_Academico, Alumno
    }
    
}

Now in my service (imperative way) I do the following:

Role.class

    public static Boolean existsRoleSeed(){
        return Role.count() > 0;
    }

RoleService.class

    @Transactional
    public void seedRoles() {
        if (!Role.existsRoleSeed()) {
            for(Role.UserRole userRole: Role.UserRole.values()){
                Role role = Role.builder()
                        .name(userRole)
                        .build();
                
                role.persist();
            }
        }
    }

This will obviously register all roles from the UserRole enum in the database and it is working correctly. What I am trying to achieve is to replicate this method but using the reactive form. These are the changes that I have made in the code

Role.class

    public static Uni<Boolean> existsRoleSeed() {
        return Role.count().map(x -> x > 0);
    }

RoleService.class

    @ReactiveTransactional
    public void seedRoles() {
        Role.existsRoleSeed()
                .map(exists -> {
                    if (!exists) {
                        Multi.createFrom()
                                .iterable(Arrays
                                        .stream(Role.UserRole.values())
                                        .map(userRole -> Role.builder()
                                                .name(userRole)
                                                .build())
                                        .collect(Collectors.toList()))
                                .map(role -> role.persistAndFlush())
                                .subscribe().with(item -> LOGGER.info("Something happened"), failure -> LOGGER.info("Something bad happened"));
                    }
                    return null;
                }).subscribe().with(o -> {
                });
    }

When I run the application, it does not give any error, the logs show that something happened, the database creates the table, however it does not insert anything. I have tried it in different ways, however, I have not succeeded in making it work as I hope.

  • From where does @ReactiveTransactional come from? – Clement Sep 22 '21 at 07:53
  • With Hibernate Reactive with Panache, create your transaction with Panache.transaction(() -> ...) – Clement Sep 22 '21 at 07:53
  • @Clement it's from the Quarkus guide of Hibernate Reactive with Panache, here is an abstract `Transactions Make sure to wrap methods modifying your database (e.g. entity.persist()) within a transaction. Marking a CDI bean method @ReactiveTransactional will do that for you and make that method a transaction boundary.` – Rene Nochebuena Sep 22 '21 at 23:02
  • 1
    @Clement also in the same guide `Alternatively, you can use Panache.withTransaction() for the same effect. We recommend doing so at your application entry point boundaries like your REST endpoint controllers.` [Quarkus Guide](https://quarkus.io/guides/hibernate-reactive-panache) – Rene Nochebuena Sep 22 '21 at 23:03
  • Funny, I didn't know about @ReactiveTransaction. Anyway, your code is not totally correct. I would recommend returning a Uni (and let Quarkus subscribes) instead of trying to do it yourself. ReactiveTransaction only works if you return an async type (Uni). Here with void it won't work. If you don't want to return a Uni, use PAnache.withTransaction. – Clement Sep 24 '21 at 06:18
  • @Clement thanks you give me a idea, and the point its the annotation and how I misunderstand `Marking a CDI bean method @ReactiveTransactional will do that for you and make that method a transaction boundary`. This part of my service is not on the Endpoint boundary and doesn't return anything and as you say I need to use Panache.withTransaction. So based on the @Haroon and your comment I found a solution. – Rene Nochebuena Sep 24 '21 at 06:57

2 Answers2

1

Based on the @Haroon answer and the comment of @Clement I did the following

  • Removed the @ReactiveTransactional as my method returns void and is not on the REST boundary
  • As I removed the annotation I need to use the Panache.withTransaction method
  • And finally in the method I subscribed to the multi

One note is that I changed the transformToUniAndMerge from the @Haroon answer to transformToUniAndConcatenate to maintain the order of the roles.

public void seedRoles() {
        Role.existsRoleSeed()
                .onItem().transformToMulti(exists -> {
                    if (!exists) {
                        return Multi.createFrom().items(Role.UserRole.values());
                    } else {
                        return Multi.createFrom().nothing();
                    }
                })
                .map(userRole -> Role.builder().name(userRole).build())
                .onItem().transformToUniAndConcatenate(role -> Panache.withTransaction(role::persist))
                .subscribe().with(subscription -> LOGGER.infov("Persisting: {0}", subscription));
    }
0

Haven't tested this. But this should give you some kind of idea.

Multi<Role> savedRoles = Role.existsRoleSeed()
                                .onItem().transformToMulti(exists -> {
                                    if (!exists) {
                                        return Multi.createFrom().items(Role.UserRole.values());
                                    }
                                    return Multi.createFrom().nothing();
                                })
                                .map(userRole -> Role.builder().name(userRole).build())
                                .onItem().transformToUniAndMerge(role -> Panache.<Role>withTransaction(role::persist));

Haroon
  • 734
  • 1
  • 6
  • 11
  • Hi, I tried with the sample code and some modifications that the IDE say it could improve. It partially works, when I subscribe to the multi I added a Logger to see the output, `2021-09-21 13:40:00,344 INFO [mx.dnt.ori.ser.RoleService] (vert.x-eventloop-thread-1) Role(name=Administrador)` And is the same for all the Roles, but they aren't persisted on the DB as the DB says that the roles table is empty. Also it does not throw any error either when the method is executed. – Rene Nochebuena Sep 21 '21 at 18:58
  • I'm not aware of the changes you have made. But can you check the following i.) log role.id since its a generated value. ii.) Is there a chance you are looking at different db's `cat_role` table? iii.) Try role::persistAndFlush iv.) Check if jta dependencies are added in pom.xml – Haroon Sep 22 '21 at 05:04
  • Hi, here is a pastebin of all changes, logs and the points that you mentioned on the comment [Pastebin](https://pastebin.com/WeAKNZb1) – Rene Nochebuena Sep 22 '21 at 22:55
  • Can you try removing `@ReactiveTransactional` annotation? link to a [discussion](https://github.com/quarkusio/quarkus/issues/14883) involving transaction and hibernate reactive. Seems transactional annotations are not supported at the moment and Panache.withTransaction is preferred. – Haroon Sep 24 '21 at 10:51
  • Yeah I finally found a solution check the answer y remove the `@ReactiveTransactional` and I subscribe in the same method and everything went fine, thanks. – Rene Nochebuena Sep 24 '21 at 17:05