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.
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:
Item
record in DB with storage_id = null
storage_id
to value that it should beTo 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