I'm trying to insert a new Item
entity which has a Set
of Tag
children. However, if one of the Tag
s does not exist in the join table already, the transaction fails with this error:
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.logExceptions SQL Error: 0, SQLState: 23503
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.logExceptions ERROR: insert or update on table "item_tag" violates foreign key constraint "fk_tag_name"
Detail: Key (tag_name)=(new tag) is not present in table "tag".
I was under the impression that making the parent the owning side & performing operations on it would cascade them to the child objects, but it seems as if Hibernate is attempting to save data to the join table before the child is added. Checking if each tag on the item exists & inserting it if it doesn't seems clunky & a bit counter-intuitive, so what is the proper approach to this?
This is the relevant part of the Item
class:
public class Item {
...
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.DETACH})
@JoinTable(
name = "item_tag",
joinColumns = @JoinColumn(name = "item_id"),
inverseJoinColumns = @JoinColumn(name = "tag_name")
)
private Set<Tag> tags = new HashSet<>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Item item = (Item) o;
return id == item.id && Objects.equals(owner, item.owner) && Objects.equals(ownerName, item.ownerName) && Objects.equals(name, item.name) && Objects.equals(description, item.description) && Objects.equals(date, item.date) && Objects.equals(images, item.images) && Objects.equals(tags, item.tags) && Objects.equals(location, item.location);
}
@Override
public int hashCode() {
return Objects.hash(id, owner, ownerName, name, description, date, images, tags, location);
}
...
}
and the Tag
class:
public class Tag {
...
@ManyToMany(mappedBy = "tags", fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JsonIgnore
private Set<Item> items = new HashSet<>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Tag tag = (Tag) o;
return Objects.equals(description, tag.description) && Objects.equals(firstUsed, tag.firstUsed) && Objects.equals(items, tag.items);
}
@Override
public int hashCode() {
return Objects.hash(description, firstUsed, items);
}
...
}
Here is the structure of the respective tables:
Table "public.item"
Column | Type | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+------------------------------
id | bigint | | not null | generated always as identity
name | character varying | | not null |
description | character varying | | |
date | timestamp with time zone | | not null | now()
location_id | bigint | | |
owner_id | bigint | | |
Indexes:
"items_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"fk_location_id" FOREIGN KEY (location_id) REFERENCES location(id)
"fk_owner_id" FOREIGN KEY (owner_id) REFERENCES users(id)
Referenced by:
TABLE "image" CONSTRAINT "fk_item_id" FOREIGN KEY (item_id) REFERENCES item(id)
TABLE "item_tag" CONSTRAINT "fk_item_id" FOREIGN KEY (item_id) REFERENCES item(id)
Table "public.tag"
Column | Type | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+---------
description | character varying | | not null |
first_used | timestamp with time zone | | | now()
Indexes:
"tag_pkey" PRIMARY KEY, btree (description)
Referenced by:
TABLE "item_tag" CONSTRAINT "fk_tag_name" FOREIGN KEY (tag_name) REFERENCES tag(description)
and the join table:
Table "public.item_tag"
Column | Type | Collation | Nullable | Default
----------+-------------------+-----------+----------+---------
item_id | bigint | | not null |
tag_name | character varying | | not null |
Indexes:
"item_tag_pkey" PRIMARY KEY, btree (item_id, tag_name)
Foreign-key constraints:
"fk_item_id" FOREIGN KEY (item_id) REFERENCES item(id)
"fk_tag_name" FOREIGN KEY (tag_name) REFERENCES tag(description)
EDIT: I've just changed the CascadeType
to CascadeType.ALL
in the Item
entity's @ManyToMany
annotation. Is there a reason why it would work like this but not when all CascadeType
s save CascadeType.REMOVE
are passed?
I've just changed the
CascadeType
toCascadeType.ALL
in theItem
entity's@ManyToMany
annotation. Is there a reason why it would work like this but not when allCascadeType
s saveCascadeType.REMOVE
are passed?
Yes and no. Hibernate has additional cascade types not specified by JPA, and its own, extended version of the the CascadeType
enum. It apparently accepts javax.persistence.CascadeType.ALL
as inclusive of all its cascade types, including the extended ones, but "all [javax.persistence.
]CascadeType
s save CascadeType.REMOVE
" omits not just REMOVE
but also Hibernate's extended cascade types, including DELETE
, LOCK
, REPLICATE
, and SAVE_UPDATE
. Of those, SAVE_UPDATE
is the only one that seems likely to be relevant to your problem, and indeed, you reported in comments that adding that resolved the issue.
I account it a Hibernate bug that it requires you to express a non-standard cascade type in order to get persist operations to cascade across your relationship. CascadeType.PERSIST
should accomplish that by itself. It's ok for Hibernate to provide for cascading operations outside the scope of JPA's cascade types, and it would be ok for it to provide finer-grained cascade types, but if it claims to conform to the JPA specs (which as far as I know, it does) then it needs to behave as JPA specifies.
I guess it's possible that you were using (only) org.hibernate.annotations.CascadeType
all along, instead of javax.persistence.CascadeType
. It might then be argued that Hibernate has license for its PERSIST
cascade type to behave differently than JPA's standard one, but even then there is at least a documentation flaw, because Hibernate does not appear to document any difference between the effects of its PERSIST
and of JPA's.
I note also that this might be impacted by the version of Hibernate you are using. In Hibernate 6.5, CascadeType.SAVE_UPDATE
is deprecated. The only non-deprecated extended cascade type relevant to modern Hibernate is apparently LOCK
.