如何用Kotlin和JPA将多表数据合并为单一实体?
解决方案:Kotlin + JPA 实现关联表到目标DTO的转换
针对你的需求,我们可以通过JPA实体关联设计 + DTO映射的方式实现,避开复杂的继承策略,直接用关联字段处理多表关联逻辑,具体步骤如下:
1. JPA实体设计
核心思路
在Ownership实体中同时保留与OwnerBusiness、OwnerIndividual的关联,通过owner_type字段区分实际关联的所有者类型;Asset实体关联过滤后的Ownership集合(仅owned_type='ASSET'的记录)。
Asset实体
@Entity @Table(name = "asset") data class Asset( @Id @Column(name = "asset_id") val assetId: Int, @Column(name = "other_from_asset_table") val otherFromAssetTable: String, // 关联所有权记录,仅过滤owned_type为ASSET的条目 @OneToMany(mappedBy = "asset", fetch = FetchType.LAZY) @Where(clause = "owned_type = 'ASSET'") val owners: MutableList<Ownership> = mutableListOf() )
Ownership实体
@Entity @Table(name = "ownership") data class Ownership( @Id @Column(name = "ownership_id") val ownershipId: Int, @Column(name = "owner_id") val ownerId: Int, @Column(name = "owned_id") val ownedId: Int, @Column(name = "owner_type") val ownerType: String, @Column(name = "owned_type") val ownedType: String, // 关联资产表 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owned_id", referencedColumnName = "asset_id", insertable = false, updatable = false) val asset: Asset?, // 关联企业所有者(仅owner_type为BUSINESS时有效) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id", referencedColumnName = "owner_business_id", insertable = false, updatable = false) val businessOwner: OwnerBusiness?, // 关联个人所有者(仅owner_type为INDIVIDUAL时有效) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id", referencedColumnName = "owner_individual_id", insertable = false, updatable = false) val individualOwner: OwnerIndividual? )
OwnerBusiness & OwnerIndividual实体
@Entity @Table(name = "owner_business") data class OwnerBusiness( @Id @Column(name = "owner_business_id") val ownerBusinessId: Int, @Column(name = "other_from_owner_business_table") val otherFromOwnerBusinessTable: String ) @Entity @Table(name = "owner_individual") data class OwnerIndividual( @Id @Column(name = "owner_individual_id") val ownerIndividualId: Int, @Column(name = "other_from_owner_individual_table") val otherFromOwnerIndividualTable: String )
2. DTO结构设计
完全匹配你需要的JSON格式,用可空字段兼容不同所有者类型的差异:
// 资产DTO data class AssetDTO( val assetId: Int, val otherFromAssetTable: String, val owners: List<OwnerDTO> ) // 所有者DTO data class OwnerDTO( val ownershipId: Int, val ownerId: Int, val ownedId: Int, val ownerType: String, val otherFromOwnerBusinessTable: String? = null, val otherFromOwnerIndividualTable: String? = null )
3. 实体与DTO的转换
手动转换(适合简单场景)
// Asset转AssetDTO fun Asset.toDTO(): AssetDTO { val ownerDTOs = owners.map { ownership -> when (ownership.ownerType) { "BUSINESS" -> OwnerDTO( ownershipId = ownership.ownershipId, ownerId = ownership.ownerId, ownedId = ownership.ownedId, ownerType = ownership.ownerType, otherFromOwnerBusinessTable = ownership.businessOwner?.otherFromOwnerBusinessTable ) "INDIVIDUAL" -> OwnerDTO( ownershipId = ownership.ownershipId, ownerId = ownership.ownerId, ownedId = ownership.ownedId, ownerType = ownership.ownerType, otherFromOwnerIndividualTable = ownership.individualOwner?.otherFromOwnerIndividualTable ) else -> throw IllegalArgumentException("未知所有者类型: ${ownership.ownerType}") } } return AssetDTO( assetId = assetId, otherFromAssetTable = otherFromAssetTable, owners = ownerDTOs ) }
用MapStruct自动转换(适合复杂场景)
添加MapStruct依赖后,定义映射接口即可自动生成转换代码:
@Mapper(componentModel = "spring") interface AssetMapper { fun toDTO(asset: Asset): AssetDTO fun toOwnerDTO(ownership: Ownership): OwnerDTO // 自定义填充所有者详情逻辑 @AfterMapping fun fillOwnerDetails(@MappingTarget dto: OwnerDTO, ownership: Ownership) { when (ownership.ownerType) { "BUSINESS" -> dto.otherFromOwnerBusinessTable = ownership.businessOwner?.otherFromOwnerBusinessTable "INDIVIDUAL" -> dto.otherFromOwnerIndividualTable = ownership.individualOwner?.otherFromOwnerIndividualTable } } }
4. 查询优化(避免N+1问题)
用Spring Data JPA的@Query实现关联查询,一次性加载所有关联数据:
interface AssetRepository : JpaRepository<Asset, Int> { @Query("SELECT a FROM Asset a LEFT JOIN FETCH a.owners o LEFT JOIN FETCH o.businessOwner LEFT JOIN FETCH o.individualOwner WHERE a.assetId = :assetId") fun findByIdWithOwners(@Param("assetId") assetId: Int): Asset? }
5. 更新逻辑处理
更新时需先查询现有实体,再合并DTO数据,避免主键冲突:
fun updateAssetFromDTO(existingAsset: Asset, dto: AssetDTO) { // 更新资产基础信息 existingAsset.otherFromAssetTable = dto.otherFromAssetTable // 处理所有者的增删改 val existingOwnershipMap = existingAsset.owners.associateBy { it.ownershipId } val dtoOwnershipMap = dto.owners.associateBy { it.ownershipId } // 删除DTO中不存在的所有权记录 existingOwnershipMap.keys.forEach { id -> if (!dtoOwnershipMap.containsKey(id)) { existingAsset.owners.remove(existingOwnershipMap[id]) } } // 更新或添加新的所有权记录 dtoOwnershipMap.values.forEach { dtoOwner -> existingOwnershipMap[dtoOwner.ownershipId]?.let { existing -> // 更新已有记录 existing.ownerId = dtoOwner.ownerId existing.ownerType = dtoOwner.ownerType when (dtoOwner.ownerType) { "BUSINESS" -> existing.businessOwner?.otherFromOwnerBusinessTable = dtoOwner.otherFromOwnerBusinessTable "INDIVIDUAL" -> existing.individualOwner?.otherFromOwnerIndividualTable = dtoOwner.otherFromOwnerIndividualTable } } ?: run { // 添加新记录 val newOwnership = when (dtoOwner.ownerType) { "BUSINESS" -> Ownership( ownershipId = dtoOwner.ownershipId, ownerId = dtoOwner.ownerId, ownedId = existingAsset.assetId, ownerType = dtoOwner.ownerType, ownedType = "ASSET", asset = existingAsset, businessOwner = OwnerBusiness(dtoOwner.ownerId, dtoOwner.otherFromOwnerBusinessTable!!), individualOwner = null ) "INDIVIDUAL" -> Ownership( ownershipId = dtoOwner.ownershipId, ownerId = dtoOwner.ownerId, ownedId = existingAsset.assetId, ownerType = dtoOwner.ownerType, ownedType = "ASSET", asset = existingAsset, businessOwner = null, individualOwner = OwnerIndividual(dtoOwner.ownerId, dtoOwner.otherFromOwnerIndividualTable!!) ) else -> throw IllegalArgumentException("未知所有者类型: ${dtoOwner.ownerType}") } existingAsset.owners.add(newOwnership) } } }
内容的提问来源于stack exchange,提问作者ConfusedUser
相关产品推荐
相关产品推荐

