1

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.

milanHrabos
  • 2,010
  • 3
  • 11
  • 45
  • I think you are missing EqualsAndHashcode implementation. – SKumar Sep 09 '21 at 16:09
  • @SKumar well the User has `@Data` lombok annotation which should override the EqualAndHascode. – milanHrabos Sep 09 '21 at 17:45
  • Ok, but does it know that out of all fields in `User` class, it only needs to verify on `username` field ? `id` is also populated. Can you confirm that if you log `f1` it has `id` populated. I suspect it doesn't have it. – SKumar Sep 09 '21 at 18:05
  • @SKumar I don't understand what you mean, but if you look at the edit, I have override the equals and hashCode myself (well the IDE did) – milanHrabos Sep 09 '21 at 18:43
  • @SKumar the overrided `equals()` is using all fields now – milanHrabos Sep 09 '21 at 18:46
  • I meant what is the output of `System.out.println(f1)` . Does it contain `id` field populated? – SKumar Sep 09 '21 at 19:01
  • @SKumar it does `User{id=2, username='friend1', about='null', friendships={}, posts=[]}` – milanHrabos Sep 09 '21 at 19:21

1 Answers1

0

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.
SKumar
  • 1,940
  • 1
  • 7
  • 12
  • Well the second point of not generating hash and equals at all... May be not ideal approach but do I really need to override this methods? If I am not explicitly using equals in my code, then do I really need it? I know the `Map<>` is using it, but I don't so I don't need to override it, right? – milanHrabos Sep 10 '21 at 11:28
  • @milanHrabos Looks like you don't need to override those methods as this https://stackoverflow.com/q/1638723/11244881 explains. – SKumar Sep 10 '21 at 11:42
  • The reason the `hashCode` is broken is not because of change of the `id`, but because of collection. Please look here -> https://stackoverflow.com/questions/69142972/how-is-possible-the-map-will-find-the-right-element-when-the-hascode-of-that . I have made the `equalsAndHasCode` using only the `id`, and still it worked (but it shouldn't -> becuase as you said, the `id` is `null`, then assigned by hibernate when persisting). How is this possible? – milanHrabos Sep 11 '21 at 12:21