Skip to content
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

Spring Boot 3.4.0 breaks Kotlin JPA entity: detached entity #3696

Closed
devxzero opened this issue Nov 29, 2024 · 2 comments
Closed

Spring Boot 3.4.0 breaks Kotlin JPA entity: detached entity #3696

devxzero opened this issue Nov 29, 2024 · 2 comments
Labels
for: external-project For an external project and not something we can fix

Comments

@devxzero
Copy link

devxzero commented Nov 29, 2024

When I upgraded my project from Spring Boot 3.3.6 to 3.4.0, it somehow causes Kotlin JPA entity objects to become detached, causing the exception jakarta.persistence.EntityExistsException: detached entity passed to persist.

I tried to narrow the problem down and found that it happens with the following combination of components:

  • Spring Boot 3.4.0 (which uses hibernate-core:6.6.2.Final) (but works fine with <= 3.3.6, which uses hibernate-core:6.5.3.Final)
  • @jakarta.persistence.Version annotation. When this is removed from a field from the Entity, the problem disappears.
  • Kotlin entities. It works fine when using Java entites with about the same code, but translated to Java.

I tried it with Spring Boot 3.4.0 using a downgraded hibernate-core from 6.6.2.Final to 6.5.3.Final, which causes the problem to dissapear. So it is related to Hibernate. But because Kotlin is also part of the problem and because it doesn't happen with plain Java, I thought the place to report this is in this Spring Data JPA project.

This is the failing Kotlin code when used with Spring Boot 3.4.0:

@Entity
class ProductK(
    var name: String,

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0
)

@Entity
class ProductTagK(
    @ManyToOne
    var product: ProductK,
    var tag: String,

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0
) {
    // Removing this annotation removes the problem
    @Version
    private var updated: LocalDateTime = LocalDateTime.now()
}

/**
 * Kotlin client code with Kotlin Entity.
 * Fails with: Exception in thread "main" jakarta.persistence.EntityExistsException: detached entity passed to persist: demo.ProductTagK
 */
@Component
class DetachedEntityProblemK(
    em: EntityManager,
    transactionManager: PlatformTransactionManager
) : AbstractDetachedEntityProblemK(em, transactionManager) {

    fun run() {
        val query = "select p from ProductK p where p.name = 'Car'"

        runInCommitTx { em ->
            val p = em.createQuery(query, ProductK::class.java).resultList.firstOrNull()
            if (p == null) {
                println("Creating new product")
                em.persist(ProductK("Car"))
            } else {
                println("Existing product found")
            }
        }

        runInRollbackTx { em ->
            val p = em.createQuery(query, ProductK::class.java)
                .resultList.first()

            val t = ProductTagK(p, "vehicle")

            // This line fails
            em.persist(t)
        }
    }
}

fun main() {
    val context = runApplication<DemoApplication>()
    context.getBean(DetachedEntityProblemK::class.java).run()
    context.close()
}

It fails with:

Exception in thread "main" jakarta.persistence.EntityExistsException: detached entity passed to persist: demo.ProductTagK
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:126)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:173)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:767)
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:745)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:320)
	at jdk.proxy2/jdk.proxy2.$Proxy92.persist(Unknown Source)
	at demo.DetachedEntityProblemJK.lambda$run$1(DetachedEntityProblemJK.java:37)
	at demo.AbstractDetachedEntityProblemJ.runInRollbackTx(AbstractDetachedEntityProblemJ.java:26)
	at demo.DetachedEntityProblemJK.run(DetachedEntityProblemJK.java:31)
	at demo.DetachedEntityProblemJK.main(DetachedEntityProblemJK.java:44)
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: demo.ProductTagK
	at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:90)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:79)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:761)
	... 9 more

But about equivalent Java entities work fine:

@Entity
public class ProductJ {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    public ProductJ() {}
    public ProductJ(String name) {
        this.name = name;
    }
}


@Entity
public class ProductTagJ {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    private ProductJ product;
    private String tag;
    @Version
    private LocalDateTime updated = LocalDateTime.now();

    protected ProductTagJ() {}
    public ProductTagJ(ProductJ product, String tag) {
        this.product = product;
        this.tag = tag;
    }
}

build.gradle.kts:

plugins {
    java
    kotlin("jvm") version "1.9.25"
    kotlin("plugin.spring") version "1.9.25"

    // Change this to 3.3.6 to remove the problem
    id("org.springframework.boot") version "3.4.0"
//    id("org.springframework.boot") version "3.3.6"

    id("io.spring.dependency-management") version "1.1.6"
    kotlin("plugin.jpa") version "1.9.25"
}

group = "demo"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // Downgrading hibernate-core also removes the problem
//    constraints {
//        implementation("org.hibernate.orm:hibernate-core") {
//            version {
//                strictly("6.5.3.Final")
//            }
//        }
//    }
//    implementation("org.hibernate.orm:hibernate-core:6.5.3.Final")

    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    runtimeOnly("com.h2database:h2")
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")
    }
}

allOpen {
    annotation("jakarta.persistence.Entity")
}

Attached is the complete project code. It contains the following example runs:

  • DetachedEntityProblemJ: uses Java client code with Java entities. Runs fine.
  • DetachedEntityProblemJK: uses Java client code with Kotlin entities. Fails.
  • DetachedEntityProblemK: uses Kotlin client code with Kotlin entities. Fails.
  • DetachedEntityProblemKJ: uses Kotlin client code with Java entities. Runs fine.

demo.zip

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Nov 29, 2024
@mp911de
Copy link
Member

mp911de commented Dec 2, 2024

This is a hibernate issue. Hibernate now requires to call merge if you provide a value for a generated Id or version. Please reach out to the Hibernate team if you have further questions or concerns.

@mp911de mp911de closed this as not planned Won't fix, can't repro, duplicate, stale Dec 2, 2024
@mp911de mp911de added for: external-project For an external project and not something we can fix and removed status: waiting-for-triage An issue we've not yet triaged labels Dec 2, 2024
@Fossan
Copy link

Fossan commented Jan 16, 2025

@devxzero have you reported this to Hibernate team by any chance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: external-project For an external project and not something we can fix
Projects
None yet
Development

No branches or pull requests

4 participants