I'm completely stumped about the proper way to cascade delete entities in Spring. Here's some entity classes in my project:
@Entity
@Getter
@Setter
public class User {
@OneToMany(cascade = CascadeType.ALL)
private Set<Ticket> tickets;
}
@Entity
@Getter
@Setter
public class Ticket {
@ManyToOne
@JoinColumn
private User user;
@ManyToOne
@JoinColumn
private Screening screening;
}
@Entity
@Getter
@Setter
public class Screening {
@OneToMany(cascade = CascadeType.ALL)
private Set<Ticket> tickets;
}
I'm needing to be able to delete a User and it cascade delete all of the tickets associated with it, but when I do, I get an exception that says "Cannot add or update a child row: a foreign key constraint fails".
I've tried adding a @PreRemove method to the ticket entity:
@Entity
@Getter
@Setter
public class Ticket {
@ManyToOne
@JoinColumn
private User user;
@ManyToOne
@JoinColumn
private Screening screening;
@PreRemove
public void preRemove() {
user.getTickets().remove(this);
setUser(null);
screening.getTickets().remove(this);
setScreening(null);
}
}
But when I do that, I get a ConcurrentModificationException. I've also tried adding an onDelete method to the ticket service class:
@Service
@Transactional
public class TicketServiceImpl implements TicketService {
private final UserService userService;
public TicketServiceImpl(UserService userService) {
this.userService = userService;
}
@Override
protected void onDelete(Ticket ticket) {
User user = ticket.getUser();
user.getTickets.remove(ticket);
ticket.setUser(null);
Screening screening = ticket.getScreening();
screening.getTickets().remove(ticket);
ticket.setScreening(null);
}
@Override
public void purchaseTicket(Long userId, TicketForm ticketForm) {
// depends on UserService to fetch User by id
}
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
private final TicketService ticketService;
public UserServiceImpl(TicketService ticketService) {
this.ticketService = ticketService;
}
@Override
protected void onDelete(User user) {
// I'm required to implement the cascade myself in order to
// have the onDelete method called for Tickets
ticketService.deleteAll(user.getTickets());
}
}
There's two problems with the above approach. The first problem is now I'm forced to implement cascading myself rather than letting Hibernate do it if I want the onDelete method to be called. And secondly, now there's a circular dependency between the two services if I want to be able to delete Users and also delete Tickets separately.
I've tried looking around for answers to solving this problem but can't find the answer. I thought for sure the @PreRemove method was the "correct" way to do it but from what I've found apparently the ConcurrentModificationException is a common issue with it and the last approach is what I've found recommended. But of course, the last approach has its own issues. I'm completely stumped, any help is greatly appreciated!
CodePudding user response:
So the goal is to trigger cascade delete of tickets when the user is deleted and to keep two sides of bidirectional mapping in sync. First of all, I suggest getting rid of bidirectional mapping and switching to a unidirectional one. Why?
- You will not have to track both sides of the relationship
- You will not come across an infinite recursion problem when serializing your entities to jsons
As already mentioned, In bidirectional mapping, you have to track both sides of the relationship. The way to do this is to use synchronization methods, like your onDelete
. Usually, these methods are placed directly under one side of the mapping. For instance:
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private Set<Ticket> tickets;
}
@Entity
@Getter
@Setter
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "screening", cascade = CascadeType.ALL)
private Set<Ticket> tickets;
public void removeTicket(Ticket ticket) {
tickets.remove(ticket);
ticket.setScreening(null);
}
}
This way you can simplify your service classes and do something like that:
@Service
public class UserServiceImpl implements UserService {
private final EntityManager entityManager;
public UserServiceImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
@Override
public void onDelete(User user) {
user.getTickets().forEach(ticket -> {
ticket.getScreening().removeTicket(ticket);
});
entityManager.remove(user);
}
}
Note that I added also mappedBy
, which prevents hibernate from creating intersection table. Without this attribute hibernate generates the following SQL for onDelete
invocation:
Hibernate: delete from user_tickets where user_id=?
Hibernate: delete from ticket where id=?
Hibernate: delete from ticket where id=?
Hibernate: delete from user where id=?
As you can see hibernate creates an additional table, because he does not know who is the owner of the relationship. With mappedBy
instead:
Hibernate: delete from ticket where id=?
Hibernate: delete from ticket where id=?
Hibernate: delete from user where id=?
For brevity I didn't add addTicket
methods in both enities and removeTicket
in user entity.