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 to run the following snippet for a group with one member, where group
is an entity just retrieved from JpaRepository and user
is the member of that group and detached entity.
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 out that User.equals()
was called during .contains()
call, but the user in Hibernate collection had null fields, and thus .equals()
evaluated to false.
So why it is so odd and what is the correct way of calling .contains()
here?
There's a couple of pieces to this puzzle. First, the fetch type for @ManyToMany
associations is LAZY
. So in your Group, the members
field employs lazy loading. When lazy loading is used, Hibernate will use proxies for objects to only do the actual loading on accessing them. The actual collection is most likely some implementation of PersistentBag
or PersistentCollection
(forgot which, and the Hibernate javadocs seem inaccessible at the moment) which does some of the magic behind your back.
Now, you may wonder, when you're calling group.getMembers()
shouldn't you then get the actual collection and be capable of using it without worrying about its implementation? Yes, but there's still a catch to the lazy loading. You see, the objects in the collection themselves are proxies, which initially only have their identifier loaded but not other properties. It's only on accessing such a property that the full object is initialized. This allows Hibernate to do some clever things:
The next piece of the puzzle is that in your User
class, you've used property access instead of field access. Your annotations are on the getters instead of the fields (like in Group
). Maybe this has changed, but in at least some older versions of Hibernate, getting only the identifier via the proxies only worked with property access, because the proxy operates by substituting the methods, but can't circumvent field access.
So what happens is that in your equals method this part probably works fine: if (id != user.id) return false;
... but this doesn't: return email.equals(user.email);
You might as well have gotten a nullpointer exception there it didn't so happen that the contains method calls the equal on the provided object (your filled-in, detached user) with its collection entries as argument. The other way around could've cause a nullpointer. This is the final piece of the puzzle. You're using the fields directly here instead of using the getter for email, so you're not forcing Hibernate to load the data.
So here's some experiments you may perform. I'd try them myself, but it's getting late here and I must be going. Let me know what the outcome is, to see if my answer is correct and make it more useful for later visitors.
User
to field access by putting the JPA/Hibernate annotations on the fields. Unless this has changed in recent versions, it should cause all properties of the User instances to be initialized when accessing the collection, rather than just proxies with the identifiers filled in. This might not work anymore, however.user1
instance from the collection first via the iterator. Seeing how you didn't do explicit property access, I strongly suspect that getting an iterator on the collection and fetching an element from it also forces initialization of that element. The Java implementation of contains
for a List, for example, calls indexOf
which just goes through the internal array, but doesn't call any methods like get
which could trigger initialization.equals
method instead of direct field access. I've found that when dealing with JPA, it's better to always use the getters and setters, even for methods in the class itself, to avoid issues like these. As an actual solution, this is probably the most robust way. Do make sure to handle cases where email
may be null, though.JPA does some crazy magic behind your back and tries to make it mostly invisible for you, but sometimes it comes back to bite you. I'd dig a bit more in the Hibernate source code and run some experiments if I had the time, but I might revisit this later to verify the above claims.
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