How to manage JPA bidirectional relationships properly

In this troubleshooting guide, we will explore how to properly map bidirectional relationships and common mapping pitfalls that cause exceptions such as DataIntegrityViolationException, a ConstrainViolationException and a PropertyValueException.

We will take an unusual approach in this guide by first building the faulty software, and then we will discuss how and why this should be fixed.

Mapping Bi-Directional Relationships (the wrong way)

As discussed, we will set up our database tables and our entity models in order to reproduce the exceptions, and then we will discuss how the issue can be resolved. (Spoiler alert: you need to make sure your data is consistent in memory)

Database tables

Let us start with our database tables. The first is a Customer table and the second is a Bank Branch. A customer can belong to a single bank branch, but a bank branch can have many customers (ie. one-to-many / many-to-one). Let us start with the bank branch table:

--
-- Table: public.bank_branch

-- DROP TABLE public.bank_branch;

CREATE TABLE public.bank_branch
(
    id bigint NOT NULL,
    branch_code character varying(255) COLLATE pg_catalog."default" NOT NULL,
    zip_code character varying(255) COLLATE pg_catalog."default" NOT NULL,
    name character varying(255) COLLATE pg_catalog."default" NOT NULL,
    CONSTRAINT bank_branch_pkey PRIMARY KEY (id),
    CONSTRAINT bank_branch_branch_code_key UNIQUE (branch_code)

)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

ALTER TABLE public.bank_branch
    OWNER to postgres;

And the customer database table definition:

-- Table: public.customer

-- DROP TABLE public.customer;

