You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 14:49:52