Integrating Jakarta Data with Quarkus
For relational database persistence support, Quarkus provides several extensions for developers, including Hibernate ORM, Hibernate Reactive and Hibernate ORM Panache. And Hibernate ORM Panache provides a generic Repository
pattern that similar to the existing popular frameworks, such as Spring Data JPA, Micronaut Data, etc. Quarkus expanded this Panache Repository pattern to none-relational world, such as MongoDb etc. But they do not share the common APIs as Spring Data Commons.
Jakarta Data specification tries to define a collection of common APIs to access relational databases and none-relational databases. As planned, Jakarta Data 1.0 will be part of the upcoming Jakarta EE 11.
Jakarta Data 1.0 was just released, check the final Jakarta Data 1.0 specification documentation here.
If you are looking for an integration solution for Spring framework, check Integrating Jakarta Data with Spring.
In this post, we will utilize the existing Hibernate ORM extension and try to integrate Jakarta Data with Quarkus.
Firstly create a simple Quarkus project via Quarkus Code. Open your browser, navigate to https://code.quarkus.io/, add the following extensions and keep other options as it is.
- Hibernate ORM
- JDBC Drivers-Postgres
- Resteasy Classic
- Resteasy Classic Jackson
Hint the Generate your application button, then download the generated project archive, and extract the files into your local disk, then import into your favorite IDE, eg. JetBrains Intellij IDEA.
Expands the project folder, open the pom.xml file in the project root, and add the following dependencies.
<properties>
<hibernate.version>6.6.0.Alpha1</hibernate.version>
// ...
</properties>
<dependencies>
// ...
<dependency>
<groupId>jakarta.data</groupId>
<artifactId>jakarta.data-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
Here we add Jakarta Data API explicitly and we will use the new Jakarta Data APIs to implement data persistence.
We also update Hibernate ORM to the latest 6.6.0.Alpha1
to align with Jakarta Data 1.0 specification. Including Lombok is to erase the tedious getters/betters, equals
/hashCode
, toString
methods, append builder, etc. for POJO classes.
For multiple annotation processors in the same project, we could have to configure them in a certain order.
Add lombok
and Hiberante jpamodelgen
into the configuration/annotationProcessorPaths
node in the Maven compiler plugin.
<build>
<plugins>
// ...
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
Now let’s create two @Entity
classes, Post
and Comment
which are a one-to-many relation.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "posts")
public class Post implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
UUID id;
@Basic(optional = false)
String title;
@Basic(optional = false)
String content;
@Enumerated
@Builder.Default
Status status = Status.DRAFT;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
// cascade does not work in StatelessSession
// see:https://docs.jboss.org/hibernate/orm/6.6/repositories/html_single/Hibernate_Data_Repositories.html#programming-model
@OnDelete(action = OnDeleteAction.CASCADE)
List<Comment> comments;
@CreationTimestamp
LocalDateTime createdAt;
@UpdateTimestamp
LocalDateTime lastModifiedAt;
@Override
public String toString() {
return "Post{" +
"id=" + id +
", title='" + title + '\'' +
", content='" + content + '\'' +
", createdAt=" + createdAt +
", lastModifiedAt=" + lastModifiedAt +
'}';
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "comments")
public class Comment implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
UUID id;
@Basic(optional = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
@JsonIgnore
private Post post;
@CreationTimestamp
private LocalDateTime createdAt;
}
Next, create two Repository
classes for these @Entity
classes respectively. The Repository interfaces are extended from the Jakarta Data CrudRepository
.
@Repository
public interface PostRepository extends CrudRepository<Post, UUID> {
}
@Repository
public interface CommentRepository extends CrudRepository<Comment, UUID> {
}
Open application.properties
in your editor, add the following property to specify the database type we will use.
quarkus.datasource.db-kind=postgresql
Now compile the project, it will generate the two Repository
implementation classes in the target/generated-sources/annotations folder.
Open the PostRepository_.java
, you will see, unlike the Quarkus Panache or Spring Data JPA, here it uses the Hibernate StatelessSession
to implement the PostRepository
interface.
@RequestScoped
@Generated("org.hibernate.processor.HibernateProcessor")
public class PostRepository_ implements PostRepository {
protected @Nonnull StatelessSession session;
@Inject
public PostRepository_(@Nonnull StatelessSession session) {
this.session = session;
}
//...
}
In Quarkus, all @Repository
implementation classes are annotated with a @RequestScoped
, which means the Repository
beans are shortly lived in a request lifecycle. Like other CDI beans in the project, the Repository
beans can be recoginized by Quarkus Arc container and can be injected into other beans freely. No need extra bean registration as we’v done in Spring integration.
Let’s create a DataInitializer
bean as following to initialize sample data at the application startup. Firstly inject PostRepository
and CommentRepository
beans via @Inject
, the onStart
method observes a StartupEvent
to ensure it will be called at the application startup.
@ApplicationScoped
public class DataInitializer {
private final static Logger LOGGER = Logger.getLogger(DataInitializer.class.getName());
@Inject
PostRepository posts;
@Inject
CommentRepository comments;
@Transactional
public void onStart(@Observes StartupEvent ev) {
LOGGER.info("The application is starting...");
Post first = Post.builder().title("Hello Quarkus").content("My first post of Quarkus").build();
Post second = Post.builder().title("Hello Again, Quarkus").content("My second post of Quarkus").build();
this.posts.insertAll(List.of(first, second));
this.posts.findAll()
.forEach(p -> {
LOGGER.log(Level.INFO, "Post: {0}", new Object[]{p});
var comment = Comment.builder()
.content("Test Comment at " + LocalDateTime.now())
.post(p)
.build();
this.comments.insert(comment);
});
this.posts.findAll().forEach(p -> LOGGER.log(Level.INFO, "Post: {0}", new Object[]{p}));
this.comments.findAll().forEach(c -> LOGGER.log(Level.INFO, "Comment: {0}", new Object[]{c}));
}
void onStop(@Observes ShutdownEvent ev) {
LOGGER.info("The application is stopping...");
}
}
Open your terminal, switch to the project root folder, run the following command to start up the application in development mode.
mvn clean quarkus:dev
We do not configure the datasource connection info in the application.properties file, when starting the application in development mode, Quarkus will startup a Postgres dev service firstly, and setup the datasource connection in the background.
To use Quarkus Dev Services, ensure you have installed Docker Desktop or Podman. More details, check Quarkus Dev Services.
I encountered a weird issue when upgrading to use Quarkus 3.11 here, check quarkus#40932. Add a property
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect
into the application.properties explicitly to overcome this issue temporarily.
The following codes demonstrate using the Repository
built-in methods to insert some sample data and display the saved data.
@Inject
private PostRepository posts;
// ...
Post first = Post.builder().title("Hello Quarkus").content("My first post of Quarkus").build();
Post second = Post.builder().title("Hello Again, Quarkus").content("My second post of Quarkus").build();
this.posts.insertAll(List.of(first, second));
this.posts.findAll().forEach(p -> LOGGER.log(Level.INFO, "Post:{0}", p));
assertEquals(2, this.posts.findAll().toList().size(), "result list size is 2");
For the complete sample codes, please check
PostRepositoryTest
.
In the Integrating Jakarta Data with Spring, we’ve encountered Spring transaction management because Spring lacks transaction support when using Hibernate StatelessSession
.
In Quarkus, the transaction management works seamlessly with the Jakarta Data Repository interfaces.
To enable transaction, add a @Transactional
annotation on the PostRepository
interface or on the certain methods, eg. deleteAll()
, when it is compiled, it will be added in the generated implementation class too.
@Repository
public interface PostRepository extends CrudRepository<Post, UUID> {
@Delete
@Transactional
void deleteAll();
}
In PostRepositoryTest
, create a @BeforeEach
hook method, we can use it to clean up the sample data for all tests.
@BeforeEach
public void setup() {
this.posts.deleteAll();
}
We have set @OneToMany(cascade = CascadeType.ALL...
on the comments
field, but this does not work when using Jakarta Data Repository because StatelessSession
is lack of cascade support. If there are some Comment
dirty data that exists in the comments
table, the above invoking deleteAll()
will fail the tests due to the foreign key constraints in the comments
table.
More details about Jakarta Data implementation in Hibernate, check Hibernate Data Repositories.
To enable on delete cascade on comments
foreign key(post_id
), add a Hibernate specific @OnDelete(action = OnDeleteAction.CASCADE)
on the comments
field.
Then PostRepository.deleteAll()
will work well as expected. Let’s enable Hibernate SQL log to check what happened.
Add quarkus.hibernate-orm.log.sql=true
to the application.properties file. And then run PostRepositoryTest
, watch the logs in the IDE console, the following sql is executed after the posts
and comments
tables are created.
alter table if exists comments
add constraint FKh4c7lvsc298whoyd4w9ta25cr
foreign key (post_id)
references posts
on delete cascade
As you see, it adds a on delete cascade
clause on the foreign key constraint.
As I mentioned in the post Integrating Jakarta Data with Spring, utilizing the Jakarta Data built-in annotations, eg. @Query
, @Find
(and @By
, @Param
, @OrderBy
, and Order
, Limit
, PageRequest
method parameters), @Insert
/@Save
/@Update
, @Delete
, etc., Jakarta Data allows you create custom methods freely in the Repository interface or a standalone interface.
Let’s create a simple interface Blogger
which is used to manage the blog posts and comments, do not forget to annotate it with the Jakarta Data @Repository
.
@Transactional
@Repository
public interface Blogger {
// ...
}
To perform basic CRUD operations on Entity classes, just need to add @Find
, @Insert
, @Update
, @Delete
annotations on the methods that the input parameter or result type matches Entity type.
@Find
Optional<Post> byId(UUID id);
@Insert
Post insert(Post post);
@Insert
Comment insert(Comment comment);
@Update
Post update(Post post);
@Delete
void delete(Post post);
To return a pageable result, use Page
as result type which includes parameterized type to indicate the result data type. And add a PageRequest
as method parameter to accept pagintion settings for the client.
@Find
@OrderBy("createdAt")
Page<Post> byTitle(@Pattern String title, PageRequest page);
The @OrderBy
will sort the query result by the specified property in the generated query.
Alternatively, you can specify an Order
parameter to the query result data.
To get a smaller chunk of a large result list, you can use a Limit
parameter to specify the data range by position.
@Find
List<Post> byStatus(Status status, Order<Post> order, Limit limit);
The following is an example using @Query
and JDQL(Jakarta Data Query Language). This method accepts a parameter named title and an extra PageRequest
for pagination request, and return a paginated result. The query result will be mapped to result parameterized type PostSummary
automatically.
@Query("""
SELECT p.id, p.title, size(c) FROM Post p LEFT JOIN p.comments c
WHERE p.title LIKE :title
OR p.content LIKE :title
OR c.content LIKE :title
GROUP BY p
ORDER BY p.createdAt DESC
""")
Page<PostSummary> allPosts(@Param("title") String title, PageRequest page);
The BloggerTest
shows the usage of these custom methods.
var blog = blogger.insert(Post.builder().title("Jakarta Data").content("content of Jakarta Data").build());
blogger.insert(Comment.builder().post(blog).content("test comment").build());
blogger.insert(Post.builder().title("Quarkus and Jakarta Data").content("content of Quarkus and Jakarta Data").build());
var byTitlePattern = blogger.byTitle("%Jakarta%", PageRequest.ofPage(1, 10, true));
assertThat(byTitlePattern.totalElements()).isEqualTo(2);
byTitlePattern.content().forEach(post -> log.debug("byTitlePattern post: {}", post));
var byStatusPattern = blogger.byStatus(Status.DRAFT, Order.by(List.of(Sort.desc("createdAt"), Sort.asc("title"))), Limit.range(1, 10));
assertThat(byStatusPattern.size()).isEqualTo(2);
byStatusPattern.forEach(post -> log.debug("byStatusPattern post: {}", post));
var pagedPosts = blogger.allPosts("%Jakarta%", PageRequest.ofPage(1, 10, true));
assertThat(pagedPosts.totalElements()).isEqualTo(2);
pagedPosts.content().forEach(post -> log.debug("post: {}", post));
Get the complete sample codes from my Github, and experience the Jakarta Data features yourself.