Skip to content

Add Quarkus Data Hibernate getting started tutorial#2784

Draft
lucamolteni wants to merge 2 commits into
quarkusio:mainfrom
lucamolteni:39-tutorial-squash
Draft

Add Quarkus Data Hibernate getting started tutorial#2784
lucamolteni wants to merge 2 commits into
quarkusio:mainfrom
lucamolteni:39-tutorial-squash

Conversation

@lucamolteni

Copy link
Copy Markdown
Contributor

Summary

Step-by-step guide covering Quarkus Data Hibernate repositories: entity mapping, repository pattern, managed vs stateless sessions, native SQL queries, and Hibernate Reactive.

Note: Class and package names are not final — pending the rename in quarkusio/quarkus#54610. Tutorial is tested against Quarkus 3.37.

Step-by-step guide covering Quarkus Data Hibernate repositories: entity mapping, repository pattern, managed vs stateless sessions, native SQL queries, and Hibernate Reactive.
@lucamolteni

Copy link
Copy Markdown
Contributor Author

@FroMage @yrodiere take a look at it when you can

@yrodiere yrodiere left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, here are a few comments.

The main thing, IMO, is that we probably need to rethink the "start with SQL and go from there" approach for a tutorial. Yes, I know that you had presented that a long time ago, but I don't think it works as well as we had hoped specifically for this tutorial, so it's worth reconsidering.

I believe we're trying to conflate three things into this tutorial, and that's too much and should be split:

  1. An entry point for Quarkus Data documentation, telling you what you can do and what to chose
  2. A getting started guide for entity mapping and repositories with Quarkus Data
  3. A getting started guide for pure-SQL repositories with Quarkus data

Comment on lines +34 to +39
We'll start this tutorial by creating a new empty Quarkus application. The easiest way to do so is to have the Quarkus CLI tool available:

[source,bash]
----
quarkus create app --name="Quarkus Data Tutorial" quarkus-data-tutorial --platform-bom=io.quarkus.platform:quarkus-bom:3.37.0.CR1
----

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are asciidoc "partials" that you can include for this, and will display a nice UI where you can chose between the CLI, Maven or Gradle.

E.g. https://quarkus.io/guides/hibernate-orm#setting-up-and-configuring-hibernate-orm

Image

Or https://quarkus.io/guides/getting-started#bootstrapping-the-project

Image

Maybe use something similar here?

Comment on lines +47 to +48
quarkus extension add quarkus-data-hibernate
quarkus extension add quarkus-jdbc-postgresql

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments, take what you like:

  1. Same as above, you might want to present CLI/Maven/Gradle
  2. You can add extensions directly in the first CLI call (quarkus create app), no need for a separate call
  3. If you really want to split this, you can probably use a single CLI call to add both extensions, which would likely be a bit faster. Not sure if extensions are comma-separated or space-separated though.

----

The first dependency, `quarkus-data-hibernate`, is the Quarkus extension that provides everything you need for database access.
The second dependency, `quarkus-jdbc-postgresql`, is the JDBC driver that tells Quarkus how to talk to a PostgreSQL database. If you're using a different database (MySQL, MariaDB, H2, etc.), pick the corresponding `quarkus-jdbc-*` driver instead. Quarkus Data Hibernate cannot work without a JDBC driver.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A link to the relevant section of the datasource guide that lists available extensions would likely make sense here?

Comment on lines +56 to +57
Quarkus Data Hibernate uses the Hibernate annotation processor, a key component that reads your code at compile time, validates queries, and generates implementation classes under `target/generated-sources/annotations/`.
These generated classes are compiled together with your code and wired up automatically by Quarkus. They are also simple to read, in case you want to understand how it works internally.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this path holds for Gradle.

Suggested change
Quarkus Data Hibernate uses the Hibernate annotation processor, a key component that reads your code at compile time, validates queries, and generates implementation classes under `target/generated-sources/annotations/`.
These generated classes are compiled together with your code and wired up automatically by Quarkus. They are also simple to read, in case you want to understand how it works internally.
Quarkus Data Hibernate uses the Hibernate annotation processor, a key component that reads your repository interfaces at compile time, validates queries, and generates implementation classes.
These generated classes are compiled together with your code and wired up automatically by Quarkus. They are also simple to read, in case you want to understand how it works internally.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. Perhaps here it's too early, but somewhere, it would be useful to tell people where those files are generated.

Comment on lines +61 to +78
[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters> <!-- already present -->
<annotationProcessorPathsUseDepMgmt>true</annotationProcessorPathsUseDepMgmt>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
----

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs Gradle example?


=== About entity identifiers

`WithId.AutoLong` is more powerful than Hibernate's traditional `@Id @GeneratedValue` pair: there are variants for different ID types out of the box:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not more powerful, quite obviously, since it's based on Hibernate ORM :)

