James
James

Reputation: 3184

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?

Upvotes: 13

Views: 7423

Answers (1)

pants
pants

Reputation: 192

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

Upvotes: 3

Related Questions