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

Spring Boot中Hibernate复合主键@OneToMany关联实现咨询

用户评分系统的Hibernate实现方案

一、需求与初始疑问

我要构建如下数据库结构:

User表

idusername
1Mary
2George
3Candy

Movie表

idname
1Slumdog Millionaire
2Avatar
3Star Trek

Rating表

user_idmovie_idrate
116
238
329

需求是用户只能为同一部电影评分一次,我打算把Rating表的user_id和movie_id设为复合主键,同时在User-Rating、Movie-Rating之间建立一对多关联,这样设计是否正确?

我缺乏Hibernate经验,常见示例都是单一主键的一对多关联,需要一个复合主键关联的正确实现示例。


二、已尝试的代码(更新内容)

我用@Embeddable实现了部分实体,代码如下:

User实体

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Rating> ratings = new HashSet<>();

    public void addRating(Rating rating) {
        ratings.add(rating);
    }

    public void removeRating(Rating rating) {
        ratings.remove(rating);
    }

    // Getters, setters, constructors and equals methods
}

Movie实体

@Entity
@Table(name = "movie")
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    
    @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Rating> ratings = new HashSet<>();
    
    public void addRating(Rating rating) {
        ratings.add(rating);
    }

    public void removeRating(Rating rating) {
        ratings.remove(rating);
    }
    
    // ... Getters, setters, constructors and equals methods
}

Rating实体

@Entity
@Table(name = "rating")
public class Rating {

    @EmbeddedId
    private RatingId ratingId;
    
    private int rate;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    private Movie movie;
}

RatingId(嵌入式主键)

@Embeddable
public class RatingId implements Serializable {

    private Long userId;

    private Long movieId;
    
    // ...
}

三、修正后的正确实现方案

你的初始设计思路是正确的:复合主键保证“用户-电影”唯一配对,一对多关联符合业务逻辑。但现有代码存在两个关键问题:嵌入式主键与关联字段未绑定,双向关联未维护。以下是修正后的完整代码:

1. 完善RatingId嵌入式主键

必须实现equals()和hashCode(),同时提供无参构造函数(Hibernate要求):

@Embeddable
public class RatingId implements Serializable {
    private Long userId;
    private Long movieId;

    // Hibernate必需的无参构造函数
    public RatingId() {}

    // 带参构造函数,方便创建主键实例
    public RatingId(Long userId, Long movieId) {
        this.userId = userId;
        this.movieId = movieId;
    }

    // 主键比较必须实现equals和hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        RatingId ratingId = (RatingId) o;
        return Objects.equals(userId, ratingId.userId) && Objects.equals(movieId, ratingId.movieId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(userId, movieId);
    }

    // Getters and Setters
}

2. 修正Rating实体的关联映射

使用@MapsId将@ManyToOne关联的ID同步到嵌入式主键的对应字段,避免冗余存储:

@Entity
@Table(name = "rating")
public class Rating {
    @EmbeddedId
    private RatingId ratingId;

    private int rate;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("userId") // 将user的ID映射到RatingId的userId字段
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("movieId") // 将movie的ID映射到RatingId的movieId字段
    private Movie movie;

    // 无参构造函数
    public Rating() {}

    // 带参构造函数,快速创建评分实例
    public Rating(User user, Movie movie, int rate) {
        this.user = user;
        this.movie = movie;
        this.rate = rate;
        this.ratingId = new RatingId(user.getId(), movie.getId());
    }

    // Getters and Setters
}

3. 维护双向关联的一致性

在User和Movie的addRating/removeRating方法中设置反向关联,确保对象模型与数据库同步:

User类修改:

public void addRating(Rating rating) {
    ratings.add(rating);
    rating.setUser(this); // 绑定评分到当前用户
}

public void removeRating(Rating rating) {
    ratings.remove(rating);
    rating.setUser(null); // 解除绑定
}

Movie类修改:

public void addRating(Rating rating) {
    ratings.add(rating);
    rating.setMovie(this); // 绑定评分到当前电影
}

public void removeRating(Rating rating) {
    ratings.remove(rating);
    rating.setMovie(null); // 解除绑定
}

关键说明

  • @MapsId的作用:让Hibernate自动将关联实体的ID赋值到嵌入式主键的对应字段,无需手动维护,避免数据不一致。
  • 双向关联维护:必须在添加/删除评分时同步设置反向关联,否则会出现Hibernate无法识别的孤立记录,导致持久化失败。
  • 复合主键的equals()/hashCode():Hibernate依赖这两个方法判断主键唯一性,必须正确实现。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 18:30:27