micronaut-hibernate-reactive-maven-java

Requirements

Project Structure

application.yml

micronaut:
  application:
    name: reactivehibernate
---
netty:
  default:
    allocator:
      max-order: 3
---
#tag::application[]
application:
  max: 50
#end::application[]
---
#tag::jpa[]
jpa:
  default:
    reactive: true
    properties:
      hibernate:
        connection:
          db-type: mysql
        hbm2ddl:
          auto: create-drop
        show_sql: true
#end::jpa[]
application-prod.yml
micronaut:
  application:
    name: reactivehibernate
---
netty:
  default:
    allocator:
      max-order: 3
---
#tag::application[]
application:
  max: 50
#end::application[]
---
#tag::jpa[]
jpa:
  default:
    reactive: true
    properties:
      hibernate:
        connection:
          db-type: mysql
          url: jdbc:mysql://localhost:3306/micronaut
          username: ${USERNAME}
          password: ${PASSWORD}
        hbm2ddl:
          auto: update
#          auto: create-drop
        show_sql: true
#end::jpa[]
/*
 * Copyright 2017-2024 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package example.micronaut.domain;


import io.micronaut.serde.annotation.Serdeable;
import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.util.HashSet;
import java.util.Set;

import static jakarta.persistence.GenerationType.AUTO;

@Serdeable
@Entity
@Table(name = "genre")
public class Genre {

    @Id
    @GeneratedValue(strategy = AUTO)
    private Long id;

    @NotNull
    @Column(name = "name", nullable = false, unique = true)
    private String name;

    @JsonIgnore
    @OneToMany(mappedBy = "genre")
    private Set<Book> books = new HashSet<>();

    public Genre() {}

    public Genre(@NotNull String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Book> getBooks() {
        return books;
    }

    public void setBooks(Set<Book> books) {
        this.books = books;
    }

    @Override
    public String toString() {
        return "Genre{" +
            "id=" + id +
            ", name='" + name + '\'' +
            '}';
    }
}
Book.java
/*
 * Copyright 2017-2024 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package example.micronaut.domain;

import io.micronaut.serde.annotation.Serdeable;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;

import static jakarta.persistence.GenerationType.AUTO;

@Serdeable
@Entity
@Table(name = "book")
public class Book {

    @Id
    @GeneratedValue(strategy = AUTO)
    private Long id;

    @NotNull
    @Column(name = "name", nullable = false)
    private String name;

    @NotNull
    @Column(name = "isbn", nullable = false)
    private String isbn;

    @ManyToOne
    private Genre genre;

    public Book() {}

    public Book(@NotNull String isbn,
                @NotNull String name,
                Genre genre) {
        this.isbn = isbn;
        this.name = name;
        this.genre = genre;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Genre getGenre() {
        return genre;
    }

    public void setGenre(Genre genre) {
        this.genre = genre;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", isbn='" + isbn + '\'' +
                ", genre=" + genre +
                '}';
    }
}
/*
 * Copyright 2017-2024 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package example.micronaut;

import example.micronaut.domain.Genre;
import jakarta.inject.Singleton;
import org.hibernate.SessionFactory;
import org.hibernate.reactive.stage.Stage;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import jakarta.persistence.PersistenceException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletionStage;

@Singleton // <1>
public class GenreRepositoryImpl implements GenreRepository {

    private static final List<String> VALID_PROPERTY_NAMES = Arrays.asList("id", "name");
    private final ApplicationConfiguration applicationConfiguration;
    private final Stage.SessionFactory sessionFactory;

    public GenreRepositoryImpl(
            ApplicationConfiguration applicationConfiguration, // <2>
            SessionFactory sessionFactory // <3>
    ) {
        this.applicationConfiguration = applicationConfiguration;
        this.sessionFactory = sessionFactory.unwrap(Stage.SessionFactory.class);
    }

    @Override
    public Publisher<Optional<Genre>> findById(long id) {
        return Mono.fromCompletionStage(sessionFactory.withTransaction(session -> // <4>
            find(session, id)
        ));
    }

    CompletionStage<Optional<Genre>> find(Stage.Session session, Long id) {
        return session.find(Genre.class, id).thenApply(Optional::ofNullable);
    }

    @Override
    public Publisher<Genre> save(String name) {
        return Mono.fromCompletionStage(sessionFactory.withTransaction(session -> {
            Genre entity = new Genre(name);
            return session.persist(entity).thenApply(v -> entity);
        }));
    }

    @Override
    public Publisher<Genre> saveWithException(String name) {
        return Mono.fromCompletionStage(sessionFactory.withTransaction(session -> {
            Genre entity = new Genre(name);
            return session.persist(entity).thenApply(v -> {
                throw new PersistenceException();
            });
        }));
    }

    @Override
    public void deleteById(long id) {
        sessionFactory.withTransaction(session -> session.find(Genre.class, id).thenApply(session::remove));
    }

    @Override
    public Publisher<Genre> findAll(SortingAndOrderArguments args) {
        String qlString = createQuery(args);
        return Mono.fromCompletionStage(sessionFactory.withTransaction(session -> {
                    Stage.SelectionQuery<Genre> query = session.createQuery(qlString, Genre.class);
                    query.setMaxResults(args.max() == null ? applicationConfiguration.getMax() : args.max());
                    if (args.offset() != null) {
                        query.setFirstResult(args.offset());
                    }
                    return query.getResultList();
                }))
                .flatMapMany(Flux::fromIterable);
    }

    private String createQuery(SortingAndOrderArguments args) {
        String qlString = "SELECT g FROM Genre as g";
        String order = args.order();
        String sort = args.sort();
        if (order != null && sort != null && VALID_PROPERTY_NAMES.contains(sort)) {
            qlString += " ORDER BY g." + sort + ' ' + order.toLowerCase();
        }
        return qlString;
    }

    @Override
    public Publisher<Integer> update(long id, String name) {
        return Mono.fromCompletionStage(sessionFactory.withTransaction(session -> session.createQuery("UPDATE Genre g SET name = :name where id = :id")
                        .setParameter("name", name)
                        .setParameter("id", id)
                        .executeUpdate()));
    }
}

Running & Testing local

Testing

Running

Running prod (real db)

Running

curl -X "POST" "http://localhost:8080/genres" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{ "name": "music" }'
     
curl -X "POST" "http://localhost:8080/genres" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{ "name": "music#2" }'     
mysql> show tables;
+---------------------+
| Tables_in_micronaut |
+---------------------+
| book                |
| book_SEQ            |
| genre               |
| genre_SEQ           |
+---------------------+
4 rows in set (0,00 sec)


mysql> select * from genre;
+----+---------+
| id | name    |
+----+---------+
|  1 | music   |
|  2 | music#2 |
+----+---------+
2 rows in set (0,00 sec)

Source code: https://github.com/ZbCiok/zjc-examples/tree/main/micronaut/micronaut-hibernate-reactive-maven-java

Mark