Update the hibernate example#1923
Conversation
8bfef72 to
3f10f19
Compare
|
|
||
| @Entity | ||
| @Table(name = "Albums") | ||
| class AlbumsEntity( |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
The Albums entity now refers to the Artists entity instead of id in the DB
| criteriaQuery.select(root) | ||
|
|
||
| session.createQuery(criteriaQuery) | ||
| session.createSelectionQuery(criteriaQuery) |
There was a problem hiding this comment.
| company = c.company, | ||
| country = c.country, | ||
| customerId = c.customerId ?: -1, | ||
| customerId = c.customerId ?: error("Loaded customer must have a generated id"), |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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
- requires adding two more dependencies:
hibernate-processorandkapt, so it seemed unjustified to add this to replace just two strings; - might introduce unnecessary complexity and distract users from the main thing we want to show them: how to use DataFrame :)
What do you think?
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
It seems that hibernate-processor doesn't support KSP :(
The guide says: you can use strings, but type-safety is a compelling advantage :)
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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) | ||
|
|
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
It was already there, just cool things to find out about optimizations for reading.
session.isDefaultReadOnly = truechanges 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.MANUALRecommended 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 { |
There was a problem hiding this comment.
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" } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
3f10f19 to
b0a44ee
Compare
Fixes #1680