CREATE TABLE public.customer
(
    id bigint NOT NULL,
    name character varying COLLATE pg_catalog."default" NOT NULL,
    type character varying COLLATE pg_catalog."default",
    branch_id bigint NOT NULL,
    version bigint NOT NULL DEFAULT 0,
    CONSTRAINT "Customer_pkey" PRIMARY KEY (id),
    CONSTRAINT customer_unique_constraint_1 UNIQUE (name)
,
    CONSTRAINT customer_foreignkey_1 FOREIGN KEY (branch_id)
        REFERENCES public.bank_branch (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

ALTER TABLE public.customer
    OWNER to postgres;

The customer table has the branch_id column, which is a foreign key to the id column of the bank branch table.

Entity Models

Now, let us define the problematic database models. Let us start with the bank branch entity:

package com.nullbeans.persistence.models;

import org.hibernate.annotations.NaturalId;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "bank_branch" )
public class BankBranch {

    private long id;

    private String branchCode;

    private String name;

    private String zipCode;

    private List<Customer> customers = new ArrayList<>();


    public BankBranch() {
    }

    @Id
    @GeneratedValue
    @Column(name = "id", nullable = false)
    public long getId() {
        return id;
    }

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

    @NaturalId
    @Column(name = "branch_code", unique = true, nullable = false)
    public String getBranchCode() {
        return branchCode;
    }

    public void setBranchCode(String branchCode) {
        this.branchCode = branchCode;
    }

    @Column(name = "name", nullable = false)
    public String getName() {
        return name;
    }

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

    @Column(name = "zip_code", nullable = false, length = 50)
    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @OneToMany(mappedBy = "branch", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    public List<Customer> getCustomers() {
        return customers;
    }

    public void setCustomers(List<Customer> customers) {
        this.customers = customers;
    }

    @Override
    public String toString() {
        return "BankBranch{" +
                "id=" + id +
                ", branchCode='" + branchCode + '\'' +
                ", name='" + name + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}

Notice the @OneToMany relationship. The relationship defines three parameters. First is the “mappedBy” field which indicates the field in the Customer class where the BankBranch is mapped. The second setting is the FetchType. Here we indicate that we would like to lazy load the list of customers (recommended for performance reasons). The third parameter is the “cascade” parameter. A cascade of type CascadeType.ALL means that we would like to cascade all event that occur on the parent entity to the children.

Below is the Customer entity class:

package com.nullbeans.persistence.models;

import javax.persistence.*;

@Entity
@Table(name = "customer")
public class Customer {

    private long id;

    private long version;

    private String name;

    private String type;

    private BankBranch branch;

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    public long getId() {
        return id;
    }

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

    @Version
    @Column(name = "version", nullable = false)
    public long getVersion() {
        return version;
    }

    public void setVersion(long version) {
        this.version = version;
    }

    @Column(name = "name", nullable = false)
    public String getName() {
        return name;
    }

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

    @Column(name = "type")
    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    @ManyToOne
    @JoinColumn(name = "branch_id", referencedColumnName = "id", nullable = false)
    public BankBranch getBranch() {
        return branch;
    }

    public void setBranch(BankBranch branch) {
        this.branch = branch;
    }
}

Reproducing the error

Now let us move on to our tests. Our first test will attempt to save a new bank branch and a customer.

	@Test
	public void testSaveBranchAndCustomer(){

		//Create a new branch
		BankBranch bankBranch = new BankBranch();
		bankBranch.setZipCode("222");
		bankBranch.setName("TESTBRANCH");
		bankBranch.setBranchCode("ZZZ123");

		//Create a new customer
		Customer customer = new Customer();
		customer.setName("TestCustomer");
		customer.setType("TEST");

		//Add the customer to the branch's customer list
		bankBranch.getCustomers().add(customer);

		//save everything
		bankBranchRepository.save(bankBranch);
		entityManager.flush();
	}

When we save everything, we assume that the customer will be added through cascading the save event from the branch and everything will be nice. But that is not true. We get the following nice exception (scroll down to see the full stack trace):

org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : com.nullbeans.persistence.models.Customer.branch; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : com.nullbeans.persistence.models.Customer.branch

	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:314)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:253)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
	at com.sun.proxy.$Proxy83.save(Unknown Source)
	at com.nullbeans.NullbeansPersistenceApplicationTests.testCreateCustomer(NullbeansPersistenceApplicationTests.java:52)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.hibernate.PropertyValueException: not-null property references a null or transient value : com.nullbeans.persistence.models.Customer.branch
	at org.hibernate.engine.internal.Nullability.checkNullability(Nullability.java:108)
	at org.hibernate.engine.internal.Nullability.checkNullability(Nullability.java:56)
	at org.hibernate.action.internal.AbstractEntityInsertAction.nullifyTransientReferencesIfNotAlready(AbstractEntityInsertAction.java:115)
	at org.hibernate.action.internal.AbstractEntityInsertAction.makeEntityManaged(AbstractEntityInsertAction.java:124)
	at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:289)
	at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:263)
	at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:250)
	at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:367)
	at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:292)
	at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:200)
	at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:143)
	at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:192)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:135)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:824)
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:791)
	at org.hibernate.engine.spi.CascadingActions$7.cascade(CascadingActions.java:298)
	at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:471)
	at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:396)
	at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:197)
	at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:504)
	at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:436)
	at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:399)
	at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:197)
	at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:130)
	at org.hibernate.event.internal.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:486)
	at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:298)
	at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:200)
	at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:143)
	at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:192)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:135)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:62)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:800)
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:785)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:308)
	at com.sun.proxy.$Proxy79.persist(Unknown Source)
	at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:489)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:359)
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:200)
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:644)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:608)
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.lambda$invoke$3(RepositoryFactorySupport.java:595)
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:595)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
	... 40 more

Hibernate complains that the branch field is null or transient. This is because we did not set the branch field in the customer entity. Since we defined the CascadeType.ALL property in the BankBranch entity, the cascade save event was propagated to the customer entity. But the customer instance had a null branch field. Therefore we get the error. Note that hibernate will not set both sides of the relationships automatically. You have to do it your self.

Let us move on to another test, which reproduces another data integrity violation exception. The test attempts to delete a bank branch, but first, it tries to move the customer of the branch to another branch before we make a deletion.

	@Test
	public void testDeleteBranchAndKeepCustomer(){

		//get branch to be deleted
		BankBranch branchToDelete = bankBranchRepository.findByBranchCode("SYS001");
		//get branch to replace the deleted one
		BankBranch branchToReplaceOldOne = bankBranchRepository.findByBranchCode("VEGAS9936");
		//Add customers to new branch
		branchToReplaceOldOne.getCustomers().addAll(branchToDelete.getCustomers());
		//Clear the list of customers from the old branch
		branchToDelete.getCustomers().clear();
		//delete the toBeDeleted branch
		bankBranchRepository.delete(branchToDelete);
		entityManager.flush();



	}

