Hibernate - @ElementCollection - Strange delete/insert behavior
Asked Answered
G

4

50
@Entity
public class Person {

    @ElementCollection
    @CollectionTable(name = "PERSON_LOCATIONS", joinColumns = @JoinColumn(name = "PERSON_ID"))
    private List<Location> locations;

    [...]

}

@Embeddable
public class Location {

    [...]

}

Given the following class structure, when I try to add a new location to the list of Person's Locations, it always results in the following SQL queries:

DELETE FROM PERSON_LOCATIONS WHERE PERSON_ID = :idOfPerson

And

A lotsa' inserts into the PERSON_LOCATIONS table

Hibernate (3.5.x / JPA 2) deletes all associated records for the given Person and re-inserts all previous records, plus the new one.

I had the idea that the equals/hashcode method on Location would solve the problem, but it didn't change anything.

Any hints are appreciated!

Gavingavini answered 18/9, 2010 at 18:20 Comment(2)
I had the opposite problem - Hibernate was deleting nothing when I updated the parent. This was the only related post I found on the net, including the Hibernate documentation at JBoss. Turns out for reasons now unknown, I had the collection variable on the parent declared as final and Hibernate silently did nothing. I took out the final and it started updating and deleting nicely.Preachy
I've found this happens with fetch = FetchType.EAGER on simple list operations as well (not update or delete)l. @OrderColumn seems to have no effectFonseca
C
73

The problem is somehow explained in the page about ElementCollection of the JPA wikibook:

Primary keys in CollectionTable

The JPA 2.0 specification does not provide a way to define the Id in the Embeddable. However, to delete or update a element of the ElementCollection mapping, some unique key is normally required. Otherwise, on every update the JPA provider would need to delete everything from the CollectionTable for the Entity, and then insert the values back. So, the JPA provider will most likely assume that the combination of all of the fields in the Embeddable are unique, in combination with the foreign key (JoinColunm(s)). This however could be inefficient, or just not feasible if the Embeddable is big, or complex.

And this is exactly (the part in bold) what happens here (Hibernate doesn't generate a primary key for the collection table and has no way to detect what element of the collection changed and will delete the old content from the table to insert the new content).

However, if you define an @OrderColumn (to specify a column used to maintain the persistent order of a list - which would make sense since you're using a List), Hibernate will create a primary key (made of the order column and the join column) and will be able to update the collection table without deleting the whole content.

Something like this (if you want to use the default column name):

@Entity
public class Person {
    ...
    @ElementCollection
    @CollectionTable(name = "PERSON_LOCATIONS", joinColumns = @JoinColumn(name = "PERSON_ID"))
    @OrderColumn
    private List<Location> locations;
    ...
}

References

Conversation answered 18/9, 2010 at 19:29 Comment(7)
Any ideas how to create such an primery key using hibernate xml mappings?Jara
I found out the same "Delete All", "Insert row1", "Insert row2" ... sql behaviour. I am using OpenJPA2.2.2 and inserting @Id annotation to embeddable child entity did not help. OrderColumn requires a new integer sortorder db column and my DB schema is not maintained by this JPA app so it might not be an option.Selfinterest
would this also work with a Map? using MapKey(Join)Column etc. JPA always does the delete for me.7Eyde
Does anyone know if this solution works for basic collections? (e.g. would it work if the field was List<String> instead of List<Location>) Or does it only work with Embeddables?Eudora
@Eudora yes it does work for basic collections. The issue with the above solution is that if you delete the first element of the list, all of the elements will have their index updated (shifted one position to the left)Berzelius
Hi all, I know this issue is pretty old, but @Eudora 's comment did help for me. In my case the List<String> was correct for 'findAll' request, but for 'findOne' the items were duplicated. (database had no duplicated rows) Fixed by changing List<String> to List<CustomObject> (where CustomObject is Embeddable).Itis
for my H5.24 - it did not work. but the answer of Vlad Mihalcea is working. I have to add at least one 1)nullable = false, 2) have equals and hashcode. I removed @OrederColumn and everything works, Hibernate doesn't delete existing records.Kris
C
11

