Home > Blockchain >  Hibernate: Cannot fetch data back to Map<>
Hibernate: Cannot fetch data back to Map<>

Time:09-17

having this Entities:

User.java:

@Entity
@Data
@NoArgsConstructor
public class User {
    @Id @GeneratedValue
    private int id;
    private String username;
    private String about;
    @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    private Map<User, Friendship> friendships = new HashMap<>();
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    private Collection<Post> posts = new ArrayList<>();

    public User(String username) {
        this.username = username;
    }

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

    public void addPost(Post post){
        post.setAuthor(this);
        this.posts.add(post);
    }
}

Friendship.java:

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Friendship {
    @EmbeddedId
    private FriendshipId key = new FriendshipId();
    private String level;
    @ManyToOne
    @MapsId("ownerId")
    private User owner;
    @ManyToOne
    @MapsId("friendId")
    private User friend;
}

FriendshipId.java:

@Embeddable
public class FriendshipId implements Serializable {
    private int ownerId;
    private int friendId;
}

UserRepository.java:

public interface UserRepository extends JpaRepository<User, Integer> {
    public User findByUsername(String username);
}

and finaly 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");

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

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

            }
        };
    }

After the userRepo.save(u1) operation, the tables are as follows:

mysql> select * from user;
 ---- ------- ---------- 
| id | about | username |
 ---- ------- ---------- 
|  1 | NULL  | user1    |
|  2 | NULL  | friend1  |
|  3 | NULL  | friend2  |
 ---- ------- ---------- 

select * from friendship;
 ------- ----------- ---------- ----------------- 
| level | friend_id | owner_id | friendships_key |
 ------- ----------- ---------- ----------------- 
| NULL  |         2 |        1 |               2 |
| NULL  |         3 |        1 |               3 |
 ------- ----------- ---------- ----------------- 

As you can see all friends were saved. However this statement:

        System.out.println(fetchedUser.getFriendships().get(f1));

returns null. Even though the fetchedUser has the Map of friends fetched:

        System.out.println(fetchedUser);

prints:

User(id=1, username=user1, about=null, friendships={User(id=2, username=friend1, about=null, friendships={}, posts=[])=com.example.demo.model.Friendship@152581e8, User(id=3, username=friend2, about=null, friendships={}, posts=[])=com.example.demo.model.Friendship@58a5d38}, posts=[])

So why does the friend f1 couldn't be fetched (more precisely is null), when the Map friendships is fully fetched (all friends are fetched, as you could see from the above statement) ?

PS:

I have deleted the @Data lombok annotation (just added @Getter,@Setter and @NoArgsConstrutor`) and overrided the equalsAndHashCode myself:

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return 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);
    }

Or in other words, the equals() method uses all fields of the User class.

CodePudding user response:

As you can see all friends were saved. However this statement:

    System.out.println(fetchedUser.getFriendships().get(f1)); returns null. 

Even though the fetchedUser has the Map of friends fetched:

    System.out.println(fetchedUser);

prints:

 User(id=1, username=user1, about=null, friendships={User(id=2, username=friend1, about=null, friendships={}, posts=[])=com.example.demo.model.Friendship@152581e8, User(id=3, username=friend2, about=null, friendships={}, posts=[])=com.example.demo.model.Friendship@58a5d38}, posts=[])

The issue is that when the f1 User is added to friendships HashMap, primary key id was not present. It gets updated later by Hibernate at some point. This changes the HashCode value !!!

hashcode value of a key should not be changed after it is added to a Map. This is causing the issue. Simple Test Code to simulate the issue - https://www.jdoodle.com/a/3Bg3

import lombok.*;
import java.util.*;

public class MyClass {
    public static void main(String args[]) {
      Map<User, String> friendships = new HashMap<>();
        User f1 = new User();
        f1.setUsername("friend1");
        
        User f2 = new User();
        f2.setUsername("friend2");
        friendships.put(f1, "I am changed. Can't find me");
        friendships.put(f2, "Nothing changed. So, you found me");
        
        System.out.println(f1.hashCode()); // -600090900
        f1.setId(1); // Some id gets assigned by hibernate. Breaking the hashcode
        System.out.println(f1.hashCode()); // -600090841 (this changed !!!)

        System.out.println(friendships); // prints f1, f2 both
        System.out.println(friendships.get(f1)); // prints null
        System.out.println(friendships.get(f2));
    }
}

// @Data
@Getter
@Setter
@EqualsAndHashCode
@ToString
class User
{
    private int id;
    private String username;
}

Solution

The hashcode value should not be changed after a User is added to the Map. I think there are couple of options which can be tried to solve this -

  1. Persist the friends in the Database before it is put into the friendship Map. So that id is already assigned.
  2. Don't override equals and hashcode at all. Work with defaults. Based on, object identities.
  3. Use a fixed hashcode. For example, if username never changes after it is assigned, this field can be used to generate hashcode value.
  • Related