Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to work with Hibernate's PersistentBag not obeying List equals contract?

I have an entity with a list:

@Entity
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)  
    @JoinColumn(name="orderId", nullable=false)
    private List<Item> items;
}

@Entity
@Data
public class Item {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @EqualsAndHashCode.Exclude
    private Long id;

    private String description;
}

I have a service that checks if two orders have the same items and if so returns the items; otherwise it returns null:

public List<Item> getItemsIfSame(Order order1, Order order2) {
      if (order1.getItems() != null && order1.getItems().equals(order2.getItems())) {
           return order1.getItems();
     }
     return null;
 }

I have a unit test where order1 and order2 have the same items. And as expected the list of items are returned from the getItemsIfSame method.

But when I run my application and it is passed two orders with the same items, null is returned . After debugging and research, I found that the actual type returned by the Order method getItems is org.hibernate.collection.internal.PersistentBag. Its documentation states:

Bag does not respect the collection API and do an JVM instance comparison to do the equals. The semantic is broken not to have to initialize a collection for a simple equals() operation.

And confirming in the source code, it just calls Object's equals method (even though it implements List).

I suppose I could copy all elements from PersistentBag to ArrayListand then compare but sometimes I'm checking equality on a object that has some nested property with a list. Is there some better way to check equality of lists between entities?

like image 771
James Avatar asked Apr 10 '19 20:04

James


People also ask

What is persistentbag in hibernate?

java.lang.Object org.hibernate.collection.AbstractPersistentCollection org.hibernate.collection.PersistentBag An unordered, unkeyed collection that can contain the same element multiple times. The Java collections API, curiously, has no Bag . Most developers seem to use List s to represent bag semantics, so Hibernate follows this practice.

Why is there no bag in hibernate Collection API?

The Java collections API, curiously, has no Bag . Most developers seem to use List s to represent bag semantics, so Hibernate follows this practice. Nested classes/interfaces inherited from class org.hibernate.collection. AbstractPersistentCollection Fields inherited from class org.hibernate.collection. AbstractPersistentCollection

Can hibernate initialize be used for collections as well?

The Hibernate.initialize can be used for collections as well. Now, because second-level cache collections are read-through, meaning that they are stored in the cache the first time they get loaded when running the following test case: Hibernate executes an SQL query to load the PostComment collection:

Why does hibernate return the same object twice in one session?

That means that no two objects are equal and all of them have a different hash code value. Hibernate makes sure to return the same object if you read the same entity twice within a Session.


1 Answers

Solution #1: Using Guava's Iterables#elementsEqual

Iterables.elementsEqual(
            order1.getItems() != null ? order1.getItems() : new ArrayList<>(),
            order2.getItems() != null ? order2.getItems() : new ArrayList<>());

Solution #2: Using java.util.Objects#deepEquals

    Objects.deepEquals(
        order1.getItems() != null ? order1.getItems().toArray() : order1,
        order2.getItems() != null ? order2.getItems().toArray() : order2);

Solution #3: Using new ArrayList objects

(order1.getItems() != null ? new ArrayList(order1.getItems()) : new ArrayList())
        .equals(order2.getItems() != null ? new ArrayList(order2.getItems()) : new ArrayList());

Solution #4 Using Apache's CollectionUtils#isEqualCollection

    CollectionUtils.isEqualCollection(
        order1.getItems() != null ? order1.getItems() : new ArrayList(),
        order2.getItems() != null ? order2.getItems() : new ArrayList());

Note that the Javadocs for the List#toArray method state the following:

Returns an array containing all of the elements in this list in proper sequence (from first to last element). The returned array will be "safe" in that no references to it are maintained by this list. (In other words, this method must allocate a new array). The caller is thus free to modify the returned array.

As such, comparing the lists in-place using Iterables may use less memory than solutions 2, 3, and 4 which all allocate new Lists or Arrays either implicitly or explicitly.

The null check could also be moved out of the ternary but would need to be performed on both of the order objects because all of these solutions involve invoking methods that are not null-safe (Iterables#elementsEqual, Lists#toArray, new ArrayList(Collection<?> collection), CollectionUtils.isEqualCollection will all throw NullPointerExceptions when invoked with null).

Side note: this issue is tracked by a long-standing hibernate bug

like image 155
pants Avatar answered Oct 02 '22 17:10

pants