多对多关系中删除好友对象后未从数据库消失的问题
问题描述
开发支持好友添加的系统时,删除好友功能无法同步到数据库:调用removeFriend后内存中集合已更新,但数据库的user_friend记录仍存在,再次查询时好友列表又恢复原状。使用Spring Boot + Hibernate,为新手,不清楚问题原因。
User实体类代码
@Entity @Table(name = "users") }) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; private String name, username, email, password; @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable(name = "user_friend", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "friend_id") ) public Set<User> friends=new HashSet<>(); public void addFriend(User friend){ this.friends.add(friend); friend.getFriends().add(this); } public void removeFriend(User friend){ this.friends.remove(friend); friend.getFriends().remove(this); } // 省略其他方法 }
Service层删除好友代码
public void deleteFriend(Long userId, Long friendId) { User user = userRepository.findById(userId) .orElseThrow( () -> new RuntimeException(String.format("User with id %d not found", userId)) ); User friend = userRepository.findById(friendId) .orElseThrow( () -> new RuntimeException(String.format("User with id %d not found", friendId)) ); System.out.println(user.friends); System.out.println("Starting deleting"); user.removeFriend(friend); System.out.println(user.friends); System.out.println("Finishing deleting"); }
控制台输出
[com.carrot.VMeste.models.users.User@74c262c7, com.carrot.VMeste.models.users.User@41d8a002] Starting deleting Hibernate: select f1_0.user_id, f1_1.user_id, f1_1.created_at, f1_1.email, f1_1.expires_at, f1_1.is_delete, f1_1.last_login, f1_1.name, f1_1.password, f1_1.registered, f1_1.updated_at, f1_1.username from user_friend f1_0 join users f1_1 on f1_1.user_id=f1_0.friend_id where f1_0.user_id=? [com.carrot.VMeste.models.users.User@41d8a002] Finishing deleting
同时询问:同表多对多实现好友系统是否合理?有没有更优方案?
解决方案
一、解决删除好友不生效的问题
1. 核心原因:未触发Hibernate的持久化同步
你修改了内存中User实体的friends集合,但Hibernate不会自动将这种修改同步到数据库——除非当前方法处于事务上下文中,或者手动调用Repository的save()方法。
2. 具体修复步骤
(1)给Service方法添加事务注解
在deleteFriend方法上添加@Transactional,Spring会在方法执行时开启事务,方法结束时自动提交,Hibernate会同步持久化上下文的修改到数据库:
import org.springframework.transaction.annotation.Transactional; @Transactional public void deleteFriend(Long userId, Long friendId) { // 原有代码不变 }
(2)重写User类的equals和hashCode方法
因为你用了HashSet存储好友,remove()方法依赖对象的equals()和hashCode()判断是否为同一对象。如果没有重写这两个方法,默认会基于对象引用判断,可能导致Hibernate加载的不同实例无法被正确移除。
添加以下代码到User类:
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return userId != null && userId.equals(user.userId); } @Override public int hashCode() { return getClass().hashCode(); }
(注:用userId作为判断依据,因为主键是唯一标识)
二、好友系统的更优方案
1. 同表多对多的局限性
你当前的方案可以实现基础的双向好友关系,但扩展性很差:无法存储好友关系的额外信息,比如添加时间、好友状态(待确认/已同意/拉黑)、备注等。如果后续需要扩展这些功能,修改成本极高。
2. 推荐方案:使用中间实体(Friendship)
将多对多关系拆分为两个一对多关系,引入Friendship实体来维护好友关系,这样可以灵活添加额外字段:
(1)Friendship实体示例
@Entity @Table(name = "friendship") public class Friendship { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; @ManyToOne @JoinColumn(name = "friend_id", nullable = false) private User friend; // 好友状态:待确认、已同意、拉黑等 public enum Status { PENDING, ACCEPTED, BLOCKED } @Enumerated(EnumType.STRING) private Status status; private LocalDateTime createTime; private LocalDateTime updateTime; // 构造方法、getters、setters、equals、hashCode }
(2)修改User实体
@Entity @Table(name = "users") public class User { // 原有字段不变 // 维护自己发起的好友关系 @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List<Friendship> initiatedFriendships = new ArrayList<>(); // 维护别人发起的好友关系 @OneToMany(mappedBy = "friend", cascade = CascadeType.ALL, orphanRemoval = true) private List<Friendship> receivedFriendships = new ArrayList<>(); // 新增获取所有已同意好友的方法(可选) public List<User> getFriends() { List<User> friends = new ArrayList<>(); // 自己发起且已同意的 initiatedFriendships.stream() .filter(f -> f.getStatus() == Friendship.Status.ACCEPTED) .map(Friendship::getFriend) .forEach(friends::add); // 别人发起且已同意的 receivedFriendships.stream() .filter(f -> f.getStatus() == Friendship.Status.ACCEPTED) .map(Friendship::getUser) .forEach(friends::add); return friends; } // 省略其他方法 }
这种方案的优势:
- 支持好友申请流程(比如A发送申请,B确认后成为好友)
- 可以存储拉黑、备注等额外信息
- 便于后续扩展好友相关的业务逻辑
内容的提问来源于stack exchange,提问作者CarrotAnikerok
相关产品推荐
相关产品推荐

