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 ArrayList
and 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?
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.
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
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:
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With