It's convenient syntactical sugar.

}
----
<1> We collect the books we modify so we can update them all at the end, after all business logic has run. Updating inside the body will result in multiple SQL update statements.
<2> In a stateless session, lazy associations are not fetched automatically. We must load the publisher explicitly. A join fetch query in the Book repository could be used instead of a separate fetch, but for the scope of the tutorial we're not introducing join fetches.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO you're getting too far here. You're addressing beginners -- if you think join fetches are too complicated to explain here, you shouldn't mention them. Also, beginners might not even know what a lazy association is, so we'd need to explain that.

Perhaps more importantly, this is rather bad code. In fact, it's textbook N+1 select.

I think it would make more sense to explain how one would properly implement this with a stateless session. If you keep a ToOne association here, it would need to be eager, and if you replace it with a ToMany association, it would make sense to explain join fetches, because they would be a core feature for any application using stateless session.

}
}
----
<1> We use `FetchType.LAZY` so that loading a book doesn't automatically join and fetch its publisher.

@yrodiere yrodiere Jun 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of odd. Lazy to-ones are not that frequent in my experience. Neither is explicit configuration of laziness, btw -- defaults are generally fine, and probably should be in such a getting started tutorial.

You could find some other example with a to-many associations? That way you don't even need to mention laziness here, and can mention it further down.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I thought fetches were explicit in stateless sessions?

Comment on lines +676 to +677
The test is exactly the same: the behavior hasn't changed, just the implementation got simpler.
It's up to you to decide whether to use a Managed Entity or a Stateless one. The Managed Repository is more expressive, but reasoning about it is more complex and might require knowing some details about how Hibernate handles objects. If you're an experienced Hibernate developer, you probably already know the discussions about n+1 queries, and you expect that changing an object's field doesn't immediately hit the database. Some users prefer a more explicit way to control the database. Either way is fine, and Quarkus Data Hibernate makes it easy to use both and to switch without committing to one style or the other.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The test is exactly the same: the behavior hasn't changed, just the implementation got simpler.
It's up to you to decide whether to use a Managed Entity or a Stateless one. The Managed Repository is more expressive, but reasoning about it is more complex and might require knowing some details about how Hibernate handles objects. If you're an experienced Hibernate developer, you probably already know the discussions about n+1 queries, and you expect that changing an object's field doesn't immediately hit the database. Some users prefer a more explicit way to control the database. Either way is fine, and Quarkus Data Hibernate makes it easy to use both and to switch without committing to one style or the other.
The test is exactly the same: the behavior hasn't changed, the implementation only got simpler.
It's up to you to decide whether to use a Managed Entity or a Stateless one. The Managed Repository is more expressive, but reasoning about it is more complex and might require knowing some details about how Hibernate handles objects. If you're an experienced Hibernate developer, you probably already know the discussions about n+1 queries, and you expect that changing an object's field doesn't immediately hit the database. Some users prefer a more explicit way to control the database. Either way is fine, and Quarkus Data Hibernate makes it easy to use both and to switch without committing to one style or the other.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that N+1 issues are related to "not immediately hitting the DB" and this may appear to suggest that 🤔

Comment on lines +788 to +789
<1> `@RunOnVertxContext` ensures the test runs on the Vert.x event loop, which is required for reactive database access.
<2> `@TestReactiveTransaction` opens a reactive session and transaction for the test, and rolls back at the end.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not about this tutorial per se, but... this is awfully complicated. In an ideal world we'd require only @TestTransaction and @RunOnVertxContext would be inferred from UniAsserter. No?

cc @FroMage

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, we should improve this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Come to think about it, I wonder if we should not use the same sort of mechanism as I recently did to allow returning a Uni from @Startup and just plug it in the vertx loop. That would make for much nicer test methods.


[source,bash]
----
quarkus create app --name="Quarkus Data Tutorial" quarkus-data-tutorial --platform-bom=io.quarkus.platform:quarkus-bom:3.37.0.CR1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should hardcode versions there, it will not age well.

Comment on lines +56 to +57
Quarkus Data Hibernate uses the Hibernate annotation processor, a key component that reads your code at compile time, validates queries, and generates implementation classes under `target/generated-sources/annotations/`.
These generated classes are compiled together with your code and wired up automatically by Quarkus. They are also simple to read, in case you want to understand how it works internally.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. Perhaps here it's too early, but somewhere, it would be useful to tell people where those files are generated.

Quarkus Data Hibernate uses the Hibernate annotation processor, a key component that reads your code at compile time, validates queries, and generates implementation classes under `target/generated-sources/annotations/`.
These generated classes are compiled together with your code and wired up automatically by Quarkus. They are also simple to read, in case you want to understand how it works internally.

To enable it, find the `maven-compiler-plugin` in your `pom.xml` (it was generated by `quarkus create app`) and add the annotation processor configuration to it:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's machinery for that, not unless it includes the quickstarts code.

Perhaps @aloubyansky or @ia3andy know?

Quarkus Data Hibernate uses the Hibernate annotation processor, a key component that reads your code at compile time, validates queries, and generates implementation classes under `target/generated-sources/annotations/`.
These generated classes are compiled together with your code and wired up automatically by Quarkus. They are also simple to read, in case you want to understand how it works internally.

