Skip to content

Update the hibernate example#1923

Open
Allex-Nik wants to merge 3 commits into
masterfrom
modernize-hibernate-example
Open

Update the hibernate example#1923
Allex-Nik wants to merge 3 commits into
masterfrom
modernize-hibernate-example

Conversation

@Allex-Nik

@Allex-Nik Allex-Nik commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1680

  • Bump hibernate to 7.4.4.Final
  • Add JPA plugin
  • Replace xml config with code config
  • Some other fixes

@Allex-Nik Allex-Nik added the examples Something related to the examples label Jul 8, 2026
@Allex-Nik Allex-Nik force-pushed the modernize-hibernate-example branch from 8bfef72 to 3f10f19 Compare July 8, 2026 14:21

@Entity
@Table(name = "Albums")
class AlbumsEntity(

@Allex-Nik Allex-Nik Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The JPA plugin (applies no-arg and all-open compiler plugins) makes entities open classes and generates a constructor with no parameters for each of them.
Making the entities open classes allows Hibernate to apply some features like lazy loading.
The no-arg constructor allows us to remove default values where they are not actually needed and are not intended.

Usage of the JPA plugin is recommended here. The JPA plugin applies all-open since Kotlin 2.3.20.

@Column(name = "ArtistId", nullable = false)
var artistId: Int = 0,
var title: String,
@ManyToOne(fetch = FetchType.LAZY, optional = false)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Artist is loaded only if it is needed, and an album must have an artist

var title: String,
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "ArtistId", nullable = false)
var artist: ArtistsEntity,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This way Hibernate knows and tracks the connection (FK) between entities: we don't need to manually flush ids into the DB and call them with !!.

val artist2 = ArtistsEntity(name = "Queen")
session.persist(artist1)
session.persist(artist2)
session.flush()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The Albums entity now refers to the Artists entity instead of id in the DB

criteriaQuery.select(root)

session.createQuery(criteriaQuery)
session.createSelectionQuery(criteriaQuery)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

createSelectionQuery returns SelectionQuery instead of Query, exposing only methods which are relevant to selection queries.
When used with HQL, createSelectionQuery also throws an exception if passed a query string that begins with "insert", "delete", or "update".

company = c.company,
country = c.country,
customerId = c.customerId ?: -1,
customerId = c.customerId ?: error("Loaded customer must have a generated id"),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It seems better to fail soon in case of null id, even though this situation is highly unlikely


/** DTO used for aggregation projection. */
private data class CountryCountDto(val country: String, val customerCount: Long)
private data class CountryCountDto(val country: String?, val customerCount: Long)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

country is nullable in CustomersEntity. Without this change, if some of the customers in insertSampleData is created without a country, the example would fail with NPE.

val cq: CriteriaQuery<CountryCountDto> = cb.createQuery(CountryCountDto::class.java)
val root: Root<CustomersEntity> = cq.from(CustomersEntity::class.java)

val countryPath = root.get<String>("country")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I found a cool thing here.

Hibernate has so-called Static Metamodel Generator which allows users to construct Criteria queries in a strongly-typed way. For each entity, it generates static metamodel classes, and their properties can be used for type-safe path references instead of strings.

Using it in this example would mean that this generator would create, among others, a CustomersEntity_ class (and put it into the build folder), and we could write, for example,

root.get(CustomersEntity_.country)

instead of

root.get<String?>("country")

I didn't use it here because even though it allows type-safe access, it

  1. requires adding two more dependencies: hibernate-processor and kapt, so it seemed unjustified to add this to replace just two strings;
  2. might introduce unnecessary complexity and distract users from the main thing we want to show them: how to use DataFrame :)

What do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

kapt? hmm, maybe if it were KSP it might be nice, but kapt is quite Java oriented. That said, if the "hibernate get started" guides use it, we should probably copy it. If it's just an optional addition, we can leave it out :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It seems that hibernate-processor doesn't support KSP :(

The guide says: you can use strings, but type-safety is a compelling advantage :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

hmmm, then it might be worth adding. We also boast about type-safety after all, plus it's a good test to see if our plugin and their don't interfere :)

val countryPath = root.get<String>("country")
val idPath = root.get<Long>("customerId")
val countryPath = root.get<String?>("country")
val idPath = root.get<Int?>("customerId")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's Int? in the CustomersEntity. countExpr will still have type JpaExpression<Long!>! because of the count function, so customerCount in CountryCountDto should remain Long.

val criteriaDelete: CriteriaDelete<CustomersEntity> =
criteriaBuilder.createCriteriaDelete(CustomersEntity::class.java)
criteriaDelete.from(CustomersEntity::class.java)

@Allex-Nik Allex-Nik Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think it should be a single transaction. Otherwise, if the deletion query succeeds and the second part fails, the transaction will revert to the state when the DB is empty.

supportRepId = this.supportRepId,
)

private inline fun <T> SessionFactory.withSession(block: (session: org.hibernate.Session) -> T): T =

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we do not need withSession and withTransaction because their logic is roughly the same as in the built-in inTransaction method. The difference is that the exception handling block in rollback in the built-in version applies addSuppressed to the exception (instead of throwing it) without replacing the original exception.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

indeed, best not reinvent the wheel

private inline fun <T> SessionFactory.withReadOnlyTransaction(crossinline block: (session: Session) -> T): T =
fromTransaction { session ->
// Minimize overhead for read operations
session.isDefaultReadOnly = true

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It was already there, just cool things to find out about optimizations for reading.

session.isDefaultReadOnly = true

changes the default mode for entities and proxies in a session from modifiable to read-only. This eliminates the need for Hibernate to dirty-check such objects. Dirty-checking is tracking if an entity was changed and synchronizing these changes with the database. Dirty-checking simplifies development, but introduces overhead: Hibernate has to keep two copies of each object (the snapshot and the live object), and Hibernate has to compare entities with their copies during a flush. With read-only entities, Hibernate doesn't do any of these.

session.hibernateFlushMode = FlushMode.MANUAL

Recommended for read-only transactions. With FlushMode.AUTO, Hibernate synchronizes entities with the database before any query. When we can guarantee that the whole transaction is read-only, this synchronization is not needed.

// Minimize overhead for read operations
session.isDefaultReadOnly = true
session.hibernateFlushMode = FlushMode.MANUAL
try {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is done by the built-in fromTransaction method

# The Kotlin DataFrame Compiler plugin is the same version as the Kotlin plugin.
kotlin-dataframe = { id = "org.jetbrains.kotlin.plugin.dataframe", version.ref = "kotlin" }

kotlin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Should it be in the root libs.versions.toml as well? I saw that the Hibernate version was also specified in the root libs.versions.toml.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yep, please add all dependencies both in the examples as well as in the root toml file. This allows our dependency checker to see if there are any updates even if the specific example is not loaded :)

If you introduce new versions that need to be copied (you don't in your PR, but for the sake of explanation), they should also be added in the dfbuild.buildExampleProjects convention plugin

@Allex-Nik Allex-Nik requested review from Jolanrensen and zaleslaw July 9, 2026 08:54

@Jolanrensen Jolanrensen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

cool!

@Allex-Nik Allex-Nik force-pushed the modernize-hibernate-example branch from 3f10f19 to b0a44ee Compare July 10, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

examples Something related to the examples

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Modernize the Hibernate example if it's possible on the current version

2 participants