Search code examples
javahibernateormmigrationentity

After Hibernate migration from 5 to 5.5 adding entities to collections causes ConstraintViolationException


We have code like this (i simplified the code to make it more clear):

@Entity
@Table(name="storages")
class Storage {
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "storage_items", joinColumns = @JoinColumn(name = "storage_id"), inverseJoinColumns = @JoinColumn(name = "item_id"))
    private Set<Item> items;

    void putItemToStorage(Session session) {
        Item item = new Item();
        item.setStorage(this);
        session.save(item);
        items.add(item);
    }

}

@Entity
@Table(name="items")
class Item {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "storage_id")
    private Storage storage;

    public void setStorage(Storage storage) {
        this.storage = storage;
    }
}

We call 'putItemToStorage' in a transaction, but in hibernate 5.5 it causes the following error, while in hibernate 5 same code worked like a charm:

> javax.persistence.PersistenceException:
> org.hibernate.exception.ConstraintViolationException: could not execute statement
> ...
> caused by org.postgresql.util.PSQLException: ERROR: insert or update on table
> "storage_items" violates foreign key constraint
> "fkla3c4upmtw4myssb3bfg2svkj"   Detail: Key (storage_id)=(164) is not
> present in table "items".

So, hibernate 5 resolved both inserts into items table and storage_items table and worked as intended (adding both item to items table and linking the item to corresponding storage through joining table storage_items), but in hibernate 5.5 it no longer works. I spent quite time in google and documentation and can't find what was changed or what am I doing wrong.

I had similar error in other place, where I temporarily resolved it separating saving of an object and inserting the object into 2 separate transactions (it works, but it's definitely not a right solution), so, any help how to fix it correctly would be very much appreciated.


Solution

  • You should define mapping only on one side (the one that holds the relation), eg. like this:

    @Entity
    @Table(name="storages")
    class Storage {
    
        @OneToMany(mappedBy = "storage", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
        private Set<Item> items;
    
        void putItemToStorage() {
            Item item = new Item();
            item.setStorage(this);
            items.add(item);
        }
    
    }
    
    @Entity
    @Table(name="items")
    class Item {
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "storage_id", referencedColumnName = "id")
        private Storage storage;
    
        public void setStorage(Storage storage) {
            this.storage = storage;
        }
    }
    

    Notice that this approach will most likely result in 2 queries:

    1. That will create Item record in DB with storage_id = null
    2. That will update recods and set storage_id to value that it should be

    To prevent it adjust annotations on field storage:

    @Entity
    @Table(name="items")
    class Item {
    
        @ManyToOne(optional = false)
        @JoinColumn(name = "storage_id", referencedColumnName = "id", nullable = false, updatable = false)
        private Storage storage;
    
    }
    

    You might also consider adding orphanRemoval = true to items in case you will want to delete record in DB.

    @Entity
    @Table(name="storages")
    class Storage {
    
        @OneToMany(mappedBy = "storage", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
        private Set<Item> items;
    
    }
    

    Your code then should looks like this:

    // assumming within transaction context
    var storage = // get from eg. EntityManager or JPA repository (in spring)
    storage.putItemToStorage();
    // There is no need to call EntityManager::persist or EntityManager::merge
    // if you are withing transaction context 
    // and you are working with managed entity
    // and have cascade = CascadeType.ALL