Managing Detached Entities with Spring Boot and Hibernate
Dealing with detached objects is a common issue when working with Spring Boot and Hibernate. When you load an entity from the database, make some changes, and then save it, sometimes the updated entity becomes detached from the Hibernate Session, which can lead to issues. This post shows how to effectively manage detached entities and avoid data inconsistencies.
The problem with detached entities lies in the fact that Hibernate cannot always automatically track changes. You should be clear about the lifecycle state of your entities, i.e., new (transient
), managed (persistent
), detached
, and removed
.
Table of Contents
- An Expensive Quick Fix – Use merge()
- A Safe Bet – Save or Update
- Keep Transactions Short-Lived
- Demo Time!
- Keeping Eye on Transactions
- Persisting Changes
- Ensuring Data Consistency
- Summary
An Expensive Quick Fix – Use merge()
If you know that an entity might be detached and you want to save the modifications, you can use merge()
method. This method copies the state of the given object onto the persistent object with the same identifier and returns the persistent object.
@Entity
class SomeEntity {...}
fun() updateEntity(val entity: SomeEntity) {
entity = entityManager.merge(entity);
}
Bear in mind that the merge
operation might be less performant due to its internal workings. Use it wisely.
A Safe Bet – Save or Update
The saveOrUpdate()
method is useful when the object is detached, and you are not sure whether the object exists in the database. This method will execute either save()
or update()
depending on the situation.
Keep Transactions Short-Lived
Arguably the best practice in managing Hibernate sessions is keeping the scope of transactions as short as possible. This means opening a session when you need to work with the database and closing it as soon as you’re done.
Remember to configure transaction boundaries to define where a new session starts and an old one ends. This is typically done by using Spring’s @Transactional
annotation.
Demo Time!
I’ve prepared a concise example that showcases some of the most typical situations where you deal with detached entities and how to make Hibernate aware of changes made to these objects.
As usual, the code is available on GitHub.
Let’s define a simple Person
entity.
import javax.persistence.*
@Entity
@Table(name = "people")
class Person(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
var name: String,
var age: Int
)
We’ll use Spring Data JPA to create a repository for our Person
entity.
import org.springframework.data.jpa.repository.JpaRepository
interface PersonRepository : JpaRepository<Person, Long>
Now, let’s create a service that will manage transactions and the persistence context.
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class PersonService(
private val personRepository: PersonRepository
) {
@Transactional(readOnly = true)
fun getPerson(personId: Long): Person {...}
@Transactional
fun updatePersonName(personId: Long, newName: String) {...}
@Transactional
fun updateDetachedPerson(detachedPerson: Person): Person {...}
}
Finally, there’s a web layer where we expose API endpoints to manage people in the database. In our case, this is a simple Spring MVC REST controller.
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/people")
class PersonController(
private val personService: PersonService
) {...}
Keeping Eye on Transactions
Please note that our service relies heavily on the @Transactional
annotation. This ensures that methods are executed within a transaction. It also places the Person
entity into the persistence context so all changes made to it within the transaction boundary are automatically persisted when the transaction finishes.
Even when loading data via Hibernate without making any modifications, it is generally a good idea to still mark the service method as @Transactional
. This annotation in Spring or any JPA-based application is not only about managing transactions for write operations but also for ensuring consistent read operations and proper session management.
By annotating a service method as @Transactional
, you inform the Spring framework to bind a session to the scope of the method. This is particularly useful for lazy loading of entities or collections. Without an active session provided by a transaction, lazy loading could result in a LazyInitializationException
because the session may be closed when the lazy-loaded entities are accessed.
Furthermore, marking a method as @Transactional
when reading data can improve performance by making use of a read-only transaction: @Transactional(readOnly = true)
. Specifying a transaction as read-only can allow for certain shortcuts, like bypassing dirty checks or enabling other read optimizations at the database level.
Persisting Changes
Making changes to an existing entity in a safe transactional manner unfolds the full potential of using an ORM framework. Under such circumstances Hibernate automatically persists changes behind the scenes.
@Transactional
fun updatePerson(personId: Long, name: String) {
val person = personRepository.findById(personId)
.orElseThrow { RuntimeException("Person not found!") }
person.name = name
// The person instance is automatically managed by Hibernate,
// thanks to the @Transactional annotation,
// meaning there's no need to explicitly save it.
}
Quite an opposite situation occurs when we’re making changes to a detached entity. In our example, there’s an endpoint that modifies multiple fields and constructs a Person
object from scratch.
@PostMapping("/{personId}")
fun updatePerson(
@PathVariable personId: Long,
@RequestBody request: CreateOrUpdatePersonRequest
): ResponseEntity<Person> {
// The `person` object here is detached since
// it's not managed by Hibernate
val detachedPerson = Person(
id = personId,
name = request.name,
age = request.age
)
val managedPerson = personService.attachPerson(detachedPerson)
return ResponseEntity.ok(managedPerson)
}
In our service method we make a call to save
or saveAndFlush
to ensure that Hibernate attaches the entity to the session and persists it.
@Transactional
fun attachPerson(detachedPerson: Person): Person {
return personRepository.saveAndFlush(detachedPerson)
}
This would work for both detached and persistent entities, where Hibernate would perform an update if the entity already has an ID, or an insert operation if it does not.
Ensuring Data Consistency
Using save
or saveAndFlush
will insert or update the entity based on whether the ID is null or not (assuming the ID is generated by the databased).
Caveats:
save
: Does not necessarily flush the changes to the database immediately, depending on the flush mode configured. This might not be suitable when you need immediate consistency with the database state.saveAndFlush
: Forces the changes to be flushed to the database immediately.
Remember that save
is part of the Spring Data JPA CrudRepository
interface, while saveAndFlush
is part of the JpaRepository
interface, which is a Spring Data JPA specific extension of the standard JPA repository interfaces.
The choice between save
and merge
often depends on whether you want to explicitly deal with merging state into the current persistence context. In Spring Data JPA, save
is typically sufficient for most CRUD operations, taking care of both persist and merge operations as necessary.
When dealing with complex relationships it might happen that save
won’t be enough to fully re-attach an entity into the persistence context. In this case, you can explicitly use merge
which takes a detached instance and returns a managed instance with the same ID.
Summary
Today, we briefly explored a few effective strategies for managing detached entities within Spring Boot and Hibernate. The key takeaway is to keep your Hibernate sessions short and avoid delaying persistence operations. In more complex scenarios, however, we may need to explicitly merge detached entities back into the session.