Skip to content

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

@devxzero

Description

@devxzero

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    for: external-projectFor an external project and not something we can fix

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions