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?

+3


source to share


2 answers


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 implementation contains

    for a list, for example, calls indexOf

    that just walks through the internal array, but doesn't call any type methods get

    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 it email

    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.

+4


source


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.

Master

/ JPA since 2009

+1


source







All Articles