Description
In this example, we will write a Micronaut application that exposes some REST endpoints and stores data in a database using Hibernate Reactive. The example uses a one-to-many mapping.
Based on: https://guides.micronaut.io/latest/micronaut-hibernate-reactive-maven-java.html

Requirements
- JDK 17+
- Docker installed to run MySQL.
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[]Genre.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 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 +
'}';
}
}
GenreRepositoryImpl.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;
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
#export MICRONAUT_ENVIRONMENTS=prod
#export USERNAME=your username
#export PASSWORD=your password
Testing
./mvnw test
Running
./mvnw mn:run
Running prod (real db)
export MICRONAUT_ENVIRONMENTS=prod
export USERNAME=your username
export PASSWORD=your password
Running
./mvnw mn:run
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)
–
