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的引用,需要调整实体的关联配置:
- 修改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) }
- 修改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
相关产品推荐
相关产品推荐

