Room @Relation使用复合索引关联多表报错问题求助
解决Room @Relation复合键关联报错的问题
你的报错原因很明确:@Relation的parentColumn和entityColumn必须指定实体类中的实际列名,而不是你定义的索引名称。你写的woItemUsagePk是索引的name属性值,并不是WorkOrderItemUsage实体里存在的列,所以Room找不到这个列,自然抛出错误。
Room的@Relation注解本身不支持复合键关联,它只能基于单一列的外键关系来关联父子实体。针对你的一对多场景,有两种可行的解决方案:
方案一:新增单一主键列(推荐,保持@Relation的便捷性)
给父实体WorkOrderItemUsage添加一个自增的单一主键列,让子实体通过这个单一列建立外键关联,这样就能正常使用@Relation了。
修改后的父实体代码:
@Entity( // 新增自增主键id,原来的复合键改为唯一索引 primaryKeys = ["id"], foreignKeys = [ ForeignKey(entity = WorkOrder::class, parentColumns = ["woId"], childColumns = ["woIdItemUsage"], onUpdate = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE) ], indices = [ Index("woIdItemUsage", "itemId", "branchId", name = "woItemUsagePk", unique = true), Index("woIdItemUsage") // 保留原来的外键索引 ] ) data class WorkOrderItemUsage( @PrimaryKey(autoGenerate = true) val id: Long = 0, val woIdItemUsage: String, val itemId: String, val branchId: String, // 其他原有字段... )
修改后的子实体代码:
@Entity( primaryKeys = ["woIdItemUsageLot", "itemId", "branchId", "lotId"], foreignKeys = [ // 现在外键指向父实体的单一主键id ForeignKey(entity = WorkOrderItemUsage::class, parentColumns = ["id"], childColumns = ["parentUsageId"], onUpdate = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE) ], indices = [ Index("woIdItemUsageLot", "itemId", "branchId", name = "woItemUsageLotPk", unique = true), Index("parentUsageId") // 新增外键索引 ] ) data class WorkOrderItemUsageLot( val woIdItemUsageLot: String, val itemId: String, val branchId: String, val lotId: String, val parentUsageId: Long, // 新增关联父实体id的列 // 其他原有字段... )
修改后的辅助实体代码:
class WorkOrderItemUsageWithLots(@Embedded var itemUsage: WorkOrderItemUsage) { // 现在用单一主键列关联 @Relation(parentColumn = "id", entityColumn= "parentUsageId") var lotList: List<WorkOrderItemUsageLot> = emptyList() }
这种方法的好处是保留了@Relation的自动关联能力,不需要手动写复杂的JOIN查询,Room会自动帮你处理父子数据的加载。
方案二:使用自定义JOIN查询替代@Relation
如果不想修改原有实体的主键结构,你可以直接在Dao中写JOIN查询,然后用一个包含所有字段的数据类来接收结果,再手动将结果分组为父子结构。
步骤1:创建用于接收JOIN结果的数据类
data class WorkOrderItemUsageLotJoined( // 父实体字段 val woIdItemUsage: String, val itemId: String, val branchId: String, // 子实体字段 val woIdItemUsageLot: String, val lotId: String, // 其他双方的字段... )
步骤2:在Dao中编写JOIN查询
@Dao interface WorkOrderDao { @Transaction @Query(""" SELECT woiu.*, woium.* FROM WorkOrderItemUsage woiu LEFT JOIN WorkOrderItemUsageLot woium ON woiu.woIdItemUsage = woium.woIdItemUsageLot AND woiu.itemId = woium.itemId AND woiu.branchId = woium.branchId """) suspend fun getAllUsageWithLots(): List<WorkOrderItemUsageLotJoined> // 手动将查询结果分组为父子结构 suspend fun getGroupedUsageWithLots(): List<WorkOrderItemUsageWithLots> { val joinedList = getAllUsageWithLots() return joinedList.groupBy { // 用父实体的复合键作为分组依据 Triple(it.woIdItemUsage, it.itemId, it.branchId) }.map { (_, group) -> val parent = group.first().run { WorkOrderItemUsage(woIdItemUsage, itemId, branchId /* 其他父字段 */) } val lots = group.map { WorkOrderItemUsageLot(it.woIdItemUsageLot, it.itemId, it.branchId, it.lotId /* 其他子字段 */) } WorkOrderItemUsageWithLots(parent).apply { this.lotList = lots } } } }
这种方法不需要修改原有实体结构,但需要手动处理数据分组,代码量会多一些,而且要注意处理左JOIN后的空数据情况。
总结
如果你能调整实体结构,方案一更推荐,因为它利用了Room的原生能力,代码更简洁易维护。如果不能修改原有主键,方案二可以作为替代方案,通过自定义查询和手动分组实现需求,不需要放弃Room的使用。
内容的提问来源于stack exchange,提问作者marcos E.
相关产品推荐
相关产品推荐

