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

Micronaut Data插入父子关联实体时父ID未填充子外键如何解决

问题描述

在一对多关联关系中,父实体自动生成的主键不会自动填充到子实体对应的外键字段中,具体复现代码如下:

1. 实体代码(Kotlin)

Parent和Child是一对多关系,二者主键均由数据库自动生成:

import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.Relation
import io.micronaut.data.annotation.Relation.Cascade.PERSIST
import io.micronaut.data.annotation.Relation.Kind.ONE_TO_MANY
import io.micronaut.data.model.naming.NamingStrategies

@MappedEntity("Parent", namingStrategy = NamingStrategies.Raw::class)
data class Parent(
  @GeneratedValue @field:Id val id: Long?,
  val name: String,
  @field:Relation(ONE_TO_MANY, mappedBy = "parent_id", cascade = [PERSIST])
  val children: List<Child>? = emptyList()
) {
  // Copy-constructor, used by ORM:
  constructor(
    name: String,
    children: List<Child>?
  ) : this(
    null,
    name,
    children
  )
}

@MappedEntity("Child", namingStrategy = NamingStrategies.Raw::class)
data class Child(
  @GeneratedValue @field:Id val id: Long?,
  val parent_id: Long?,
  val description: String,
) {
  // Copy-constructor, used by ORM:
  constructor(
    description: String,
  ) : this(null, null, description)
}

2. Repository代码(Kotlin)

import io.micronaut.data.annotation.Repository
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect.H2
import io.micronaut.data.repository.GenericRepository
import javax.transaction.Transactional

@Repository
@JdbcRepository(dialect = H2)
interface ParentRepository : GenericRepository<Parent, Long> {

  @Transactional
  fun save(parent: Parent): Parent

}

3. 测试用例(Spock/Groovy)

import groovy.sql.Sql
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

import javax.sql.DataSource

@MicronautTest
class ParentRepositorySpec extends Specification {

  @Inject
  DataSource dataSource
  @Inject
  ParentRepository parentRepository

  def 'Inserting parent with single child'() {
    given:
    loadDatabaseSchemaTo(dataSource)
    def child = new Child('Single child')
    def parent = new Parent('Parent', List.of(child))

    when:
    def savedParent = parentRepository.save(parent)

    then:
    savedParent.id != null // Primary Key field gets populated
    savedParent.items[0].id != null // Same with primary key field of child item
  }

  static void loadDatabaseSchemaTo(DataSource dataSource) {
    def createTableParent = '''
CREATE TABLE `Parent` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
)'''
    def createTableChild = '''
CREATE TABLE `Child` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `parent_id` int(5) NOT NULL,  -- <= Mandatory foreign key
  `description` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  FOREIGN KEY (`parent_id`) REFERENCES `Parent` (`id`)
)'''
    new Sql(dataSource).with { sql ->
      sql.execute(createTableParent)
      sql.execute(createTableChild)
    }
  }

}

报错信息

执行测试时抛出外键字段为空的异常:

io.micronaut.data.exceptions.DataAccessException: SQL Error executing INSERT: SQL error executing INSERT: NULL not allowed for column "PARENT_ID"; SQL statement:
INSERT INTO `Child` (`parent_id`,`description`) VALUES (?,?) [23502-200]

解决方案

你的代码有两处核心配置错误,对应两种修改方案:

方案一:单向一对多关联(无需修改Child实体结构)

你之前在@Relation注解中错误使用了mappedBy属性:mappedBy仅用于双向关联,指向子实体中对应父实体的关联属性名,不能直接填数据库外键字段名。
如果不需要在Child中持有Parent的引用,直接删掉mappedBy配置,新增@JoinColumn指定外键关联规则即可:

// 修改Parent类中children字段的注解
@field:Relation(ONE_TO_MANY, cascade = [PERSIST])
@field:JoinColumn(name = "parent_id", referencedColumnName = "id")
val children: List<Child>? = emptyList()

Micronaut Data会在插入Parent拿到生成的主键后,自动将主键值填充到关联的Child实体的parent_id字段中,再执行Child的插入操作。

方案二:双向一对多关联

如果你需要在Child中持有Parent的引用,需要调整实体的关联配置:

  1. 修改Child实体,将单独的parent_id外键值字段替换为指向Parent的关联属性,用@JoinColumn指定对应数据库的外键字段名:
@MappedEntity("Child", namingStrategy = NamingStrategies.Raw::class)
data class Child(
  @GeneratedValue @field:Id val id: Long?,
  @field:Relation(Relation.Kind.MANY_TO_ONE)
  @field:JoinColumn(name = "parent_id")
  val parent: Parent?,
  val description: String,
) {
  constructor(description: String) : this(null, null, description)
}
  1. 修改Parent中@Relation注解的mappedBy属性,指向Child中定义的parent关联属性:
@field:Relation(ONE_TO_MANY, mappedBy = "parent", cascade = [PERSIST])
val children: List<Child>? = emptyList()

额外注意

你的测试用例中savedParent.items[0].id写法有误,Parent类中对应的子实体集合属性名为children,需要修改为savedParent.children[0].id,否则测试会抛出属性不存在的异常。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 06:27:03