JPA/Hibernate处理NullID复合主键错误方法
“纵有疾风来,人生不言弃”,这句话送给正在学习文章的朋友们,也希望在阅读本文《JPA/Hibernate复合主键处理Null ID错误方法》后,能够真的帮助到大家。我也会在后续的文章中,陆续更新文章相关的技术文章,有好的建议欢迎大家在评论留言,非常感谢!

本文旨在解决JPA/Hibernate中使用`@EmbeddedId`作为复合主键时,因外键关联未正确嵌入导致`Null ID`生成错误的问题。通过将`@ManyToOne`关联直接整合到`@Embeddable`类中,并优化实体映射与保存逻辑,确保复合主键在持久化前完整初始化,从而避免运行时错误,提升数据模型的一致性和健壮性。
理解JPA/Hibernate中嵌入式复合主键的挑战
在使用JPA和Hibernate构建数据模型时,复合主键是一种常见需求,尤其当一个实体的主键由多个字段组成时。@EmbeddedId注解允许我们将一个独立的@Embeddable类用作实体的主键。然而,当这个复合主键的一部分是一个外键(即关联到另一个实体的主键)时,如果没有正确配置,很容易遇到“Null ID generated”错误。
问题的核心在于,当一个实体(例如BlockAttribute)使用@EmbeddedId,并且该@EmbeddedId包含一个外键(例如blockID,指向Block实体的主键),在保存BlockAttribute之前,BlockAttributeID中的所有组件都必须被正确初始化。如果BlockAttributeID仅仅包含一个Long blockID字段,而BlockAttribute实体本身又有一个@ManyToOne Block block字段,那么在保存BlockAttribute时,JPA/Hibernate可能无法自动将Block实体的主键值填充到BlockAttributeID中的blockID字段。
考虑以下初始的数据模型:
1. BlockAttributeID (嵌入式主键类)
@Embeddable
@Data // Lombok注解,用于生成getter/setter, equals, hashCode等
public class BlockAttributeID implements Serializable {
@Column(name = "block_id")
Long blockID; // 仅包含Block的ID
String attribute;
// equals 和 hashCode 方法的实现需要注意,尤其是当blockID可能为null时
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BlockAttributeID)) return false;
BlockAttributeID that = (BlockAttributeID) o;
return Objects.equals(blockID, that.blockID) && Objects.equals(attribute, that.attribute);
}
@Override
public int hashCode() {
return Objects.hash(blockID, attribute);
}
}2. BlockAttribute (使用嵌入式主键的实体)
@Data
@Table(name = "block_attribute")
@Entity
public class BlockAttribute {
@EmbeddedId
BlockAttributeID blockAttributeID;
// 冗余的ManyToOne关联,与EmbeddedId中的blockID形成冲突或混淆
@ManyToOne(fetch = FetchType.LAZY)
@JsonIgnore
@JoinColumn(name = "block_id") // 这个@JoinColumn通常会导致问题
Block block; // 这里又有一个Block实体引用
String label;
// ... 其他字段
// equals 和 hashCode 同样需要基于复合主键进行正确实现
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BlockAttribute)) return false;
BlockAttribute that = (BlockAttribute) o;
return Objects.equals(blockAttributeID, that.blockAttributeID);
}
@Override
public int hashCode() {
return Objects.hash(blockAttributeID);
}
}3. Block (父实体)
@Table(name = "block")
@Entity
@Data
public class Block {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "block_id")
Long blockID; // Block的主键
// ... 其他字段和关联
@OneToMany(mappedBy = "block", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
Set<BlockAttribute> blockAttributes = new HashSet<>();
// ... 其他方法
}当尝试以下保存逻辑时,就会出现Null ID generated for: class BlockAttribute错误:
// 1. 保存父Block实体,生成其blockID block = blockRepository.save(block); // 2. 设置BlockAttribute的block字段 blockAttribute.setBlock(block); // 此时blockAttributeID中的blockID并未被设置 // 3. 尝试保存BlockAttribute blockAttributeRepository.save(blockAttribute); // 抛出Null ID错误
问题在于,blockAttribute.setBlock(block)只是设置了BlockAttribute实体中的block引用,但@EmbeddedId中的blockID字段仍然是null。JPA在保存BlockAttribute时,需要BlockAttributeID中的所有主键组件都非空。
解决方案:将外键关联嵌入到@Embeddable类中
解决此问题的关键在于,如果一个外键是复合主键的一部分,那么该外键的@ManyToOne关联应该直接放在@Embeddable类中,而不是在主实体中重复定义。这样,@EmbeddedId就能直接持有对关联实体的引用,从而确保在创建复合主键时,能够获取到关联实体的主键信息。
1. 修正后的 BlockAttributeID 类
我们将@ManyToOne关联直接移入BlockAttributeID。
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; // 推荐使用Lombok简化代码
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
@Embeddable
@Data // 确保生成了getter/setter以及默认的equals/hashCode,但需手动优化
public class BlockAttributeID implements Serializable {
// 将ManyToOne关联直接嵌入到复合主键类中
@ManyToOne(fetch = FetchType.LAZY)
@JsonIgnore // 通常在嵌入式ID中,避免序列化Block实体,防止循环引用
@JoinColumn(name = "block_id", referencedColumnName = "block_id") // 明确指定关联列
Block block; // 现在直接持有Block实体引用
String attribute;
// 构造函数,方便创建复合主键实例
public BlockAttributeID(Block block, String attribute) {
this.block = block;
this.attribute = attribute;
}
// JPA规范要求存在无参构造函数
public BlockAttributeID() {
}
// 优化后的equals方法:基于Block的ID和attribute进行比较
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BlockAttributeID)) return false;
BlockAttributeID that = (BlockAttributeID) o;
// 比较Block实体时,应比较其主键ID,而不是整个实体对象,以避免代理问题
return Objects.equals(
this.block != null ? this.block.getBlockID() : null,
that.block != null ? that.block.getBlockID() : null
) && Objects.equals(this.attribute, that.attribute);
}
// 优化后的hashCode方法:基于Block的ID和attribute生成
@Override
public int hashCode() {
return Objects.hash(
this.block != null ? this.block.getBlockID() : null,
this.attribute
);
}
}关键点:
- @ManyToOne Block block; 直接定义在BlockAttributeID中。
- @JoinColumn(name = "block_id", referencedColumnName = "block_id") 明确指定了外键列。
- equals() 和 hashCode() 方法被优化,以Block的ID和attribute字段作为比较和哈希的依据,这对于包含实体引用的@Embeddable类至关重要。
2. 修正后的 BlockAttribute 类
由于BlockAttributeID现在已经包含了Block的关联信息,BlockAttribute实体中的冗余@ManyToOne Block block;字段应该被移除。
import lombok.Data;
import javax.persistence.*;
import java.util.Objects;
@Data
@Table(name = "block_attribute")
@Entity
public class BlockAttribute {
@EmbeddedId
BlockAttributeID blockAttributeID; // 复合主键,现在包含了Block的引用
// 移除冗余的Block字段,因为它已经包含在blockAttributeID中
// @ManyToOne(fetch = FetchType.LAZY)
// @JsonIgnore
// @JoinColumn(name = "block_id")
// Block block;
String label;
@Enumerated(EnumType.STRING)
Type type;
@Enumerated(EnumType.STRING)
Unit unit;
String value;
// equals 和 hashCode 应该基于 @EmbeddedId
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BlockAttribute)) return false;
BlockAttribute that = (BlockAttribute) o;
return Objects.equals(blockAttributeID, that.blockAttributeID);
}
@Override
public int hashCode() {
return Objects.hash(blockAttributeID);
}
}关键点:
- 移除了BlockAttribute中直接的@ManyToOne Block block字段,避免了重复映射和潜在的混淆。
- equals()和hashCode()现在完全依赖于blockAttributeID,确保了一致性。
修正后的保存逻辑
在实体映射调整后,保存逻辑也需要相应修改,以确保在创建BlockAttribute时,其@EmbeddedId能够被正确初始化。
// 1. 首先保存父Block实体,确保其主键(blockID)已生成 Block savedBlock = blockRepository.save(block); // 2. 创建BlockAttributeID实例,传入已保存的Block实体和attribute值 // 此时savedBlock已经拥有了数据库生成的主键ID BlockAttributeID blockAttributeID = new BlockAttributeID(savedBlock, completeBlockDTO.getBlockAttributeDTO().getAttribute()); // 3. 将创建好的BlockAttributeID设置到BlockAttribute实体中 blockAttribute.setBlockAttributeID(blockAttributeID); // 4. 保存BlockAttribute实体 blockAttributeRepository.save(blockAttribute); // 对于其他依赖于Block的子实体(如BlockBoundary),如果其关联方式是ManyToOne, // 则可以直接设置Block实体引用,因为它的ID是独立的,不作为其复合主键的一部分。 // blockBoundary.setBlock(savedBlock); // blockBoundaryRepository.save(blockBoundary);
总结与最佳实践
- 外键作为复合主键的一部分: 当一个外键是@EmbeddedId的一部分时,应将@ManyToOne关联直接定义在@Embeddable类中,而不是在主实体中重复定义。
- @Embeddable中的equals()和hashCode(): 务必为@Embeddable类正确实现equals()和hashCode()方法。如果@Embeddable类包含实体引用(如Block block),则在这些方法中应比较关联实体的主键ID,而不是整个实体对象,以避免Hibernate代理对象带来的问题。
- 保存顺序: 在保存使用@EmbeddedId的子实体之前,必须先保存其关联的父实体,以确保父实体的主键已经生成并可用于构建复合主键。
- 避免冗余映射: 如果外键关联已在@EmbeddedId中定义,则主实体中不应再有重复的@ManyToOne映射到同一外键,这可能导致混淆或错误。
- @JoinColumn的精确性: 在@ManyToOne映射中使用@JoinColumn时,确保name和referencedColumnName属性准确无误,指向正确的数据库列。
遵循这些最佳实践,可以有效避免JPA/Hibernate中嵌入式复合主键相关的Null ID生成错误,构建出更加健壮和易于维护的数据模型。
理论要掌握,实操不能落!以上关于《JPA/Hibernate处理NullID复合主键错误方法》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!
JavaPath类使用全攻略
- 上一篇
- JavaPath类使用全攻略
- 下一篇
- HTML怎么保存运行教程
-
- 文章 · java教程 | 25秒前 | java 多线程
- Java多线程优化性能与响应速度
- 473浏览 收藏
-
- 文章 · java教程 | 9分钟前 |
- Jackson处理嵌套对象与列表扁平化方法
- 248浏览 收藏
-
- 文章 · java教程 | 24分钟前 |
- iBeacon十六进制解析与解码技巧
- 365浏览 收藏
-
- 文章 · java教程 | 24分钟前 |
- ApachePDFBoxPDF文本搜索教程
- 433浏览 收藏
-
- 文章 · java教程 | 38分钟前 | java 成绩报表
- Java学生作业成绩统计与报表实现方法
- 367浏览 收藏
-
- 文章 · java教程 | 41分钟前 |
- IntelliJIDEA安装与Java配置教程
- 301浏览 收藏
-
- 文章 · java教程 | 56分钟前 | java 构造函数
- Java构造函数三种类型全解析
- 334浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java线程安全对象注册与注销方法
- 417浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- JDK路径选错怎么改?修复教程详解
- 226浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java统一异常处理技巧分享
- 364浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java环境变量配置教程
- 218浏览 收藏
-
- 文章 · java教程 | 2小时前 |
- JavaArrayList如何判断包含与查找缺失元素
- 289浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3222次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3436次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3467次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4575次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3845次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

