-
Notifications
You must be signed in to change notification settings - Fork 82
Update the hibernate example #1923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,12 @@ package org.jetbrains.kotlinx.dataframe.examples.hibernate | |
|
|
||
| import jakarta.persistence.Column | ||
| import jakarta.persistence.Entity | ||
| import jakarta.persistence.FetchType | ||
| import jakarta.persistence.GeneratedValue | ||
| import jakarta.persistence.GenerationType | ||
| import jakarta.persistence.Id | ||
| import jakarta.persistence.JoinColumn | ||
| import jakarta.persistence.ManyToOne | ||
| import jakarta.persistence.Table | ||
| import org.jetbrains.kotlinx.dataframe.annotations.ColumnName | ||
| import org.jetbrains.kotlinx.dataframe.annotations.DataSchema | ||
|
|
@@ -17,9 +20,10 @@ class AlbumsEntity( | |
| @Column(name = "AlbumId") | ||
| var albumId: Int? = null, | ||
| @Column(name = "Title", length = 160, nullable = false) | ||
| var title: String = "", | ||
| @Column(name = "ArtistId", nullable = false) | ||
| var artistId: Int = 0, | ||
| var title: String, | ||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "ArtistId", nullable = false) | ||
| var artist: ArtistsEntity, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ) | ||
|
|
||
| @Entity | ||
|
|
@@ -30,7 +34,7 @@ class ArtistsEntity( | |
| @Column(name = "ArtistId") | ||
| var artistId: Int? = null, | ||
| @Column(name = "Name", length = 120, nullable = false) | ||
| var name: String = "", | ||
| var name: String, | ||
| ) | ||
|
|
||
| @Entity | ||
|
|
@@ -41,9 +45,9 @@ class CustomersEntity( | |
| @Column(name = "CustomerId") | ||
| var customerId: Int? = null, | ||
| @Column(name = "FirstName", length = 40, nullable = false) | ||
| var firstName: String = "", | ||
| var firstName: String, | ||
| @Column(name = "LastName", length = 20, nullable = false) | ||
| var lastName: String = "", | ||
| var lastName: String, | ||
| @Column(name = "Company", length = 80) | ||
| var company: String? = null, | ||
| @Column(name = "Address", length = 70) | ||
|
|
@@ -61,7 +65,7 @@ class CustomersEntity( | |
| @Column(name = "Fax", length = 24) | ||
| var fax: String? = null, | ||
| @Column(name = "Email", length = 60, nullable = false) | ||
| var email: String = "", | ||
| var email: String, | ||
| @Column(name = "SupportRepId") | ||
| var supportRepId: Int? = null, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,11 @@ import jakarta.persistence.criteria.CriteriaDelete | |
| import jakarta.persistence.criteria.CriteriaQuery | ||
| import jakarta.persistence.criteria.Root | ||
| import org.hibernate.FlushMode | ||
| import org.hibernate.Session | ||
| import org.hibernate.SessionFactory | ||
| import org.hibernate.cfg.Configuration | ||
| import org.hibernate.hikaricp.internal.HikariCPConnectionProvider | ||
| import org.hibernate.jpa.HibernatePersistenceConfiguration | ||
| import org.hibernate.tool.schema.Action | ||
| import org.jetbrains.kotlinx.dataframe.DataFrame | ||
| import org.jetbrains.kotlinx.dataframe.DataRow | ||
| import org.jetbrains.kotlinx.dataframe.api.asSequence | ||
|
|
@@ -43,17 +46,16 @@ fun main() { | |
| } | ||
|
|
||
| private fun SessionFactory.insertSampleData() { | ||
| withTransaction { session -> | ||
| inTransaction { session -> | ||
| // a few artists and albums (minimal, not used further; just demo schema) | ||
| val artist1 = ArtistsEntity(name = "AC/DC") | ||
| val artist2 = ArtistsEntity(name = "Queen") | ||
| session.persist(artist1) | ||
| session.persist(artist2) | ||
| session.flush() | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| session.persist(AlbumsEntity(title = "High Voltage", artistId = artist1.artistId!!)) | ||
| session.persist(AlbumsEntity(title = "Back in Black", artistId = artist1.artistId!!)) | ||
| session.persist(AlbumsEntity(title = "A Night at the Opera", artistId = artist2.artistId!!)) | ||
| session.persist(AlbumsEntity(title = "High Voltage", artist = artist1)) | ||
| session.persist(AlbumsEntity(title = "Back in Black", artist = artist1)) | ||
| session.persist(AlbumsEntity(title = "A Night at the Opera", artist = artist2)) | ||
| // customers we'll analyze using DataFrame | ||
| session.persist( | ||
| CustomersEntity( | ||
|
|
@@ -89,15 +91,15 @@ private fun SessionFactory.loadCustomersAsDataFrame(): DataFrame<DfCustomers> = | |
| val root: Root<CustomersEntity> = criteriaQuery.from(CustomersEntity::class.java) | ||
| criteriaQuery.select(root) | ||
|
|
||
| session.createQuery(criteriaQuery) | ||
| session.createSelectionQuery(criteriaQuery) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| .resultList | ||
| .map { c -> | ||
| DfCustomers( | ||
| address = c.address, | ||
| city = c.city, | ||
| company = c.company, | ||
| country = c.country, | ||
| customerId = c.customerId ?: -1, | ||
| customerId = c.customerId ?: error("Loaded customer must have a generated id"), | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| email = c.email, | ||
| fax = c.fax, | ||
| firstName = c.firstName, | ||
|
|
@@ -118,7 +120,7 @@ private fun SessionFactory.loadCustomersAsDataFrame(): DataFrame<DfCustomers> = | |
| } | ||
|
|
||
| /** 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) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| /** | ||
| * **Hibernate + Criteria API:** | ||
|
|
@@ -132,9 +134,8 @@ private fun SessionFactory.countCustomersPerCountryWithHibernate() { | |
| val cb = session.criteriaBuilder | ||
| val cq: CriteriaQuery<CountryCountDto> = cb.createQuery(CountryCountDto::class.java) | ||
| val root: Root<CustomersEntity> = cq.from(CustomersEntity::class.java) | ||
|
|
||
| val countryPath = root.get<String>("country") | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
What do you think?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that The guide says: you can use strings, but type-safety is a compelling advantage :)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 idPath = root.get<Long>("customerId") | ||
| val countryPath = root.get<String?>("country") | ||
| val idPath = root.get<Int?>("customerId") | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's |
||
|
|
||
| val countExpr = cb.count(idPath) | ||
|
|
||
|
|
@@ -148,7 +149,7 @@ private fun SessionFactory.countCustomersPerCountryWithHibernate() { | |
| cq.groupBy(countryPath) | ||
| cq.orderBy(cb.desc(countExpr)) | ||
|
|
||
| val results = session.createQuery(cq).resultList | ||
| val results = session.createSelectionQuery(cq).resultList | ||
| results.forEach { dto -> | ||
| println("${dto.country}: ${dto.customerCount} customers") | ||
| } | ||
|
|
@@ -175,22 +176,18 @@ private fun DataFrame<DfCustomers>.analyzeAndPrintResults() { | |
| .print(columnTypes = true, borders = true) | ||
| } | ||
|
|
||
| private fun SessionFactory.replaceCustomersFromDataFrame(df: DataFrame<DfCustomers>) { | ||
| withTransaction { session -> | ||
| private fun SessionFactory.replaceCustomersFromDataFrame(df: DataFrame<DfCustomers>) = | ||
| inTransaction { session -> | ||
| val criteriaBuilder: CriteriaBuilder = session.criteriaBuilder | ||
| val criteriaDelete: CriteriaDelete<CustomersEntity> = | ||
| criteriaBuilder.createCriteriaDelete(CustomersEntity::class.java) | ||
| criteriaDelete.from(CustomersEntity::class.java) | ||
|
|
||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| session.createMutationQuery(criteriaDelete).executeUpdate() | ||
| } | ||
|
|
||
| withTransaction { session -> | ||
| df.asSequence().forEach { row -> | ||
| session.persist(row.toCustomersEntity()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun DataRow<DfCustomers>.toCustomersEntity(): CustomersEntity = | ||
| CustomersEntity( | ||
|
|
@@ -209,40 +206,22 @@ private fun DataRow<DfCustomers>.toCustomersEntity(): CustomersEntity = | |
| supportRepId = this.supportRepId, | ||
| ) | ||
|
|
||
| private inline fun <T> SessionFactory.withSession(block: (session: org.hibernate.Session) -> T): T = | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we do not need
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed, best not reinvent the wheel |
||
| openSession().use(block) | ||
|
|
||
| private inline fun SessionFactory.withTransaction(block: (session: org.hibernate.Session) -> Unit) { | ||
| withSession { session -> | ||
| session.beginTransaction() | ||
| try { | ||
| block(session) | ||
| session.transaction.commit() | ||
| } catch (e: Exception) { | ||
| session.transaction.rollback() | ||
| throw e | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Read-only transaction helper for SELECT queries to minimize overhead. */ | ||
| private inline fun <T> SessionFactory.withReadOnlyTransaction(block: (session: org.hibernate.Session) -> T): T = | ||
| withSession { session -> | ||
| session.beginTransaction() | ||
| private inline fun <T> SessionFactory.withReadOnlyTransaction(crossinline block: (session: Session) -> T): T = | ||
| fromTransaction { session -> | ||
| // Minimize overhead for read operations | ||
| session.isDefaultReadOnly = true | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = 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 |
||
| session.hibernateFlushMode = FlushMode.MANUAL | ||
| try { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is done by the built-in |
||
| val result = block(session) | ||
| session.transaction.commit() | ||
| result | ||
| } catch (e: Exception) { | ||
| session.transaction.rollback() | ||
| throw e | ||
| } | ||
| block(session) | ||
| } | ||
|
|
||
| private fun buildSessionFactory(): SessionFactory { | ||
| // Load configuration from resources/hibernate/hibernate.cfg.xml | ||
| return Configuration().configure("hibernate/hibernate.cfg.xml").buildSessionFactory() | ||
| } | ||
| private fun buildSessionFactory(): SessionFactory = | ||
| HibernatePersistenceConfiguration("hibernate-example") | ||
| .managedClasses(CustomersEntity::class.java, ArtistsEntity::class.java, AlbumsEntity::class.java) | ||
| .jdbcUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1") | ||
| .jdbcCredentials("sa", "") | ||
| .schemaToolingAction(Action.CREATE_DROP) | ||
| .showSql(true, true, true) | ||
| .property("hibernate.connection.provider_class", HikariCPConnectionProvider::class.java) | ||
| .property("hibernate.hikari.maximumPoolSize", 5) | ||
| .createEntityManagerFactory() | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,5 @@ ktlint-gradle = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gr | |
|
|
||
| # 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" } | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should it be in the root
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
There was a problem hiding this comment.
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