Hibernate/Spring Boot中复合主键与多对多关联的实现指南(关联.主键.复合.指南.Hibernate...)

wufei123 发布于 2025-09-11 阅读(1)

hibernate/spring boot中复合主键与多对多关联的实现指南

本教程详细阐述了在Spring Boot和Hibernate框架中,如何优雅地处理具有附加属性的多对多关系,特别是当连接表需要复合主键时。我们将通过构建一个用户电影评分系统为例,深入探讨@EmbeddedId、@Embeddable以及@OneToMany、@ManyToOne等JPA注解的实际应用,并提供完整的实体模型代码及关键注意事项,确保数据模型的正确性和高效性。1. 业务场景与数据模型分析

在许多实际应用中,两个实体之间存在多对多关系,并且这种关系本身还包含额外的属性。例如,在一个电影评分系统中,一个用户可以给多部电影评分,一部电影也可以被多个用户评分。此外,这个“评分”关系还包含一个具体的“分数”值。在这种场景下,我们需要一个中间表(或称连接表)来存储这些关系和附加属性。

考虑以下三个实体:User(用户)、Movie(电影)和Rating(评分)。

  • User实体: 包含用户ID (id) 和用户名 (username)。
  • Movie实体: 包含电影ID (id) 和电影名称 (title)。
  • Rating实体: 记录用户对电影的评分。它需要关联User和Movie,并存储一个rate值。由于一个用户只能对同一部电影评分一次,user_id和movie_id的组合应该作为Rating表的主键。

这种设计模式被称为“带有附加属性的多对多关系”,在JPA/Hibernate中,通常通过将多对多关系拆分为两个一对多关系来实现,并使用一个独立的实体来表示连接表。

2. 复合主键的JPA映射:@EmbeddedId与@Embeddable

当连接表(如Rating)需要由多个字段(如user_id和movie_id)共同构成主键时,我们称之为复合主键。JPA提供了两种主要的机制来映射复合主键:@EmbeddedId和@IdClass。本教程将重点介绍更常用且推荐的@EmbeddedId方法。

@EmbeddedId注解用于将一个可嵌入的类(使用@Embeddable注解)作为实体的主键。这个可嵌入的类将包含所有构成复合主键的字段。

2.1 定义复合主键类:RatingId

首先,创建一个表示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);
    }
}

注意事项:

  • @Embeddable:标记该类是一个可嵌入的类,可以作为其他实体的一部分。
  • Serializable:复合主键类必须实现Serializable接口。
  • equals()和hashCode():这是实现复合主键的关键。Hibernate在内部使用它们来比较和识别实体,尤其是在缓存和集合操作中。如果没有正确实现,可能会导致意外的行为,例如在Set中出现重复项。
  • 默认构造函数:JPA规范要求实体及其嵌入式主键类必须有一个无参的公共或受保护的构造函数。
2.2 定义连接实体:Rating

Rating实体将使用@EmbeddedId来引用RatingId作为其主键,并建立与User和Movie的@ManyToOne关系。

PIA PIA

全面的AI聚合平台,一站式访问所有顶级AI模型

PIA226 查看详情 PIA
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 +
               '}';
    }
}

注意事项:

  • @EmbeddedId:将RatingId作为Rating实体的主键。
  • @ManyToOne:Rating实体与User和Movie实体之间是多对一关系。
  • @MapsId:这是一个非常重要的注解。它告诉JPA,RatingId中的userId和movieId字段的值是从关联的User和Movie实体的主键中映射过来的。这意味着Rating表中的user_id和movie_id外键同时也是其复合主键的一部分。
  • @JoinColumn:指定数据库中用于连接的外键列名。
  • fetch = FetchType.LAZY:推荐使用延迟加载,以避免不必要的数据库查询,提高性能。
3. 定义主实体:User和Movie

User和Movie实体将维护到Rating实体的@OneToMany关系。

3.1 User实体
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);
        }
    }
}
3.2 Movie实体

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);
        }
    }
}

注意事项:

  • @OneToMany(mappedBy = "user", ...):mappedBy属性指示Rating实体中的user字段是关系的拥有者(即外键所在方)。这意味着User实体不负责管理Rating的外键,而是由Rating实体负责。
  • cascade = CascadeType.ALL:当对User或Movie实体执行持久化操作(如保存、更新、删除)时,这些操作也会级联到其关联的Rating实体。
  • orphanRemoval = true:当从User或Movie的ratings集合中移除一个Rating实体时,该Rating实体将被自动从数据库中删除。
  • 辅助方法addRating()和removeRating():这些方法对于管理双向关联至关重要。它们确保当添加或移除一个Rating时,Rating实体本身也知道其所属的User和Movie,从而保持数据的一致性。特别是对于复合主键,创建RatingId实例时需要确保User和Movie的主键已存在。
4. 总结

通过上述实体设计和JPA注解,我们成功地在Spring Boot和Hibernate中实现了一个具有复合主键和附加属性的多对多关系。

核心要点回顾:

  1. 业务建模: 将多对多关系拆分为两个一对多关系,并引入一个中间实体(如Rating)。
  2. 复合主键: 使用@Embeddable定义复合主键类(如RatingId),并确保实现Serializable、重写equals()和hashCode()。
  3. 连接实体: 在中间实体(如Rating)中使用@EmbeddedId来引用复合主键类,并通过@ManyToOne和@MapsId注解建立与主实体的关联。
  4. 主实体: 在主实体(如User和Movie)中使用@OneToMany注解维护到中间实体的关系,并利用mappedBy、cascade和orphanRemoval管理关系的生命周期。
  5. 双向关联维护: 强烈建议在主实体中提供辅助方法(如addRating()和removeRating())来同步双向关联,以防止数据不一致。

这种模式是处理复杂关系模型的标准和推荐方法,它提供了清晰的数据结构和强大的持久化能力,使得在Spring Boot应用中管理此类关系变得高效且可维护。

以上就是Hibernate/Spring Boot中复合主键与多对多关联的实现指南的详细内容,更多请关注知识资源分享宝库其它相关文章!

相关标签: word java cad app 延迟加载 spring spring boot hibernate 构造函数 数据结构 接口 数据库 大家都在看: 如何用Java操作Word?Apache POI教程 使用Java下载文件时,为什么Word和PPT文件会变成乱码的TXT文件? Freemarker生成的Word文档中如何调整w:pict标签内图片大小? Freemarker生成Word文档:如何控制图片大小? PHPWord插件读取Word文档字符串失败:如何解决TypeError错误?

标签:  关联 主键 复合 

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。