Add Quarkus Data Hibernate getting started tutorial#2784
Conversation
Step-by-step guide covering Quarkus Data Hibernate repositories: entity mapping, repository pattern, managed vs stateless sessions, native SQL queries, and Hibernate Reactive.
yrodiere
left a comment
There was a problem hiding this comment.
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:
- An entry point for Quarkus Data documentation, telling you what you can do and what to chose
- A getting started guide for entity mapping and repositories with Quarkus Data
- A getting started guide for pure-SQL repositories with Quarkus data
| 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 | ||
| ---- |
There was a problem hiding this comment.
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
Or https://quarkus.io/guides/getting-started#bootstrapping-the-project
Maybe use something similar here?
| quarkus extension add quarkus-data-hibernate | ||
| quarkus extension add quarkus-jdbc-postgresql |
There was a problem hiding this comment.
Some comments, take what you like:
- Same as above, you might want to present CLI/Maven/Gradle
- You can add extensions directly in the first CLI call (
quarkus create app), no need for a separate call - 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. |
There was a problem hiding this comment.
A link to the relevant section of the datasource guide that lists available extensions would likely make sense here?
| 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. |
There was a problem hiding this comment.
Not sure this path holds for Gradle.
| 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. |
There was a problem hiding this comment.
I don't know. Perhaps here it's too early, but somewhere, it would be useful to tell people where those files are generated.
| [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> | ||
| ---- |
|
|
||
| === 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: |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Indeed, I thought fetches were explicit in stateless sessions?
| 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. |
There was a problem hiding this comment.
| 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. |
There was a problem hiding this comment.
I'm not sure that N+1 issues are related to "not immediately hitting the DB" and this may appear to suggest that 🤔
| <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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Absolutely, we should improve this.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
I don't think we should hardcode versions there, it will not age well.
| 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. |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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: |
| 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 | ||
| ---- |
There was a problem hiding this comment.
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 :)
| 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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
| 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> |
There was a problem hiding this comment.
| 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> |
There was a problem hiding this comment.
| public interface ReactiveRepo extends PanacheRepository.Reactive<Book, Long> { // <2> | |
| public interface ReactiveRepo extends ManagedRepository.Reactive<Book> { // <2> |
| <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. |
There was a problem hiding this comment.
Absolutely, we should improve this.
Summary
Step-by-step guide covering Quarkus Data Hibernate repositories: entity mapping, repository pattern, managed vs stateless sessions, native SQL queries, and Hibernate Reactive.