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);
}
}