You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多对多关系中删除好友对象后未从数据库消失的问题

问题描述

开发支持好友添加的系统时,删除好友功能无法同步到数据库:调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 22:20:16