In addition to Pascal's answer, you have to also set at least one column as NOT NULL:

@Embeddable
public class Location {

    @Column(name = "path", nullable = false)
    private String path;

    @Column(name = "parent", nullable = false)
    private String parent;

    public Location() {
    }

    public Location(String path, String parent) {
        this.path = path;
        this.parent= parent;
    }

    public String getPath() {
        return path;
    }

    public String getParent() {
        return parent;
    }
}

This requirement is documented in AbstractPersistentCollection:

Workaround for situations like HHH-7072. If the collection element is a component that consists entirely of nullable properties, we currently have to forcefully recreate the entire collection. See the use of hasNotNullableColumns in the AbstractCollectionPersister constructor for more info. In order to delete row-by-row, that would require SQL like "WHERE ( COL = ? OR ( COL is null AND ? is null ) )", rather than the current "WHERE COL = ?" (fails for null for most DBs). Note that the param would have to be bound twice. Until we eventually add "parameter bind points" concepts to the AST in ORM 5+, handling this type of condition is either extremely difficult or impossible. Forcing recreation isn't ideal, but not really any other option in ORM 4.

Cheltenham answered 30/4, 2015 at 13:42 Comment(3)
Jeez, thanks man! I was struggling for hours with @ElementCollection, @OrderColumn, @Embeddable and @UniqueConstraints used together. On removing or changing the order of the java.util.List I got a violation of the unique constraint because Hibernate instead of deleting the whole collection tried to do an update. Changing the fields of the class marked as @Embeddable to be nullable did the trick!Horticulture
It looks that in the newest Hibernate (5.2.3) it is no longer relevant whether the columns are nullable or not. The update seems to be executed each and every time, which in fact is unfortunate for my app, because I need dropping all of the collection elements and then replacing them as I have a @UniqueConstraints which is violated when the update is executed. Considering moving to @OneToMany with @OrderColumn and defining this unique constraint as primary key... well.., if it is possible at all, let's see.Horticulture
You can add a Jira issue if you think it's a regressionCheltenham
K
3

We discovered that entities we were defining as our ElementCollection types did not have an equals or hashcode method defined and had nullable fields. We provided those (via @lombok for what it's worth) on the entity type and it allowed hibernate (v 5.2.14) to identify that the collection was or was not dirty.

Additionally, this error manifested for us because we were within a service method that was marked with the annotation @Transaction(readonly = true). Since hibernate would attempt to clear the related element collection and insert it all over again, the transaction would fail when being flushed and things were breaking with this very difficult to trace message:

HHH000346: Error during managed flush [Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1]

Here is an example of our entity model that had the error

@Entity
public class Entity1 {
@ElementCollection @Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}

public class Entity2 {
  private UUID someUUID;
}

Changing it to this

@Entity
public class Entity1 {
@ElementCollection @Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}

@EqualsAndHashCode
public class Entity2 {
  @Column(nullable = false)
  private UUID someUUID;
}

Fixed our issue. Good luck.

Khalilahkhalin answered 30/10, 2018 at 1:11 Comment(0)
P
2

I had the same issue but wanted to map a list of enums: List<EnumType>.

I got it working like this:

@ElementCollection
@CollectionTable(
        name = "enum_table",
        joinColumns = @JoinColumn(name = "some_id")
)
@OrderColumn
@Enumerated(EnumType.STRING)
private List<EnumType> enumTypeList = new ArrayList<>();

public void setEnumList(List<EnumType> newEnumList) {
    this.enumTypeList.clear();
    this.enumTypeList.addAll(newEnumList);
}

The issue with me was that the List object was always replaced using the default setter and therefore hibernate treated it as a completely "new" object although the enums did not change.

Pharmacognosy answered 24/1, 2019 at 15:55 Comment(1)
I confirm. This one works. Thanks a lotRobeson

© 2022 - 2024 — McMap. All rights reserved.