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
相关产品推荐
相关产品推荐

