0

From my previous question: Hibernate: Cannot fetch data back to Map<>, I was getting NullPointerException after I tried to fetch data back. I though the reason was the primary key (when added to Map as put(K,V), the primary key was null, but after JPA persist, it created the primary key and thus changed the HashMap()). I had this equals and hashCode:

User.java:

 @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) && Objects.equals(username, user.username) && Objects.equals(about, user.about) && Objects.equals(friendships, user.friendships) && Objects.equals(posts, user.posts);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username, about, friendships, posts);
    }

-> I used all fields in the calculation of hash. That made the NullPointerException BUT not because of id (primary key), but because of collections involved in the hash (friends and posts). So I changed both functions to use only database equality:

 @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (id == null) return false;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return this.id.equals(user.getId());
    }

    @Override
    public int hashCode() {
        return id == null ? System.identityHashCode(this) :
                id.hashCode();

So now only the id field is involved in the hash. Now, it didn't give me NullPointerException for fetched data. I used this code to test it:

(from User.java):

public void addFriend(User friend){
        Friendship friendship = new Friendship();
        friendship.setOwner(this);
        friendship.setFriend(friend);
        this.friendships.put(friend, friendship);
    }

DemoApplication.java:

@Bean
    public CommandLineRunner dataLoader(UserRepository userRepo, FriendshipRepository friendshipRepo){
        return new CommandLineRunner() {
            @Override
            public void run(String... args) throws Exception {
                User f1 = new User("friend1");
                User f2 = new User("friend2");
                User u1 = new User("user1");
                System.out.println(f1);
                System.out.println(f1.hashCode());

                u1.addFriend(f1);
                u1.addFriend(f2);
                userRepo.save(u1);

                User fetchedUser = userRepo.findByUsername("user1");
                System.out.println(fetchedUser.getFriendships().get(f1).getFriend());
                System.out.println(fetchedUser.getFriendships().get(f1).getFriend().hashCode());
            }
        };
    }

You can see I am

  1. puting the f1 User into friendship of user1 (owner of the friendship). The time when the f1.getId() == null
  2. saving the user1. The time when the f1 id gets assign its primary key value by Hibernate (because the friendship relation is Cascade.All so including the persisting)
  3. Fetching the f1 User back by geting it from the Map, which does the look-up with the hashCode, which is now broken, because the f1.getId() != null.

But even then, I got the right element. The output:

User{id=null, username='friend1', about='null', friendships={}, posts=[]}
-935581894
...
User{id=3, username='friend1', about='null', friendships={}, posts=[]}
3

As you can see: the id is null, then 3 and the hashCode is -935581894, then 3... So how is possible I was able to get the right element?

milanHrabos
  • 2,010
  • 3
  • 11
  • 45
  • I think this is because Hibernate uses a proxy on entity object. Can you try and change your `hashCode` implementation to not use `System.identityHashCode(this)` and check if that still work ? Like may be `return id == null ? 12345 : id.hashCode();`. What happens then ? – SKumar Sep 11 '21 at 15:03
  • How did it go ? Is it still working with a different hashcode implementation? – SKumar Sep 13 '21 at 07:35

2 Answers2

0

Not all Map implementation use the hashCode (for example a TreeMap implementation do not use it, and rather uses a Comparator to sort entries into a tree).

So i would first check that hibernate is not replacing the field :

private Map<User, Friendship> friendships = new HashMap<>();

with its own implementation of Map.

Then, even if hibernate keeps the HashMap, and the hashcode of the object changed, you might be lucky and both old and new hashcodes gives the same bucket of the hashmap.

As the object is the same (the hibernate session garantees that), the equals used to find the object in the bucket will work. (if the bucket has more than 8 elements, instead of the bucket being a linked list, it will be a b-tree ordered on hashcode, in that case it won't find your entry, but the map seems to have only 2-3 elements so it can't be the case).

Thierry
  • 5,270
  • 33
  • 39
  • I don't regard answer "you are just lucky" as an answer. I could be lucky for the first, maybe second time with the same result. But **everytime** I run it, it always finds the right element, so this is not about "being lucky" but about implementation. And you are right, I don't know what implementation does hibernate wrap the `HashMap`, but I know it uses the `hashCode()`, otherwise I wouldn't be getting `NullPointerException` when the `hashCode()` is calculated with collections (see my previous case). So I know it is using `hashCode()` and I know it has nothing to do with "luck" – milanHrabos Sep 11 '21 at 17:49
0

Now I understood your question. Looking at the Map documentation we read the following:

Note: great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map.

It looks like there is no definitive answer for this and as @Thierry already said it seems that you just got lucky. The key takeaway is "do not use mutable objects as Map keys".

João Dias
  • 16,277
  • 6
  • 33
  • 45