And here is the output that we get:

javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement

	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1460)
	at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1440)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:308)
	at com.sun.proxy.$Proxy79.flush(Unknown Source)
	at com.nullbeans.NullbeansPersistenceApplicationTests.testDeleteBranchAndKeepCustomer(NullbeansPersistenceApplicationTests.java:71)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
	at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:112)
	at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:99)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:178)
	at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:45)
	at org.hibernate.persister.entity.AbstractEntityPersister.delete(AbstractEntityPersister.java:3478)
	at org.hibernate.persister.entity.AbstractEntityPersister.delete(AbstractEntityPersister.java:3735)
	at org.hibernate.action.internal.EntityDeleteAction.execute(EntityDeleteAction.java:99)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:478)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:356)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1454)
	... 38 more
Caused by: org.postgresql.util.PSQLException: ERROR: update or delete on table "bank_branch" violates foreign key constraint "customer_foreignkey_1" on table "customer"
  Detail: Key (id)=(1) is still referenced from table "customer".
	at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2440)
	at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2183)
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:308)
	at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:441)
	at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:365)
	at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:143)
	at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:120)
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:175)
	... 47 more

The error occurred because the deleted bank branch is still referenced by the customers, even though we did move them to another bank branch. So what is the solution to the above issues?

Bidirectional relationships require bidirectional setters

The problem with bidirectional JPA relationships is that the data on both sides of the relationships need to be consistent. In our example, this means that if you are moving the customers of the branch to another branch, then you need to also  change the bank branch of each customer to the new bank branch. This can be done via bidirectional setters. Let us revisit the BankBranch entity class and add some convenience methods:

  
  @OneToMany(mappedBy = "branch", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    public List<Customer> getCustomers() {
        return customers;
    }

    public void setCustomers(List<Customer> customers) {
        this.customers = customers;
    }

    public void addCustomer(Customer customer){
        if(customer!=null){
            customer.setBranch(this);
            this.customers.add(customer);
        }
    }

    public void removeCustomer(Customer customer){
        if(customer!=null){
            customer.setBranch(null);
        }
        this.customers.remove(customer);
    }

Notice that the new add and remove customer methods. These methods will set the branch field in the incoming customer entity and therefore maintain data consistency. Let us revisit our tests:

	@Test
	public void testCreateBranchCustomer(){

		//Create a new branch
		BankBranch bankBranch = new BankBranch();
		bankBranch.setZipCode("222");
		bankBranch.setName("TESTBRANCH");
		bankBranch.setBranchCode("ZZZ123");

		//Create a new customer
		Customer customer = new Customer();
		customer.setName("TestCustomer");
		customer.setType("TEST");

		//Add the customer to the branch's customer list
		bankBranch.addCustomer(customer);

		//save everything
		bankBranchRepository.save(bankBranch);
		entityManager.flush();
	}

	@Test
	public void testDeleteBranchAndKeepCustomer(){

		//get branch to be deleted
		BankBranch branchToDelete = bankBranchRepository.findByBranchCode("SYS001");
		//get branch to replace the deleted one
		BankBranch branchToReplaceOldOne = bankBranchRepository.findByBranchCode("VEGAS9936");
		//Add customers to new branch
		for(Customer customer : branchToDelete.getCustomers()){
			//bidirectional setter will move the customers from the old branch to the new one
			branchToReplaceOldOne.addCustomer(customer);
		}

		//delete any reference to the customers from the old branch
		branchToDelete.getCustomers().clear();

		//delete the toBeDeleted branch
		bankBranchRepository.delete(branchToDelete);
		entityManager.flush();



	}

Notice our tests now use the add customer method to both add the customer to the customers list of the bank branch and to assign the bank branch entity of the customers. Our tests now run smoothly as both sides of the relationship have consistent data.

Summary

In this example we explored the importance of maintaining data integrity in a JPA bidirectional relationship, and which errors we could encounter when our data is not consistent in memory. The next time you encounter an ConstraintViolationException, a DataIntegrityViolationException or a PropertyValueException, then make sure that your data is consistent and that you are setting both sides of your bidirectional relationship.

How to manage JPA bidirectional relationships properly
Scroll to top