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

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; @Embeddablepublic 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关系。

喵记多 喵记多

喵记多 – 自带助理的 AI 笔记

喵记多 27 查看详情 喵记多

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 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 getRatings() {        return ratings;    }    public void setRatings(Set 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 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 getRatings() {        return ratings;    }    public void setRatings(Set 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中实现了一个具有复合主键和附加属性的多对多关系。

核心要点回顾:

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

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

以上就是Hibernate/Spring Boot中复合主键与多对多关联的实现指南的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/210635.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月3日 12:40:25
下一篇 2025年11月3日 12:41:37

相关推荐

  • 页面加载时图表显示异常,刷新后恢复正常,是怎么回事?

    样式延迟加载导致图表显示异常 问题: 在加载页面时,图表不能正常显示,刷新后才恢复正常。这是什么原因? 答案: 图表绘制时,CSS 样式文件或数据尚未加载完成,导致容器没有尺寸,只能使用默认最小值进行渲染。刷新时,由于缓存,加载速度很快,因此样式能够及时加载,图表就能正常渲染。 解决方案: 指定容器…

    2025年12月24日
    000
  • 黑暗主题的力量和性能优化:简单指南

    在当今的数字时代,用户体验是关键。增强这种体验的一种方法是在您的网站或应用程序上实施深色主题。它不仅看起来时尚,而且还可以提高现代设备的性能并节省电池寿命。让我们探索如何使用深色主题优化您的网站并提高性能。 为什么选择黑暗主题? 减少眼睛疲劳:深色主题对眼睛更温和,尤其是在弱光条件下。这使用户可以更…

    2025年12月24日 好文分享
    300
  • 不惜一切代价避免的前端开发错误

    简介 前端开发对于创建引人入胜且用户友好的网站至关重要。然而,在这方面犯错误可能会导致用户体验不佳、性能下降,甚至出现安全漏洞。为了确保您的网站是一流的,必须认识并避免常见的前端开发错误。 常见的前端开发错误 缺乏计划 跳过线框 跳过线框图过程是一种常见的疏忽。线框图有助于在任何实际开发开始之前可视…

    2025年12月24日
    000
  • 如何克服响应式布局的不足之处

    如何克服响应式布局的不足之处 随着移动设备的普及和互联网的发展,响应式布局成为了现代网页设计中必不可少的一部分。通过响应式设计,网页可以根据用户所使用的设备自动调整布局,使用户在不同的屏幕尺寸下都能获得良好的浏览体验。 然而,尽管响应式布局在提供多屏幕适应性方面做得相当出色,但仍然存在一些不足之处。…

    2025年12月24日
    000
  • 掌握响应式布局的关键技巧和实践经验

    掌握响应式布局的关键技巧和实践经验 随着移动设备的普及和多样性,越来越多的用户选择使用手机、平板等移动设备浏览网页,这就使得响应式布局成为了现代前端开发中的重要技术之一。响应式布局的目标就是让网页能够自适应不同尺寸的屏幕,确保在任何设备上都能提供良好的用户体验。 要掌握响应式布局的关键技巧和实践经验…

    2025年12月24日
    200
  • 研究响应式布局的问题和优化方法

    响应式布局存在的问题及优化方法研究 随着移动互联网的飞速发展,越来越多的人使用移动设备来浏览网页。为了让网站在不同设备上都能提供良好的用户体验,响应式布局已经成为了现代网页设计的标准之一。然而,响应式布局在实践中还存在一些问题,本文将对这些问题进行探讨,并提出一些优化方法。 首先,对于较大规模的网站…

    2025年12月24日
    000
  • 如何通过响应式布局改善用户体验?

    响应式布局如何提升用户体验? 随着移动设备的普及,越来越多的用户习惯使用不同尺寸的屏幕来浏览网页。为了在各种设备上呈现出良好的用户体验,响应式布局应运而生。响应式布局是一种能够根据设备的屏幕尺寸和特性来自动调整网页布局的技术。通过响应式布局,可以实现在不同屏幕上的内容可读性和可用性的优化,从而提升用…

    2025年12月24日
    200
  • CSS属性实现响应式图片延迟加载的方法

    CSS属性实现响应式图片延迟加载的方法 在网页开发中,经常会遇到需要加载大量图片的情况,特别是在移动设备上。为了提高页面的加载速度和用户体验,延迟加载(lazy loading)图像成为一种常见的优化方法。 延迟加载是指在页面加载时,只加载可见区域的图像,而不加载整个页面上的所有图像。这样可以大大减…

    2025年12月24日
    000
  • CSS的Word中的列表详解

    在word中,列表也是使用频率非常高的元素。在css中,列表和列表项都是块级元素。也就是说,一个列表会形成一个块框,其中的每个列表项也会形成一个独立的块框。所以,盒模型中块框的所有属性,都适用于列表和列表项。 除此之外,列表还有 3 个特有的属性 list-style-type、list-style…

    2025年12月24日
    000
  • jimdo能否添加html5弹窗_jimdo弹窗html5代码实现与触发条件【技巧】

    可在Jimdo实现HTML5弹窗的四种方法:一、用内置“弹窗链接”模块;二、通过HTML区块注入精简dialog结构(需配合内联CSS);三、外部托管HTML+iframe嵌入;四、纯CSS :target伪类无JS方案。 如果您希望在Jimdo网站中实现HTML5弹窗效果,但发现平台默认不支持直接…

    2025年12月23日
    000
  • 响应式HTML5按钮适配不同屏幕方法【方法】

    实现响应式HTML5按钮需五种方法:一、CSS媒体查询按max-width断点调整样式;二、用rem/vw等相对单位替代px;三、Flexbox控制容器与按钮伸缩;四、CSS变量配合requestAnimationFrame优化的JS动态适配;五、Tailwind等框架的响应式工具类。 如果您希望H…

    2025年12月23日
    000
  • jimdo如何添加html5表单_jimdo表单html5代码嵌入与字段设置【实操】

    可通过嵌入HTML5表单代码、启用字段验证属性、添加CSS样式反馈及替换提交按钮并绑定JS事件四种方式在Jimdo实现自定义表单行为。 如果您在 Jimdo 网站中需要自定义表单行为或字段逻辑,而内置表单编辑器无法满足需求,则可通过嵌入 HTML5 表单代码实现更灵活的控制。以下是具体操作步骤: 一…

    2025年12月23日
    000
  • vs里面怎么html5_VS新建项目选HTML5模板或文件选HTML5创建【创建】

    Visual Studio 中创建 HTML5 项目可通过四种方式:一、新建空 ASP.NET Web 应用程序后添加 HTML 页面;二、使用 UWP 的 Blank App 模板;三、直接新建 HTML 文件并手动编写标准 HTML5 结构;四、安装 Web Template Studio 扩展…

    2025年12月23日
    000
  • html5能否禁用搜索框自动填充_html5autocomplete关闭方法【教程】

    禁用HTML5搜索框自动填充有五种方法:一、设autocomplete=”off”;二、随机化name/id值;三、用无效autocomplete值如”nope”;四、JS动态设置autocomplete;五、设autocomplete=”…

    2025年12月23日
    000
  • 如何查看编写的html_查看自己编写的HTML文件效果【效果】

    要查看HTML文件的浏览器渲染效果,需确保文件以.html为扩展名保存、用浏览器直接打开、利用开发者工具调试、必要时启用本地HTTP服务器、或使用编辑器实时预览插件。 如果您编写了HTML代码,但无法直观看到其在浏览器中的实际渲染效果,则可能是由于文件未正确保存、未使用浏览器打开或文件扩展名设置错误…

    2025年12月23日
    400
  • html5怎么加php_html5用Ajax与PHP后端交互实现数据传递【交互】

    HTML5不能直接运行PHP,需通过Ajax与PHP通信:前端用fetch发送请求,PHP接收处理并返回JSON,前端解析响应更新DOM;注意跨域、编码、CSRF防护和输入过滤。 HTML5 本身是前端标记语言,不能直接运行 PHP 代码,但可以通过 Ajax(异步 JavaScript)与 PHP…

    2025年12月23日
    300
  • html5怎么设置单选_html5用input type=”radio”加name设单选按钮组【设置】

    HTML5 使用 type=”radio” 实现单选功能,需统一 name 值构成互斥组;通过 checked 设默认项;可用 CSS 隐藏原生控件并自定义样式;推荐用 fieldset/legend 增强语义;required 可实现必填验证。 如果您希望在网页中创建一组互…

    2025年12月23日
    200
  • html5 js怎么加_html5用script标签内嵌或外链引入JS代码【添加】

    在HTML5中执行JavaScript需通过script标签:一、内联编写于head或body中;二、外链引入.js文件并建议放body末尾或加defer;三、defer按序执行,async独立执行;四、可动态创建script元素插入执行。 如果您希望在HTML5页面中执行JavaScript代码,…

    2025年12月23日
    000
  • node.js怎么运行html_node.js运行html步骤【指南】

    答案是使用Node.js内置http模块、Express框架或第三方工具serve可快速搭建服务器预览HTML文件。首先通过http模块创建服务器并读取index.html返回响应;其次用Express初始化项目并配置静态文件服务;最后利用serve工具全局安装后一键启动服务器,三种方式均在浏览器访…

    2025年12月23日
    300
  • html5能否插入带表单的文档_html5表单文档嵌入与数据提交【步骤】

    HTML5中无法直接嵌入外部带表单的HTML文档并原生提交;可行方案有四:一、用iframe嵌入,需同源或CORS支持,并用postMessage通信;二、用fetch+DOMParser动态加载表单片段并手动绑定事件;三、在当前页面直接编写表单,最规范且兼容性好;四、用JavaScript+fet…

    2025年12月23日
    000

发表回复

登录后才能评论
关注微信