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

Spring+Hibernate多对多关联:如何实现单条关联记录的增删?

问题

我在学习Spring框架开发后端项目时遇到了多对多关联的问题:User和Film是多对多关系,一个用户可以选择多个影片,一个影片可以被多个用户选择。但每次向User的films列表添加或删除Film,调用userRepository.save(user)后,Hibernate会先删除该用户在user_film表中的所有关联记录,再插入新的列表。我需要实现仅增删请求中指定的Film,而非全量替换。

实体代码

User类中关联Film的部分

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(
        name = "user_film",
        joinColumns = @JoinColumn(name = "uf_user_id"),
        inverseJoinColumns = @JoinColumn(name = "uf_film_id")
)
private List<Film> films;

public List<Film> getFilms() {
    return films;
}

public void setFilms(List<Film> films) {
    this.films = films;
}

Film类中关联User的部分

@ManyToMany(cascade = CascadeType.ALL)
@JsonIgnore
@JoinTable(
        name = "user_film",
        joinColumns = @JoinColumn(name = "uf_film_id"),
        inverseJoinColumns = @JoinColumn(name = "uf_user_id")
)
private List<User> users;

public List<User> getUsers() {
    return users;
}

public void setUsers(List<User> users) {
    this.users = users;
}

业务逻辑代码

@Transactional
public UserModel addLikedFilm(Long user_id, Long film_id) {
    Optional<User> foundedUser = userRepository.findById(user_id);
    if(foundedUser.isEmpty()){
        throw new UserNotFoundException("User not found, wrong ID: " + user_id);
    }
    User user = foundedUser.get();

    Optional<Film> foundedFilm = filmRepository.findById(film_id);
    if(foundedFilm.isEmpty()){
        throw new FilmNotFoundException("Film not found, wrong ID: " + film_id);
    }
    Film film = foundedFilm.get();

    user.getFilms().add(film);
    userRepository.save(user);

    return UserModel.toModel(user);
}

@Transactional
public UserModel deleteLikedFilm(Long user_id, Long film_id) {
    Optional<User> foundedUser = userRepository.findById(user_id);
    if(foundedUser.isEmpty()){
        throw new UserNotFoundException("User not found, wrong ID: " + user_id);
    }
    User user = foundedUser.get();

    Optional<Film> foundedFilm = filmRepository.findById(film_id);
    if(foundedFilm.isEmpty()){
        throw new FilmNotFoundException("Film not found, wrong ID: " + film_id);
    }
    Film film = foundedFilm.get();

    user.getFilms().remove(film);
    userRepository.save(user);

    return UserModel.toModel(user);
}

Hibernate执行日志

添加Film 1时的日志

Hibernate: select u1_0.user_id,u1_0.birthday,u1_0.gender,case when u1_0.is_admin = 1 then true else false end,u1_0.password,u1_0.registration_date,u1_0.user_email,u1_0.user_name from user u1_0 where u1_0.user_id=?
Hibernate: select f1_0.film_id,f1_0.film_desc,f1_0.film_duration_minutes,f1_0.film_imdb_rating,f1_0.film_link,f1_0.film_name,f1_0.film_year,f1_0.image from film f1_0 where f1_0.film_id=?
Hibernate: select f1_0.uf_user_id,f1_1.film_id,f1_1.film_desc,f1_1.film_duration_minutes,f1_1.film_imdb_rating,f1_1.film_link,f1_1.film_name,f1_1.film_year,f1_1.image from user_film f1_0 join film f1_1 on f1_1.film_id=f1_0.uf_film_id where f1_0.uf_user_id=?
Hibernate: select g1_0.ufg_user_id,g1_1.genre_id,g1_1.genre_name from user_favorite_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.ufg_genre_id where g1_0.ufg_user_id=?
Hibernate: select g1_0.fg_film_id,g1_1.genre_id,g1_1.genre_name from film_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.fg_genre_id where g1_0.fg_film_id=?
Hibernate: insert into user_film (uf_user_id,uf_film_id) values (?,?)

添加Film 2时的日志

Hibernate: select u1_0.user_id,u1_0.birthday,u1_0.gender,case when u1_0.is_admin = 1 then true else false end,u1_0.password,u1_0.registration_date,u1_0.user_email,u1_0.user_name from user u1_0 where u1_0.user_id=?
Hibernate: select f1_0.film_id,f1_0.film_desc,f1_0.film_duration_minutes,f1_0.film_imdb_rating,f1_0.film_link,f1_0.film_name,f1_0.film_year,f1_0.image from film f1_0 where f1_0.film_id=?
Hibernate: select f1_0.uf_user_id,f1_1.film_id,f1_1.film_desc,f1_1.film_duration_minutes,f1_1.film_imdb_rating,f1_1.film_link,f1_1.film_name,f1_1.film_year,f1_1.image from user_film f1_0 join film f1_1 on f1_1.film_id=f1_0.uf_film_id where f1_0.uf_user_id=?
Hibernate: select g1_0.ufg_user_id,g1_1.genre_id,g1_1.genre_name from user_favorite_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.ufg_genre_id where g1_0.ufg_user_id=?
Hibernate: select g1_0.fg_film_id,g1_1.genre_id,g1_1.genre_name from film_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.fg_genre_id where g1_0.fg_film_id=?
Hibernate: select g1_0.fg_film_id,g1_1.genre_id,g1_1.genre_name from film_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.fg_genre_id where g1_0.fg_film_id=?
Hibernate: delete from user_film where uf_user_id=?
Hibernate: insert into user_film (uf_user_id,uf_film_id) values (?,?)
Hibernate: insert into user_film (uf_user_id,uf_film_id) values (?,?)

