I have a problem with constraint fail when I cascade-delete entity which references another entity.
I’m using Hibernate as my JPA Provider.
Domain model:
Every Zone can have more Actions, but Actions are not shared between Zones (OneToMany).
TeleportAction (one possible kind of Action) contains a set of Zones as targets. Every TeleportAction can have more Zones as targets and Zones and they can be used by multiple TeleportActions (ManyToMany).
How deletion works
I call ZoneManager.deleteZone(zone)
ZoneManager first loops through all Zones and their TeleportActions and removes deleted Zone from their targets.
After that ZoneManager removes given Zone from it’s list of Zones
Then I delete Zone from DB with Hibernate via session.delete(zone). This operation propagates to Actions.
Error which I get
Hibernate: /* delete cz.iwitrag.greencore.gameplay.zones.Zone */ delete from gcore_Zone where id=?
[org.hibernate.engine.jdbc.spi.SqlExceptionHelper] SQL Error: 1451, SQLState: 23000
[org.hibernate.engine.jdbc.spi.SqlExceptionHelper] Cannot delete or update a parent row: a foreign key constraint fails (`309145_mysql_db`.`gcore_tpaction_targetzones`, CONSTRAINT `FK4rvpdg8xjyt6lk5281jbi78c5` FOREIGN KEY (`targetZone_id`) REFERENCES `gcore_Zone` (`id`))
[org.hibernate.engine.jdbc.batch.internal.AbstractBatchImpl] HHH000010: On release of batch it still contained JDBC statements
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
...
This error is caused because when I delete Zone, it is still target of some TeleportActions.
Even though I deleted this Zone from TeleportAction's targets in business code before calling session.delete(zone), Hibernate doesn’t acknowledge it and doesn’t remove this Zone from ManyToMany association.
Entities
@Entity
public class Zone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true)
@Cascade(CascadeType.ALL)
@OrderColumn(name = "list_id")
@JoinColumn(name = "zone_id")
private List<Action> actions = new ArrayList<>();
...
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "actionType")
public abstract class Action {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
...
}
@Entity
@DiscriminatorValue("tp")
public class TeleportAction extends Action {
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "tpaction_targetzones", joinColumns = @JoinColumn(name="tpAction_id"), inverseJoinColumns = @JoinColumn(name="targetZone_id"))
private Set<Zone> targets = new HashSet<>();
...
}
Question
How can I force Hibernate to automatically delete removed Zone from table tpaction_targetzones before it actually deletes Zone from database to avoid foreign key constraint error?
package org;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.OrderColumn;
import org.junit.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class DeleteTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {
Zone.class,
Action.class,
TeleportAction.class
};
}
@Test
public void testIt() {
inTransaction(
session -> {
Zone zone = new Zone();
TeleportAction teleportAction = new TeleportAction();
zone.addAction( teleportAction );
Zone zone2 = new Zone();
zone2.addAction( teleportAction );
session.persist( zone );
session.persist( zone2 );
session.persist( teleportAction );
}
);
inTransaction(
session -> {
List<Zone> zones = session.createQuery( "select z from Zone z", Zone.class ).getResultList();
assertThat(zones.size()).isEqualTo( 2 );
Zone toDelete = zones.get( 0 );
TeleportAction teleportAction = session.createQuery(
"select t from TeleportAction t",
TeleportAction.class
).getSingleResult();
assertThat( teleportAction.getTargets().size() ).isEqualTo( 2 );
teleportAction.getTargets().remove( toDelete );
session.remove( toDelete );
}
);
inTransaction(
session -> {
List<Zone> zone = session.createQuery( "select z from Zone z", Zone.class ).getResultList();
assertThat( zone.size() ).isEqualTo( 1 );
TeleportAction teleportAction = session.createQuery(
"select t from TeleportAction t",
TeleportAction.class
).getSingleResult();
assertThat( teleportAction.getTargets().size() ).isEqualTo( 1 );
}
);
}
@Entity(name = "Zone")
public static class Zone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true)
@Cascade(CascadeType.ALL)
@OrderColumn(name = "list_id")
@JoinColumn(name = "zone_id")
private List<Action> actions = new ArrayList<>();
public Long getId() {
return id;
}
public List<Action> getActions() {
return actions;
}
public void addAction(TeleportAction action) {
actions.add( action );
action.addZone( this );
}
}
@Entity(name = "Action")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "actionType")
public static abstract class Action {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
public Long getId() {
return id;
}
}
@Entity(name = "TeleportAction")
@DiscriminatorValue("tp")
public static class TeleportAction extends Action {
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "tpaction_targetzones", joinColumns = @JoinColumn(name = "tpAction_id"), inverseJoinColumns = @JoinColumn(name = "targetZone_id"))
private Set<Zone> targets = new HashSet<>();
public Set<Zone> getTargets() {
return targets;
}
public void addZone(Zone zone) {
targets.add( zone );
}
}
}
and it works, naturally because of the orphanRemoval = true on List<Action> actions = new ArrayList<>() if you delete also the other Zone also TeleportAction will be deleted.