detached entity passed to persist - on findByMethod
Asked Answered
G

1

6

I understand the error message and i know how to solve it, but i want to know why it occurs in this specific place especially on a find method. I created a mini example for this.

I have three entitys:

@Entity
data class Animal(
    var name: String,
    @ManyToOne(cascade = [CascadeType.ALL]) val zoo: Zoo) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int = -1
}

@Entity
data class Zoo(
    var city: String,
    @OneToMany(cascade = [CascadeType.ALL]) val employee: MutableList<Person>) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int = -1
}

@Entity
data class Person(var age: Int) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int = -1
}

Service:

   @Transactional
    fun save(name:String, city: String){
        repo.findByZooCity(city).ifPresent {
            it.zoo.employee.add(Person(22))
        }
        repo.findByZooCity("OTHER STRING!!!!").ifPresent { println("FOUND!") }
        repo.save(Animal(name, Zoo(city, mutableListOf(Person(33)))))
    }

Repo:

interface AnimalRep: JpaRepository<Animal, Int>{

    fun findByZooCity(name: String): Optional<Animal>
}

Call:

  animalService.save("Zoo1", "Animal1")
  animalService.save("Zoo1", "Animal1")

Exception: On the second call i get a "detached entity passed to persist: com.example.Person" on repo.findByZooCity("OTHER STRING!!!!"). I know this happens because i add a "detached" person before. But WHY it occurs on a findBy? (Even its not in results?)

Is there some dirty check?

Thank you for your time and help.

Gulgee answered 11/5, 2022 at 17:16 Comment(0)
O
6

This behavior depends on FlushMode of EntityManager.
The FlushMode defines when new entities and your changes on existing ones get written to the database, in other words, defines when flush() operation is performing.

The flush process synchronizes database state with session state by detecting state changes and executing SQL statements.

The JPA specification defines the FlushModeType.AUTO as the default flush mode. It flushes the persistence context in next situations:

  • before the transaction gets committed
  • before executing a query that uses any database table for which your persistence context contains any pending changes.

According to documentation:

AUTO
The Session is sometimes flushed before query execution in order to ensure that queries never return stale state. This is the default flush mode.

Summary:
So in your example, we have default FlushMode.AUTO, during the query execution framework performs flush() operation, this is the reason.


FlushMode.AUTO is fixing consistency issues on a query basis, but on other hand can lead to unpredictable flush operations and as result performance problems in the case of huge services. I recommend to change FlushMode to COMMIT, when you do not need to resolve consistency issues. The FlushModeType.COMMIT requires a flush before committing the transaction but doesn’t define what needs to happen before executing a query. Executing any query doesn’t flush any pending changes.

Changing FlushMode:
1. Globally for server add to properties

spring.jpa.properties.org.hibernate.flushMode=COMMIT

2. Set for specific query

Query quey = entityManager.createQuery("SELECT * from Foo");
quey.setFlushMode(FlushModeType.COMMIT);

3. Set for Session
Inject EntityManager into your service.

   @Transactional
    fun save(name:String, city: String){
        entityManager.setFlushMode(FlushModeType.COMMIT)
        ...
    }

Detailed described at How does AUTO flush strategy work in JPA and Hibernate

Orris answered 15/5, 2022 at 19:46 Comment(1)
Thank you, that was the part i missed :)Gulgee

© 2022 - 2024 — McMap. All rights reserved.