Hibernate:无法将数据取回 Map<>
Hibernate: Cannot fetch data back to Map<>
拥有这个实体:
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);
}
最后 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));
}
};
}
经过userRepo.save(u1)
操作后,表格如下:
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 |
+-------+-----------+----------+-----------------+
如您所见,所有朋友都已保存。然而这个声明:
System.out.println(fetchedUser.getFriendships().get(f1));
returnsnull
。尽管 fetchedUser
已获取好友地图:
System.out.println(fetchedUser);
打印:
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=[])
那么为什么当地图 friendships
被完全获取时(所有的朋友都被获取了,因为你从上面的语句可以看出) ?
PS:
我删除了 @Data
lombok 注释(刚刚添加了 @Getter
、@Setter
和 @NoArgsConstrutor`)并自己覆盖了 equalsAndHashCode:
@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);
}
或者换句话说,equals()
方法使用 User
class.
的所有字段
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=[])
问题是当 f1
User
添加到 friendships
HashMap 时,主键 id
不存在。它会在某个时候由 Hibernate 稍后更新。这会更改 HashCode 值 !!!
hashcode
键的值在添加到 Map
后不应更改。这是导致问题的原因。模拟问题的简单测试代码 - 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;
}
解决方案
将用户添加到地图后,不应更改 hashcode
值。我认为有几个选项可以尝试解决这个问题 -
- 在将朋友放入
friendship
地图之前,将其保存在数据库中。因此该 ID 已分配。
- 根本不要覆盖
equals
和 hashcode
。使用默认值。基于对象身份。
- 使用固定的哈希码。例如,如果
username
赋值后永不改变,则可以使用该字段生成hashcode
值。
拥有这个实体:
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);
}
最后 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));
}
};
}
经过userRepo.save(u1)
操作后,表格如下:
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 |
+-------+-----------+----------+-----------------+
如您所见,所有朋友都已保存。然而这个声明:
System.out.println(fetchedUser.getFriendships().get(f1));
returnsnull
。尽管 fetchedUser
已获取好友地图:
System.out.println(fetchedUser);
打印:
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=[])
那么为什么当地图 friendships
被完全获取时(所有的朋友都被获取了,因为你从上面的语句可以看出) ?
PS:
我删除了 @Data
lombok 注释(刚刚添加了 @Getter
、@Setter
和 @NoArgsConstrutor`)并自己覆盖了 equalsAndHashCode:
@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);
}
或者换句话说,equals()
方法使用 User
class.
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=[])
问题是当 f1
User
添加到 friendships
HashMap 时,主键 id
不存在。它会在某个时候由 Hibernate 稍后更新。这会更改 HashCode 值 !!!
hashcode
键的值在添加到 Map
后不应更改。这是导致问题的原因。模拟问题的简单测试代码 - 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;
}
解决方案
将用户添加到地图后,不应更改 hashcode
值。我认为有几个选项可以尝试解决这个问题 -
- 在将朋友放入
friendship
地图之前,将其保存在数据库中。因此该 ID 已分配。 - 根本不要覆盖
equals
和hashcode
。使用默认值。基于对象身份。 - 使用固定的哈希码。例如,如果
username
赋值后永不改变,则可以使用该字段生成hashcode
值。