You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot Rest Controller保存含子实体的父实体时遇栈溢出

问题原因与解决方案

栈溢出的根源不是account.contacts.forEach触发了Hibernate保存操作,而是Jackson序列化实体时的循环引用:
当控制器返回保存后的Account实体时,Jackson需要把它转成JSON格式。但Account包含Contact集合,每个Contact又持有Account引用,Jackson会不断递归序列化这两个对象,直到栈内存耗尽。

解决方案

方案1:使用DTO(推荐)

创建专门的响应数据传输对象(DTO),只暴露需要返回的字段,避免循环引用,同时精确控制响应格式:

首先定义DTO类:

// 账户响应DTO
data class AccountResponse(
    val id: Long,
    val name: String,
    val contacts: List<ContactResponse>,
    val callServices: Boolean
)

// 联系人响应DTO
data class ContactResponse(
    val id: Long,
    val name: String,
    val phoneNumber: String,
    val account: Long // 只返回账户ID,而非整个Account对象
)

修改控制器方法,返回DTO而非实体:

@PostMapping(produces = ["application/json"])
fun createAccount(@RequestBody account: Account): AccountResponse {
    account.contacts.forEach { c -> c.account = account }
    val savedAccount = accountRepository.save(account)
    
    return AccountResponse(
        id = savedAccount.id!!,
        name = savedAccount.name,
        contacts = savedAccount.contacts.map { contact ->
            ContactResponse(
                id = contact.id!!,
                name = contact.name,
                phoneNumber = contact.phoneNumber,
                account = savedAccount.id!!
            )
        },
        callServices = savedAccount.callServices
    )
}

方案2:用Jackson注解处理循环引用

如果不想用DTO,可以通过Jackson注解标记双向关联的序列化规则:

修改实体类:

@Entity
class Account {
    @Id
    @GeneratedValue var id: Long? = null

    @Column
    lateinit var name: String

    @OneToMany(mappedBy = "account", cascade = [CascadeType.PERSIST])
    @JsonManagedReference // 标记正向引用,序列化时正常输出
    lateinit var contacts: Set<Contact>

    @Column
    var callServices: Boolean = true
}
@Entity
class Contact {
    @Id
    @GeneratedValue
    var id: Long? = null

    @Column
    lateinit var name: String

    @Column
    lateinit var phoneNumber: String

    @ManyToOne
    @JoinColumn(name = "account_id")
    @JsonBackReference // 标记反向引用,序列化时忽略该字段
    var account: Account? = null
}

注意:这种方式下,响应中的Contact对象不会包含account字段(只会有id、name、phoneNumber),如果需要返回account的ID,还是推荐用DTO方案。

补充说明

account.contacts.forEach { c -> c.account = account }这段代码是必要的,它手动建立了联系人与账户的关联关系,确保Hibernate保存联系人时能正确设置account_id外键,本身不会触发保存操作,真正的保存是由accountRepository.save(account)执行的。

内容的提问来源于stack exchange,提问作者Marko Galesic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 11:36:13