JPA@ManyToOne关联实体创建后查询报PropertyAccessException异常
问题根因
报错的核心原因有两个:
- 重复映射外键字段:
@ManyToOne关联上的@JoinColumn(name = "companyId")已经将company属性映射到数据库表的companyId外键列,你又额外定义了一个同名的companyId类属性映射同一列,属于JPA字段映射冲突。 - 非空类型接收到null值:你将单独定义的
companyId声明为Kotlin非空类型Long(不带?标记,不接受null值),同时给该字段加了@Column(insertable = false, updatable = false)配置。查询数据时,因为company关联配置了FetchType.LAZY懒加载,Hibernate会优先把结果集中的companyId值用于构造懒加载代理对象,不会给你单独定义的companyId属性赋值,直接传入null触发类型错误,也就是你看到的PropertyAccessException。
另外你之前的新增逻辑本身不符合JPA规范:JPA不会自动把你传入的companyId数值转换成对应的Company关联对象,这次能插入成功只是因为重复映射场景下Hibernate刚好取到了反序列化得到的companyId值写入外键列,属于不可预期的偶现行为,不是正确用法。
正确实现方案
1. 修正实体映射配置
删除冗余的单独companyId字段,避免同列重复映射;所有Hibernate自动赋值的字段(自增主键、自动时间戳)声明为可空类型,避免空值赋值错误。如果需要兼容前端传参、返回时的companyId字段结构,通过额外的getter方法序列化输出即可,不需要新增数据库映射字段。
修正后的实体代码:
import com.fasterxml.jackson.annotation.JsonProperty import org.hibernate.annotations.CreationTimestamp import org.hibernate.annotations.UpdateTimestamp import java.time.LocalDateTime import jakarta.persistence.* @Entity @Table(name = "Customers") data class Customer( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var customerId: Long? = null, var firstName: String, var lastName: String, var gender: String, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "companyId", referencedColumnName = "companyId") var company: Company? = null, var profilePicture: String, var email: String, var phone: String, var birthDay: String, var bio: String, var notifyByPhone: Boolean, var notifyByEmail: Boolean, var notifyBySms: Boolean, @UpdateTimestamp var updatedAt: LocalDateTime? = null, @CreationTimestamp var createdAt: LocalDateTime? = null ) { // 序列化时自动输出companyId字段,和原有接口结构完全兼容,不会触发懒加载查询 @JsonProperty(value = "companyId", access = JsonProperty.Access.READ_ONLY) fun getCompanyId(): Long? = company?.companyId }
2. 解耦接口入参与实体
不要直接拿JPA实体作为接口入参接收,新增独立的DTO类接收前端参数,避免反序列化过程破坏JPA关联状态:
data class CustomerCreateRequest( val firstName: String, val lastName: String, val gender: String, val companyId: Long, val profilePicture: String, val email: String, val phone: String, val birthDay: String, val bio: String, val notifyByPhone: Boolean, val notifyByEmail: Boolean, val notifyBySms: Boolean )
3. 修正持久化逻辑
在业务层根据传入的companyId获取对应Company的引用(用getReferenceById获取懒加载代理,不会发起额外的数据库查询,性能无损耗),再构造Customer实体完成持久化:
@Transactional @POST fun post(@Valid request: CustomerCreateRequest): Response = try { // 获取关联公司的代理引用,不触发全量查询 val companyRef = companyRepository.getReferenceById(request.companyId) val entity = Customer( firstName = request.firstName, lastName = request.lastName, gender = request.gender, company = companyRef, profilePicture = request.profilePicture, email = request.email, phone = request.phone, birthDay = request.birthDay, bio = request.bio, notifyByPhone = request.notifyByPhone, notifyByEmail = request.notifyByEmail, notifyBySms = request.notifyBySms ) repository.persist(entity) created(entity) } catch (exc: Exception) { serverError(exc) }
注意事项
- 不要将同一个数据库字段映射到两个实体属性,会引发各类不可预期的赋值、更新冲突。
- Kotlin编写JPA实体时,所有框架自动赋值的字段必须声明为可空类型(类型后加?),否则会触发非空校验错误。
- 懒加载场景下直接取关联代理的id不会触发额外关联查询,不需要为了获取外键单独冗余字段。
内容的提问来源于stack exchange,提问作者rjjdv
相关产品推荐
相关产品推荐

