JPA object with collection return returns false for contains method for individual item
I have two JPA entity classes, group and user
Group.java:
@Entity
@Table(name = "groups")
public class Group {
@Id
@GeneratedValue
private int id;
@ManyToMany
@JoinTable(name = "groups_members", joinColumns = {
@JoinColumn(name = "group_id", referencedColumnName = "id")
}, inverseJoinColumns = {
@JoinColumn(name = "user_id", referencedColumnName = "id")
})
private Collection<User> members;
//getters/setters here
}
User.java:
@Entity
@Table(name = "users")
public class User {
private int id;
private String email;
private Collection<Group> groups;
public User() {}
@Id
@GeneratedValue
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Column(name = "email", unique = true, nullable = false)
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "groups_members", joinColumns = {
@JoinColumn(name = "user_id")
}, inverseJoinColumns = {@JoinColumn(name = "group_id")})
public Collection<Group> getGroups() {
return groups;
}
public void setGroups(Collection<Group> groups) {
this.groups = groups;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
if (id != user.id) return false;
return email.equals(user.email);
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + email.hashCode();
return result;
}
}
I tried following snippet for a single member group, where group
is the object just retrieved from the JpaRepository and user
is a member of that group and a separate object.
Collection<User> members = group.getMembers();
System.out.println(members.contains(user)); //false
User user1 = members.iterator().next();
System.out.println(user1.equals(user)); //true
After some debugging, I found that during the call .contains()
was being called User.equals()
, but the user in the Hibernate collection had null fields and therefore .equals()
evaluating to false.
So why is this so weird and what is the correct way to call it .contains()
here?
source to share
Here are a couple of pieces of this puzzle. First, the sampling type for associations @ManyToMany
is LAZY
. Therefore, in your group, the field is members
using lazy loading. When lazy loading is used, Hibernate will only use proxies for objects to actually load when they are accessed. The actual collection is most likely some implementation PersistentBag
or PersistentCollection
(forgot that and the javadocs of Hibernate seem to be unavailable at the moment) doing some magic spells behind your back.
Now, you may be wondering when you call group.getMembers()
, shouldn't you get the actual collection and be able to use it without worrying about implementing it? Yes, but there is also a lazy loading trick. You see, the objects in the collection itself are proxies that initially only have their own identifier and no other properties. It is only when such a property is accessed that the complete object is initialized. This allows Hibernate to do some clever things:
- It allows you to check the size of the collection without having to load everything.
- You can only get ids (primary keys) of objects in a collection without querying the whole object. Foreign keys are usually efficient enough to be obtained when the parent is loaded by the connection, and are used for many things, such as checking if an object is known in a persistence context.
- You can get a specific object in a collection and get initialized if every object in the collection needs to be initialized. While this can lead to multiple requests ("N + 1 problem"), it can also make sure that more data is not being sent over the network and loaded into memory than necessary.
The next piece of the puzzle is that in your class, User
you used property access instead of field access. Your annotations are on getters instead of fields (eg on Group
). This may have changed, but at least in some older versions of Hibernate, getting the ID only through the proxy only worked with property access, since the proxy works by replacing methods, but cannot bypass field access.
So what's going on is that in your equals method this part is probably working fine: if (id != user.id) return false;
... but it isn't: return email.equals(user.email);
You might as well get a nullpointer exception there, it didn't work out that the contains method calls equals on the provided object (your populated, separate user) its entries as an argument. Another way could call a null pointer. This is the final piece of the puzzle. You are using fields directly here, not using a getter for email, so you are not forcing Hibernate to load the data.
So, here are some experiments you can do. I would try them myself, but it's late here and I have to go. Let me know what the result is to make sure my answer is correct and will make it more useful to later visitors.
- Modify the property
User
access in for field access by placing JPA / Hibernate annotations in the fields. If this hasn't changed in recent releases, this should initialize all properties of user instances when accessing the collection, not just a proxy with filled ids. However, this may not work. - Try to get an instance
user1
from the collection first via an iterator. Seeing as you did not do explicit property access, I strongly suspect that getting an iterator on a collection and retrieving an element from it also forces that element to be initialized. The Java implementationcontains
for a list, for example, callsindexOf
that just walks through the internal array, but doesn't call any type methodsget
that might initiate initialization. - Try using getters in your method
equals
instead of directly accessing the field. I have found that when working with JPA it is better to always use getters and setters, even for methods in the class itself, to avoid such problems. As a real solution, this is probably the most reliable way. Be sure to follow the cases when itemail
can be empty.
JPA does some crazy magic behind its back and tries to make it mostly invisible to you, but sometimes it comes back to bite you. I would dig a little deeper into the Hibernate source code and do some experimentation if I had time, but I could come back to this later to check the above statements.
source to share
Many developers using JPA have problems with individual objects. With Hibernate, the most common problem is that you cannot create lazy loaded collections in your view, because the Persistence context was closed after calling your controller / service, which is before the view starts rendering. As you can imagine, this is something that almost every JP Hibernate developer is faced with, and the behavior is so slow that EclipseLink (the JPA reference implementation) actually decided to violate the JPA spec in this area, as it IndirectList
might actually load data after the context save closed - I like EclipseLink!
The above answer about why your unmanaged user is not equal to your managed user is correct, but I am more interested in WHY are you comparing the managed and unmanaged version of the same entity! In the context of persistence, all object references must be the same, if you have pooled a separate user, the instance returned merge
is guaranteed to be the same as an already managed user with the same ID. If two instances are user
managed, you can use ==
to check if they are the same logical entity.
Another common detachment problem occurs because developers use their objects as DTOs and instantiate directly from JSon using @RequestBody
. If you know your way around JPA, single elements and merging is certainly doable, but I think most JPA developers who are able to get it right know that creating separate DTOs saves you a lot of weird mistakes. the requirements for objects and DTOs, and I recommend that you don't mix them.
/ JPA since 2009
source to share