使用@MappedSuperclass的Spring JPA分页排序时关联错误实体类型
问题背景
你在Kotlin中实现了版本化实体体系,通过泛型超类VersionedEntity关联主实体与对应的版本实体,但使用Sort分页请求时遇到路径解析错误:所有实体的latest属性被JPA错误识别为EntityAVersion类型,导致排序时找不到对应版本实体的字段。
核心实体结构
泛型超类定义:
@MappedSuperclass abstract class VersionedEntity<Self : VersionedEntity<Self, V>, V : EntityVersion<V, Self>>( @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "current_version_id", referencedColumnName = "id") var latest: V? = null, // 其他通用字段 ) : BaseEntity()
具体实体示例:
@Entity @Table(name = "entity_a") class EntityA( // 自定义字段 ) : VersionedEntity<EntityA, EntityAVersion>()
@Entity @Table(name = "entity_a_version") @JsonInclude(JsonInclude.Include.NON_NULL) class EntityAVersion( // EntityA专属字段 ) : EntityVersion<EntityAVersion, EntityA>()
类似的实体对包含B、C到G,每个主实体对应独立的版本实体。
问题根源
Spring Data JPA的QueryUtils在解析泛型超类的关联路径时,依赖Hibernate的AbstractManagedType.getAttribute()方法获取属性元数据。由于泛型类型擦除,该方法从超类获取latest属性时,始终返回第一个被加载的EntityAVersion类型定义,而非当前实体对应的版本类型,导致生成错误的Join关联。
修复方案
1. 在具体实体中重写latest属性
在每个主实体中重新声明latest属性,明确指定对应的版本实体类型。这会让Hibernate为每个实体生成独立的属性元数据,避免泛型类型擦除带来的识别错误。
示例(EntityB):
@Entity @Table(name = "entity_b") class EntityB( @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "current_version_id", referencedColumnName = "id") override var latest: EntityBVersion? = null, // EntityB自定义字段 ) : VersionedEntity<EntityB, EntityBVersion>()
此方案直接解决元数据识别问题,是最简洁有效的修复方式。
2. 升级依赖版本
当前使用的Spring Data JPA 2.6.10和Hibernate 5.6.15.Final组合存在泛型路径解析的已知bug。升级到更高版本可以修复该问题:
- 升级Spring Data JPA至3.x+(需配合Spring Boot 3.x)
- 升级Hibernate至6.x+
新版本对泛型元数据的处理更完善,能正确解析泛型超类的属性类型。
3. 自定义排序逻辑绕过自动解析
如果无法修改实体结构或升级版本,可以通过自定义Specification手动构建查询,明确指定关联的版本实体类型:
fun findEntityBWithSort(pageable: Pageable): Page<EntityB> { val spec = Specification<EntityB> { root, query, cb -> // 手动关联EntityBVersion val versionJoin = root.join<EntityB, EntityBVersion>("latest", JoinType.LEFT) // 转换排序规则到正确的关联路径 val orders = pageable.sort.map { sortOrder -> val targetPath = if (sortOrder.property.startsWith("latest.")) { versionJoin.get(sortOrder.property.substringAfter("latest.")) } else { root.get(sortOrder.property) } if (sortOrder.isAscending) cb.asc(targetPath) else cb.desc(targetPath) } query.orderBy(orders) cb.conjunction() } return entityBRepository.findAll(spec, pageable) }
此方案绕过QueryUtils的自动解析逻辑,直接控制关联和排序路径,确保使用正确的版本实体。
内容的提问来源于stack exchange,提问作者Busman