To enable it, find the `maven-compiler-plugin` in your `pom.xml` (it was generated by `quarkus create app`) and add the annotation processor configuration to it:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice.

Comment on lines +89 to +143
Now create an `import.sql` file in `src/main/resources/` to prepopulate your database. This will be useful for this tutorial:

[source,bash]
----
CREATE SEQUENCE IF NOT EXISTS Publisher_SEQ START WITH 5 INCREMENT BY 50;

CREATE TABLE IF NOT EXISTS Publisher (
id BIGINT PRIMARY KEY DEFAULT nextval('Publisher_SEQ'),
name VARCHAR(255),
country VARCHAR(10),
allowsDiscounts BOOLEAN
);

CREATE SEQUENCE IF NOT EXISTS Book_SEQ START WITH 9 INCREMENT BY 50;

CREATE TABLE IF NOT EXISTS Book (
id BIGINT PRIMARY KEY DEFAULT nextval('Book_SEQ'),
title VARCHAR(255),
isbn VARCHAR(20),
price NUMERIC(10,2),
publisher_id BIGINT REFERENCES Publisher(id)
);

DELETE FROM Book;
DELETE FROM Publisher;

INSERT INTO Publisher (id, name, country, allowsDiscounts) VALUES (1, 'Addison-Wesley', 'US', true);
INSERT INTO Publisher (id, name, country, allowsDiscounts) VALUES (2, 'Prentice Hall', 'US', false);
INSERT INTO Publisher (id, name, country, allowsDiscounts) VALUES (3, 'O''Reilly', 'UK', true);
INSERT INTO Publisher (id, name, country, allowsDiscounts) VALUES (4, 'Manning', 'UK', false);

INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (1, 'Effective Java', '978-0134685991', 45.00, 1);
INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (2, 'Clean Code', '978-0132350884', 40.00, 1);
INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (3, 'Domain-Driven Design', '978-0321125217', 55.00, 2);
INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (4, 'Refactoring', '978-0134757599', 50.00, 2);
INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (5, 'The Pragmatic Programmer', '978-0135957059', 42.00, 3);
INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (6, 'Head First Design Patterns', '978-0596007126', 38.00, 3);
INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (7, 'Java Concurrency in Practice', '978-0321349606', 48.00, 4);
INSERT INTO Book (id, title, isbn, price, publisher_id) VALUES (8, 'Kotlin in Action', '978-1617293290', 44.00, 4);
----

This SQL script creates sequences for ID generation, the `Publisher` and `Book` tables, and inserts sample data.
Each book is linked to a publisher.
The sequences start past the imported data so that new IDs don't collide.
While developing, Quarkus imports this SQL file automatically at startup, so in this tutorial we have something to start with.

Quarkus Data Hibernate can handle the database schema for you.
Since our `import.sql` already handles the full schema creation, let's tell Hibernate not to manage it and tell Dev Services to run our script when the database container starts.
Add this to `src/main/resources/application.properties`:

[source,properties]
----
quarkus.hibernate-orm.schema-management.strategy=none
quarkus.datasource.devservices.init-script-path=import.sql
----

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed. This is scary as hell, and only required for freaks like @maxandersen who are allergic to @Entity, let's split this off into a file for Max please :)

Comment on lines +676 to +677
The test is exactly the same: the behavior hasn't changed, just the implementation got simpler.
It's up to you to decide whether to use a Managed Entity or a Stateless one. The Managed Repository is more expressive, but reasoning about it is more complex and might require knowing some details about how Hibernate handles objects. If you're an experienced Hibernate developer, you probably already know the discussions about n+1 queries, and you expect that changing an object's field doesn't immediately hit the database. Some users prefer a more explicit way to control the database. Either way is fine, and Quarkus Data Hibernate makes it easy to use both and to switch without committing to one style or the other.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that N+1 issues are related to "not immediately hitting the DB" and this may appear to suggest that 🤔

[source,java]
----
@Entity
public class Book extends WithId.AutoLong implements PanacheEntity.Managed {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public class Book extends WithId.AutoLong implements PanacheEntity.Managed {
public class Book extends ManagedEntity {

@ManyToOne(fetch = FetchType.LAZY)
public Publisher publisher;

public interface Repo extends PanacheRepository<Book> { // <1>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public interface Repo extends PanacheRepository<Book> { // <1>
public interface Repo extends ManagedRepository<Book> { // <1>

List<Book> findByPublisherCountry(String country);
}

public interface ReactiveRepo extends PanacheRepository.Reactive<Book, Long> { // <2>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public interface ReactiveRepo extends PanacheRepository.Reactive<Book, Long> { // <2>
public interface ReactiveRepo extends ManagedRepository.Reactive<Book> { // <2>

Comment on lines +788 to +789
<1> `@RunOnVertxContext` ensures the test runs on the Vert.x event loop, which is required for reactive database access.
<2> `@TestReactiveTransaction` opens a reactive session and transaction for the test, and rolls back at the end.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, we should improve this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants