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.
CodePudding user response:
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