SpringBoot3.1.2升级后单向@OneToOne映射失效,报PropertyAccessException
问题描述
将API从SpringBoot 3.0.6(Hibernate 6.1.7.Final)升级至SpringBoot 3.1.2(Hibernate 6.2.6.Final)后,调用RecordDao时原有单向@OneToOne映射出现类型转换异常,报错如下:
Caused by: org.hibernate.PropertyAccessException: Could not set value of type [com.hidden.CurrentRole] : 'com.hidden.Record.futureRole' (setter)
代码结构说明
采用@Inheritance(strategy = InheritanceType.JOINED)继承策略的父实体Resource,两个子实体FutureRole和CurrentRole继承自它,Record实体通过@OneToOne映射同时关联这两个子实体。
父实体代码
@Entity @Table(name = "RESOURCE") @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn(name = "RESOURCE_TYPE_CD", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "INT") abstract class Resource( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "RESOURCE_ID_SEQ") @SequenceGenerator(name = "RESOURCE_ID_SEQ", sequenceName = "RESOURCE_ID_SEQ", allocationSize = 1) override var id: Long? = null, @Column(name = "RESOURCE_TYPE_CD", insertable = false, updatable = false) open var resourceType: ResourceType ): MutableAuditableBaseJpa<Long>()
子实体代码
@Entity @Table(name = "RESOURCE_FUTURE_ROLE") @DiscriminatorValue("1") class FutureRole( ... ): Resource(null, ResourceType.FUTURE_ROLE)
@Entity @Table(name = "RESOURCE_CURRENT_ROLE") @DiscriminatorValue("2") class CurrentRole( ... ): Resource(null, ResourceType.CURRENT_ROLE)
Record实体及Dao代码
@Entity @Table(name = "RECORD") open class Record( @Column(name = "RESOURCE_ID") override var resourceId: Long, ... ): MutableAuditableBaseJpa<Long>() { @OneToOne @JoinColumn(name = "RESOURCE_ID", updatable = false, insertable = false) @NotFound(action = NotFoundAction.IGNORE) open var futureRole: FutureRole? = null @OneToOne @JoinColumn(name = "RESOURCE_ID", updatable = false, insertable = false) @NotFound(action = NotFoundAction.IGNORE) open var employee: CurrentRole? = null }
interface RecordDao: JpaRepository<Record, Long> { fun findByResourceId(resourceId: Long): Record? }
已尝试的无效方案
曾将@OneToOne关联改为@ManyToOne并使用@JoinTable,修改后的映射代码如下:
@ManyToOne @JoinTable(name = "RESOURCE_FUTURE_ROLE", joinColumns = [JoinColumn(name = "ID", insertable=false, updatable=false)], inverseJoinColumns = [JoinColumn(name = "ID", insertable=false, updatable=false)]) @NotFound(action = NotFoundAction.IGNORE) open var futureRole: FutureRole? = null @ManyToOne @JoinTable(name = "RESOURCE_CURRENT_ROLE", joinColumns = [JoinColumn(name = "ID", insertable=false, updatable=false)], inverseJoinColumns = [JoinColumn(name = "ID", insertable=false, updatable=false)]) @NotFound(action = NotFoundAction.IGNORE) open var employee: CurrentRole? = null
修改后虽能返回资源,但所有数据均不正确。
解决方案
核心原因
Hibernate 6.2.x版本对JOINED继承策略下的多字段关联同一外键场景,类型推断逻辑做了调整。原代码中Record的两个关联字段共用RESOURCE_ID外键,但Hibernate无法自动通过鉴别器字段区分目标子实体类型,导致实例化时出现类型不匹配。
可行修复方案
方案1:添加@Where注解明确过滤条件
在每个@OneToOne关联上添加@Where注解,通过鉴别器字段指定对应子实体的类型,让Hibernate查询时精准过滤:
@OneToOne @JoinColumn(name = "RESOURCE_ID", updatable = false, insertable = false) @NotFound(action = NotFoundAction.IGNORE) @Where(clause = "RESOURCE_TYPE_CD = 1") open var futureRole: FutureRole? = null @OneToOne @JoinColumn(name = "RESOURCE_ID", updatable = false, insertable = false) @NotFound(action = NotFoundAction.IGNORE) @Where(clause = "RESOURCE_TYPE_CD = 2") open var employee: CurrentRole? = null
注意:clause中的值必须和子实体@DiscriminatorValue完全一致。
方案2:Dao层自定义JPQL查询
如果@Where注解不生效,可在Dao层通过JPQL明确关联条件,强制指定子实体类型:
interface RecordDao: JpaRepository<Record, Long> { @Query(""" SELECT r FROM Record r LEFT JOIN FETCH r.futureRole fr LEFT JOIN FETCH r.employee e WHERE r.resourceId = :resourceId AND fr.resourceType = com.hidden.ResourceType.FUTURE_ROLE AND e.resourceType = com.hidden.ResourceType.CURRENT_ROLE """) fun findByResourceId(resourceId: Long): Record? }
方案3:调整实体映射结构(推荐)
将Record中的两个子实体关联改为父实体类型,通过类型转换获取子实体,符合JPA继承映射规范:
@Entity @Table(name = "RECORD") open class Record( @Column(name = "RESOURCE_ID") override var resourceId: Long, ... ): MutableAuditableBaseJpa<Long>() { @OneToOne @JoinColumn(name = "RESOURCE_ID", updatable = false, insertable = false) @NotFound(action = NotFoundAction.IGNORE) open var resource: Resource? = null // 提供类型转换的getter方法 fun getFutureRole(): FutureRole? = resource as? FutureRole fun getCurrentRole(): CurrentRole? = resource as? CurrentRole }
这种方式避免了多字段关联同一外键的歧义,同时简化了映射逻辑。
内容的提问来源于stack exchange,提问作者GitFlowin

