
在许多实际应用中,两个实体之间存在多对多关系,并且这种关系本身还包含额外的属性。例如,在一个电影评分系统中,一个用户可以给多部电影评分,一部电影也可以被多个用户评分。此外,这个“评分”关系还包含一个具体的“分数”值。在这种场景下,我们需要一个中间表(或称连接表)来存储这些关系和附加属性。
考虑以下三个实体:User(用户)、Movie(电影)和Rating(评分)。
这种设计模式被称为“带有附加属性的多对多关系”,在JPA/Hibernate中,通常通过将多对多关系拆分为两个一对多关系来实现,并使用一个独立的实体来表示连接表。
当连接表(如Rating)需要由多个字段(如user_id和movie_id)共同构成主键时,我们称之为复合主键。JPA提供了两种主要的机制来映射复合主键:@EmbeddedId和@IdClass。本教程将重点介绍更常用且推荐的@EmbeddedId方法。
@EmbeddedId注解用于将一个可嵌入的类(使用@Embeddable注解)作为实体的主键。这个可嵌入的类将包含所有构成复合主键的字段。
首先,创建一个表示Rating实体复合主键的类RatingId。这个类必须实现Serializable接口,并包含构成主键的字段(userId和movieId)。此外,为了确保正确比较和哈希,必须重写equals()和hashCode()方法。
import java.io.Serializable;
import java.util.Objects;
import jakarta.persistence.Embeddable; // 对于Spring Boot 3+
// 对于Spring Boot 2.x,使用 javax.persistence.Embeddable
// import javax.persistence.Embeddable;
@Embeddable
public class RatingId implements Serializable {
private static final long serialVersionUID = 1L; // 推荐添加
private Long userId;
private Long movieId;
// 默认构造函数是JPA规范要求
public RatingId() {}
public RatingId(Long userId, Long movieId) {
this.userId = userId;
this.movieId = movieId;
}
// Getters and Setters
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getMovieId() {
return movieId;
}
public void setMovieId(Long movieId) {
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);
}
}注意事项:
Rating实体将使用@EmbeddedId来引用RatingId作为其主键,并建立与User和Movie的@ManyToOne关系。
import jakarta.persistence.*; // 对于Spring Boot 3+
// 对于Spring Boot 2.x,使用 javax.persistence.*
@Entity
@Table(name = "rating")
public class Rating {
@EmbeddedId
private RatingId ratingId;
@Column(name = "rate")
private int rate;
// @ManyToOne 关联到 User 实体
// @MapsId("userId") 表示 user_id 字段由 ratingId 中的 userId 映射
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("userId") // 将 RatingId 中的 userId 映射到 User 实体的主键
@JoinColumn(name = "user_id") // 数据库中的外键列名
private User user;
// @ManyToOne 关联到 Movie 实体
// @MapsId("movieId") 表示 movie_id 字段由 ratingId 中的 movieId 映射
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("movieId") // 将 RatingId 中的 movieId 映射到 Movie 实体的主键
@JoinColumn(name = "movie_id") // 数据库中的外键列名
private Movie movie;
// 默认构造函数
public Rating() {}
// 构造函数,方便创建 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
public RatingId getRatingId() {
return ratingId;
}
public void setRatingId(RatingId ratingId) {
this.ratingId = ratingId;
}
public int getRate() {
return rate;
}
public void setRate(int rate) {
this.rate = rate;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Movie getMovie() {
return movie;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
// 为了方便调试和日志输出,可以重写 toString
@Override
public String toString() {
return "Rating{" +
"ratingId=" + ratingId +
", rate=" + rate +
'}';
}
}注意事项:
User和Movie实体将维护到Rating实体的@OneToMany关系。
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.*; // 对于Spring Boot 3+
// 对于Spring Boot 2.x,使用 javax.persistence.*
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password; // 假设有密码字段
// @OneToMany 关联到 Rating 实体
// mappedBy 指向 Rating 实体中拥有关系(即外键)的字段
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Rating> ratings = new HashSet<>();
// 默认构造函数
public User() {}
public User(String username, String password) {
this.username = username;
this.password = password;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Rating> getRatings() {
return ratings;
}
public void setRatings(Set<Rating> ratings) {
this.ratings = ratings;
}
// 辅助方法:添加和移除评分,确保双向关联的同步
public void addRating(Rating rating) {
ratings.add(rating);
rating.setUser(this); // 确保 Rating 知道其所属的 User
if (rating.getRatingId() == null) {
rating.setRatingId(new RatingId(this.id, rating.getMovie().getId()));
} else {
rating.getRatingId().setUserId(this.id);
}
}
public void removeRating(Rating rating) {
ratings.remove(rating);
rating.setUser(null);
if (rating.getRatingId() != null) {
rating.getRatingId().setUserId(null);
}
}
}Movie实体的结构与User实体类似。
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.*; // 对于Spring Boot 3+
// 对于Spring Boot 2.x,使用 javax.persistence.*
@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 Movie() {}
public Movie(String title) {
this.title = title;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Set<Rating> getRatings() {
return ratings;
}
public void setRatings(Set<Rating> ratings) {
this.ratings = ratings;
}
// 辅助方法:添加和移除评分,确保双向关联的同步
public void addRating(Rating rating) {
ratings.add(rating);
rating.setMovie(this); // 确保 Rating 知道其所属的 Movie
if (rating.getRatingId() == null) {
rating.setRatingId(new RatingId(rating.getUser().getId(), this.id));
} else {
rating.getRatingId().setMovieId(this.id);
}
}
public void removeRating(Rating rating) {
ratings.remove(rating);
rating.setMovie(null);
if (rating.getRatingId() != null) {
rating.getRatingId().setMovieId(null);
}
}
}注意事项:
通过上述实体设计和JPA注解,我们成功地在Spring Boot和Hibernate中实现了一个具有复合主键和附加属性的多对多关系。
核心要点回顾:
这种模式是处理复杂关系模型的标准和推荐方法,它提供了清晰的数据结构和强大的持久化能力,使得在Spring Boot应用中管理此类关系变得高效且可维护。
以上就是Hibernate/Spring Boot中复合主键与多对多关联的实现指南的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号