Search code examples
javaspringspring-boothibernatejpa

Hibernate does not throw EntityNotFoundException after deleting Entity


In our Spring application, we use a set of generic repository tests to validate our most basic use cases for using Spring repositories. One of those tests validates that an EntityNotFoundException is thrown if you try to get a previously deleted entity.

Before our migration to Spring Boot 3 and Hibernate 6, these tests worked correctly, but after the update, the expected exception was not thrown anymore.

You can reproduce this behavior with the following setup:

  1. Entity.java (A simple Entity)
package de.test.hibernate_delete.repository;
import jakarta.persistence.*;
@jakarta.persistence.Entity
@Table(name = "entity")
public class Entity {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) Long id;
    @Version long version;
    @Column(name = "data") String data;
    public Entity(String data) {this.data = data;}
    public Entity() {}
    public Long getId() {return id;}
    public void setId(Long id) {this.id = id;}
    public long getVersion() {return version;}
    public void setVersion(long version) {this.version = version;}
    public String getData() {return data;}
    public void setData(String data) {this.data = data;}
}

  1. EntiryRepository.java (A simple JpaRepository)
package de.test.hibernate_delete.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EntityRepository extends JpaRepository<Entity, Long> {
}

  1. EntityDeleteTest.java (The testcase resulting in the error)
package de.test.hibernate_delete;

import de.test.hibernate_delete.repository.Entity;
import de.test.hibernate_delete.repository.EntityRepository;
import jakarta.persistence.EntityNotFoundException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = HibernateDeleteApplication.class)
public class EntityDeleteTest {
    @Autowired private EntityRepository repository;
    @Test public void testDelete() {
        Entity storedSample = repository.saveAndFlush(new Entity("data"));
        assertNotNull(storedSample.getId());
        repository.delete(storedSample);
        repository.flush();
        assertThrows(EntityNotFoundException.class, () -> assertNull(repository.getOne(storedSample.getId())));
    }
}

The test results in the following fail-message:

org.opentest4j.AssertionFailedError: Unexpected exception type thrown, 
Expected :class jakarta.persistence.EntityNotFoundException
Actual   :class org.opentest4j.AssertionFailedError

I have also pushed my minimal example to a github repo: https://github.com/LorenzSLA/hibernate_delete_minimal_exampel The failed test can be seen in this GitHub Actions Build: https://github.com/LorenzSLA/hibernate_delete_minimal_exampel/actions/runs/6094606983/job/16536481715

Has anyone had similar problems after migrating to Spring Boot 3?

Update:

Thanks to Andreys comment I changed the test case:

    @Test
    public void testDelete() {
        T storedSample = getRepository().saveAndFlush(getSample());
        assertNotNull(storedSample.getId());
        getRepository().delete(storedSample);
        getRepository().flush();

        assertFalse(getRepository().existsById(storedSample.getId()));
    }

Solution

  • That seems not to be related to Hibernate 6:

    repository.getOne(storedSample.getId()) method returns proxy object, actually not backed up by live entity, so any interaction with such proxy object is expected to fail with EntityNotFoundException, but the problem is your test case is actually checking junit assertions module.

    Previous implementation of assertNull method was trying to report assertion failure via:

    String stringRepresentation = actual.toString();
    if (stringRepresentation == null || stringRepresentation.equals("null")) {
        fail(format(null, actual, message), null, actual);
    } else {
        fail(buildPrefix(message) + "expected: <null> but was: <" + actual + ">", null, actual);
    }
    

    where actual.toString() call was throwing EntityNotFoundException, now junit calls #toString() method using more accurate way:

    catch (Throwable throwable) {
      UnrecoverableExceptions.rethrowIfUnrecoverable(throwable);
    
      return defaultToString(obj);
    }