添加Film 3时的日志

Hibernate: select u1_0.user_id,u1_0.birthday,u1_0.gender,case when u1_0.is_admin = 1 then true else false end,u1_0.password,u1_0.registration_date,u1_0.user_email,u1_0.user_name from user u1_0 where u1_0.user_id=?
Hibernate: select f1_0.film_id,f1_0.film_desc,f1_0.film_duration_minutes,f1_0.film_imdb_rating,f1_0.film_link,f1_0.film_name,f1_0.film_year,f1_0.image from film f1_0 where f1_0.film_id=?
Hibernate: select f1_0.uf_user_id,f1_1.film_id,f1_1.film_desc,f1_1.film_duration_minutes,f1_1.film_imdb_rating,f1_1.film_link,f1_1.film_name,f1_1.film_year,f1_1.image from user_film f1_0 join film f1_1 on f1_1.film_id=f1_0.uf_film_id where f1_0.uf_user_id=?
Hibernate: select g1_0.ufg_user_id,g1_1.genre_id,g1_1.genre_name from user_favorite_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.ufg_genre_id where g1_0.ufg_user_id=?
Hibernate: select g1_0.fg_film_id,g1_1.genre_id,g1_1.genre_name from film_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.fg_genre_id where g1_0.fg_film_id=?
Hibernate: select g1_0.fg_film_id,g1_1.genre_id,g1_1.genre_name from film_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.fg_genre_id where g1_0.fg_film_id=?
Hibernate: select g1_0.fg_film_id,g1_1.genre_id,g1_1.genre_name from film_genre g1_0 join genre g1_1 on g1_1.genre_id=g1_0.fg_genre_id where g1_0.fg_film_id=?
Hibernate: delete from user_film where uf_user_id=?
Hibernate: insert into user_film (uf_user_id,uf_film_id) values (?,?)
Hibernate: insert into user_film (uf_user_id,uf_film_id) values (?,?)
Hibernate: insert into user_film (uf_user_id,uf_film_id) values (?,?)
解决方案

1. 修正多对多关联的维护端与级联配置

当前问题的核心原因:

  • 两端都配置了@JoinTable,导致Hibernate无法确定关联关系的维护端,触发全量删除再插入的逻辑。
  • CascadeType.ALL在多对多场景下完全没必要,Film和User是独立实体,不需要通过一方的操作级联影响另一方。

修改方式:

  • 仅在User端配置@JoinTable作为维护端,Film端使用mappedBy指向User的关联字段,作为反向端。
  • 移除CascadeType.ALL,若不需要级联操作可直接不设置级联;若需要保存新用户时自动关联新影片,可设置CascadeType.PERSIST。

修改后的User类代码:

@ManyToMany
@JoinTable(
        name = "user_film",
        joinColumns = @JoinColumn(name = "uf_user_id"),
        inverseJoinColumns = @JoinColumn(name = "uf_film_id")
)
private List<Film> films;

// getter和setter不变

修改后的Film类代码:

@ManyToMany(mappedBy = "films")
@JsonIgnore
private List<User> users;

// getter和setter不变

2. 确保集合是持久化代理对象

通过userRepository.findById获取User后,关联的films集合是懒加载的,调用user.getFilms()会触发数据库查询加载Hibernate的持久化代理集合(如PersistentBag),对该集合的增删操作会被Hibernate跟踪,生成单条增删SQL。

需在实体构造方法中初始化集合,避免手动new普通ArrayList导致Hibernate无法跟踪变化:

// User类构造方法
public User() {
    this.films = new ArrayList<>();
}

// Film类构造方法
public Film() {
    this.users = new ArrayList<>();
}

3. 优化删除逻辑(可选)

直接调用remove(film)可能因对象引用不一致失效,可通过ID匹配删除,无需查询Film实体:

// 修改deleteLikedFilm中的删除逻辑
user.getFilms().removeIf(f -> f.getId().equals(film_id));

验证效果

修改后,添加Film时Hibernate仅执行单条insert语句,删除时仅执行单条delete语句,不会再出现全量删除后重新插入的情况。


内容的提问来源于stack exchange,提问作者Fatia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 00:55:54