Foreign key fails on deletion because Hibernate doesn't remove association first

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:

ERD

  • 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

  1. I call ZoneManager.deleteZone(zone)
    1. ZoneManager first loops through all Zones and their TeleportActions and removes deleted Zone from their targets.
    2. After that ZoneManager removes given Zone from it’s list of Zones
  2. 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?

I am having this same problem. session.delete() should take care of foreign key constraints automatically.

Hi @Iwitrag ,

I tried this

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.