Persistence magic with SB and Hibernate
|

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()

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.

Similar Posts