Search code examples
kotlinspring-data-jpaspring-repositories

Spring Data: How to automatically remove a child relation when it is removed from the parent


I have a User entity that holds a Character entity in a @OneToOne relation. However I wantt he Character record to be removed as soon as it gets detached from the User entity.

Here is my User.kt entity class:

// User.kt

@Entity
class User(
    @Id
    var id: String,
    var email: String,
    @OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true)
    var character: Character?,
    var isAdmin: Boolean
) { // ... }

This is the unit test I wrote to test this behaviour:

// UserRepositoryTest.kt

@Test
fun `should remove orphan character entity when being removed from user entity`() {
    val user = UserTestTemplate.testUser()
    val character = CharacterTestTemplate.testCharacter()

    user.character = character
    userRepository.save(user)

    user.character = null
    userRepository.save(user)

    val actual = userRepository.findById(user.id).orElse(null)

    assertThat(actual).isNotNull()
    assertThat(actual.character).isNull()

    val savedCharacter = characterRepository.findById(character.id)
    assertThat(savedCharacter.get()).isNull() // fails
}

I added the CascadeType.ALL and orphanRemoval = true option since those are the only things I read about being related to my request.

What I do in the unit test is creating a user and character instance. Then adding the character instance to the user and saving the user via the UserRepository. Thanks to CascadeType.ALL the character instance will be saved automatically. Now I'd like to have the same thing in reverse when removing the character from the user. This however does not work as expected as you can see in the last line of the unit test


Solution

  • Two things to be aware of:

    • transactional write behind pattern
    • first level cache
    @Test
    fun `should remove orphan character entity  entity`() {
        val user = UserTestTemplate.testUser()
        val character = CharacterTestTemplate.testCharacter()
    
        user.character = character
        userRepository.save(user)
    
        user.character = null
    
        //use saveAndFlush here to force immediate DB update
        //otherwise may be deferred until transactional method returns
        userRepository.saveAndFlush(user)
    
        //clear the persistence context to ensure you will be reading from 
        //the database rather than first level cache
        //entityManager is injected to test via @PersistenceContext annotation
        entityManager.clear();
    
        //now you are guaranteed a db read reflecting all flushed updates
        val actual = userRepository.findById(user.id).orElse(null)
    
        assertThat(actual).isNotNull()
        assertThat(actual.character).isNull()
    
        val savedCharacter = characterRepository.findById(character.id)
        assertThat(savedCharacter.get()).isNull() // fails
    }