深夜调Bug:那次我被@OneToMany坑到怀疑人生

内容分享2周前发布
0 0 0

大家好,我是小米,一个31岁的Java后端开发者。

我发现程序员这行啊,最容易让人“精神内耗”的不是加班、不是需求改动,而是——被注解支配的恐惧

有一天,我在项目里写了一个看似普通的实体类映射,然后一运行,控制台瞬间爆红:

com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)。

我心想:“完了,这回真是递归到天荒地老了。”

没错,今天这篇文章,就想和你聊聊我那次因为 @OneToMany 映射引发的“血案”,以及我后来是怎么优雅化解循环依赖这场灾难的。

事情的起因:那条看似无害的关联

那天,我在写一个订单模块。需求非常简单——订单(Order)和明细(Detail)是一对多的关系。于是我写了这样一段代码:

深夜调Bug:那次我被@OneToMany坑到怀疑人生

看起来没毛病对吧?但过了两分钟,我在前端调试接口的时候,发现返回结果是这样的:

无限递归!

就像照镜子一样,order 里有 details,details 里又有 order,一层层往里套,最后栈直接爆掉。

那一夜,我查了整整三十页 StackOverflow

凌晨两点,我还在和 Google 斗智斗勇。

各种答案都有,有人说用 @JsonIgnore,有人说用 @JsonBackReference,也有人说干脆别用 @OneToMany。但我不甘心。因为我知道,在 JPA 世界里,关系是神圣的:

我必须搞清楚,问题到底出在哪。

于是我重新审视了这段映射。我原来的写法用了 mappedBy,意味着由子表(Detail)维护外键

这本身没错,但如果我在序列化时不加限制,父子就会互相引用,Jackson 序列化器就会懵掉。

破局的关键:那几个被我忽略的注解

经过多次实验,我最终写出了一个更优雅的版本:

深夜调Bug:那次我被@OneToMany坑到怀疑人生

看似几行代码,却暗藏乾坤。让我带你一步步拆解其中的玄机。

@OneToMany:一对多的核心关系

这是灵魂注解,告诉 JPA “我是一对多的关系”。

我加上 cascade = CascadeType.ALL 是为了让订单保存时自动级联保存明细。而 fetch = FetchType.LAZY 则是懒加载策略:

除非我真的要访问 details,否则它不会立刻查数据库。

这一点,在性能优化中非常重要。

@JoinColumn:谁来维护外键?

这是这次重构的关键。

以前我用了 mappedBy,代表让子类(Detail)去维护外键;但现在,我改成了 @JoinColumn,相当于告诉 JPA:“我自己来指定外键字段。”

name = “foreign_key_id” 表示外键列的名字,referencedColumnName = “id” 表示它关联的是主表的主键。

然后重点来了——insertable = false, updatable = false。

这两行意味着这个外键字段在插入和更新时都不会被当前实体操作,防止主从表互相操作外键导致冲突。

一句话总结:

这段注解定义了外键,但不让它干预外键的更新。

@JsonBackReference:循环引用的终结者

当 Jackson 序列化时,它看到 @JsonBackReference 就会跳过这个字段。

也就是说,它不会再序列化回去找父对象,从而避免无限递归。如果不加这个注解,父对象有子对象,子对象又有父对象,那就完了,递归直奔 StackOverflow。

@JSONField(serialize = false):FastJSON 的版本

有些老项目或者多框架共存的项目,会同时使用 Jackson 和 FastJSON。

为了保险起见,我又加上了这个注解,让两个序列化框架都能识别并跳过这段引用。

这一步虽然简单,却让我在不同环境下的接口返回都更稳定。

那一刻,我终于看到了干净的 JSON

当我再次启动项目、调用接口时,返回结果终于变得干净整洁:

深夜调Bug:那次我被@OneToMany坑到怀疑人生

没有无限嵌套,没有栈溢出,连日志都静悄悄的。那一刻,我真的有种“风暴过后,天朗气清”的感觉。

那些被坑过的“小细节”

我总结了几个小坑,分享给同样在和 JPA 打仗的你:

@JsonManagedReference 和 @JsonBackReference 要成对出现:前者放在父类,后者放在子类.否则 Jackson 可能仍然不知道从哪儿断开递归。别乱用 EAGER:一对多关系如果设为 EAGER,数据库查询会爆炸性增长;用懒加载(LAZY)更安全。insertable=false, updatable=false 的含义要搞清楚:它并不是“不能操作”,而是“由另一方维护外键”。双向关系不是必须的:有时候,单向关系(例如从父查子)就够用了,能少写点注解就少写点。

从“被坑”到“通透”的成长

那次之后,我对 ORM 有了新的理解。

以前我总觉得:ORM 就是用注解把数据库表粘起来的工具。现在我明白,ORM 更像是一种“关系哲学”:

父与子、主与从,谁掌控谁、谁依附谁,关系不清楚就容易出事。

这和我们的人际关系、团队协作,其实有点像。

有时候,问题不是技术本身,而是边界没划清楚

END

每一个被注解“坑过”的人,其实都离“架构师”更近了一步。

我常说,代码最怕两件事:

一是循环依赖,二是盲目依赖。

前者让程序崩溃,后者让人迷失。

而我们要做的,就是用清晰的边界,让系统和自己都保持独立又有联系。

如果你也曾被 JPA 的循环引用困扰,欢迎留言告诉我你是怎么解决的。

也许你的一个小技巧,能帮别人少熬一个夜。

我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...