@Where annotation with @ManyToOne relationship retrieves soft deleted entity

Viewed 1053

For reference: https://hibernate.atlassian.net/browse/HHH-12104

I have a bidirectional OneToMany relationship between Customer and Project entities. Both supports SOFT delete. When I load a Project, I can retrieve the Customer even if it was SOFT deleted. It only happens if the ManyToOne is EAGERly fetched.

I wrote a test case that shows the issue:

Create a Customer and a Project
SOFT Delete only the Customer
Fetch the Project

I except the Project to have no Customer since it was deleted.

You might argue that the test case is wrong because the integrity is broken. This is true logically but not physically (FOREIGN KEY constraint is still valid). I saw that kind of case on a real project. The good implementation is to delete all projects and then delete the customer.

The test case (FAIL):

@RunWith(SpringRunner.class)
@DataJpaTest
public class SoftDeleteTest {

    @Autowired
    private EntityManager entityManager;

    @Autowired
    private ProjectRepository projectRepository;

    @Autowired
    private CustomerRepository customerRepository;

    private void newSession() {
        entityManager.flush();
        entityManager.clear();
    }

    @Test
    public void testIntegrityConstraintManyToOne() {
        Project project = new Project();
        project.setName("Framework");

        Customer customer = new Customer();
        customer.setName("StackOverflow");
        customer.addProject(project);

        customerRepository.save(customer);
        projectRepository.save(project);

        Long customerId = customer.getId();
        Long projectId = project.getId();

        newSession();
        customerRepository.delete(customerId);

        newSession();
        Project orphanProject = projectRepository.findOne(projectId);
        try {
            Customer deletedCustomer = orphanProject.getCustomer();
            deletedCustomer.toString();
            fail("EntityNotFoundException expected");
        } catch (EntityNotFoundException enfe) {
        }

    }

The Customer entity (AbstractEntity has a Boolean deleted field):

@Entity
@Table(name = "CUSTOMER")
@SQLDelete(sql = "UPDATE customer SET deleted = TRUE WHERE id = ?")
@Where(clause = "deleted = false")
public class Customer extends AbstractEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "customer")
    private Set<Project> projects = new HashSet<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Project> getProjects() {
        return projects;
    }

    public void setProjects(Set<Project> projects) {
        this.projects = projects;
    }

    public void addProject(Project project) {
        projects.add(project);
        project.setCustomer(this);
    }

    public void removeProject(Project project) {
        projects.remove(project);
        project.setCustomer(null);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Customer customer = (Customer) o;
        return Objects.equals(id, customer.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

The Project entity:

@Entity
@Table(name = "PROJECT")
@SQLDelete(sql = "UPDATE project SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class Project extends AbstractEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    private Customer customer;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(Customer customer) {
        this.customer = customer;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Project)) return false;
        Project project = (Project) o;
        return Objects.equals(id, project.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
1 Answers

It might be a bug, so feel free to add a Jira issue with a replicating test case.

For a work around, check out this article:

@Entity(name = "PostDetails")
@Table(name = "post_details")
@SQLDelete(sql = 
    "UPDATE post_details " +
    "SET deleted = true " +
    "WHERE id = ?")
@Loader(namedQuery = "findPostDetailsById")
@NamedQuery(name = "findPostDetailsById", query = 
    "SELECT pd " +
    "FROM PostDetails pd " +
    "WHERE " +
    "   pd.id = ?1 AND " +
    "   pd.deleted = false")
@Where(clause = "deleted = false")
public class PostDetails 
    extends BaseEntity {

    ...
}

So, use a dedicated @Loaded which includes the deleted column as well.

Related