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 -
- Persist the friends in the Database before it is put into the
friendship
Map. So that id is already assigned. - Don't override
equals
andhashcode
at all. Work with defaults. Based on, object identities. - Use a fixed hashcode. For example, if
username
never changes after it is assigned, this field can be used to generatehashcode
value.