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

Room中@Embedded注解关联实体ID始终为0的问题排查

Room关联查询中@Embedded实体ID始终为0的问题排查

我在Room中执行包含JOIN的查询时遇到问题:关联多个实体的结果里,被@Embedded注解标记的实体ID始终为0。

实体类代码

@Entity(
    tableName = "FinancialCategories",
    indices = [Index(value = ["name"], unique = true)]
)
data class FinancialCategory(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    val id: Long = 0L,

    @ColumnInfo(name = "name")
    val name: String,

    @ColumnInfo(name = "parent")
    val parent: Long
)
@Entity(
    tableName = "FinancialRecords",
    foreignKeys = [ForeignKey(
        entity = FinancialCategory::class,
        parentColumns = ["id"],
        childColumns = ["category"],
        onDelete = ForeignKey.CASCADE
    )],
    indices = [Index(value = ["title"], unique = true), Index(value = ["category"], unique = false)]
)
data class FinancialRecord(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0L,

    @ColumnInfo(name = "title")
    val title: String,

    @ColumnInfo(name = "amount")
    val amount: BigDecimal,

    @ColumnInfo(name = "category")
    val category: Long
)

关联实体类

data class CategoryWithRecords(
    @Embedded
    val category: FinancialCategory,

    @Relation(
        parentColumn = "id",
        entityColumn = "category"
    )
    val records: List<FinancialRecord>
)

DAO查询代码

@Transaction
@Query("SELECT *" +
       "FROM FinancialCategories " + 
       "LEFT JOIN FinancialRecords " +
       "ON FinancialCategories.id = FinancialRecords.category " +
       "WHERE FinancialCategories.parent = (SELECT id FROM FinancialCategories WHERE name = :categoryName);")
fun getCategoriesWithItems(categoryName: String): Flow<List<CategoryWithRecords>>

仓库层代码

fun getFinancialData(category: FinancialCategories): Flow<Map<FinancialCategory, List<FinancialRecord>>> {
    return categoryDao.getCategoriesWithItems(category.displayName).map {
        it.associate {
                categoryWithRecords -> categoryWithRecords.category to categoryWithRecords.records
        }
    }
}

数据存储代码

override fun addCategory(intent: FinancialRecordIntent.AddFinanceCategory) {
    viewModelScope.launch(Dispatchers.IO) {
         repository.storeCategory(category = intent.category)
    }
}

问题原因

核心是混用了Room的@Relation注解和手动JOIN查询。@Relation是Room专门用来处理一对多关联的机制,它会自动执行两次查询:先查询父实体列表,再根据每个父实体的主键批量查询对应的子实体。

而你手动写了LEFT JOIN的SQL,返回的结果集结构和Room期望的@Embedded+@Relation映射逻辑不兼容:JOIN会返回包含父、子实体所有字段的组合行,当一个父实体对应多个子实体时,父实体的字段会被重复返回。Room无法从这种混合结果集中正确识别父实体的主键值,最终导致解析出的FinancialCategory的ID为默认值0。

解决方案

方案一:移除手动JOIN,让Room自动处理关联(推荐)

修改DAO的查询方法,只查询父实体,关联逻辑交给@Relation自动处理:

@Transaction
@Query("SELECT * FROM FinancialCategories WHERE parent = (SELECT id FROM FinancialCategories WHERE name = :categoryName)")
fun getCategoriesWithItems(categoryName: String): Flow<List<CategoryWithRecords>>

这样Room会先执行该查询拿到符合条件的所有FinancialCategory,再自动根据每个category的id去FinancialRecords表中查询对应的records,完全匹配@Embedded+@Relation的设计逻辑,能正确解析父实体的ID。

方案二:手动JOIN时自定义映射类(适合特殊场景)

如果必须用手动JOIN,不要使用@Relation,而是自定义包含所有所需字段的数据类,手动处理结果映射:

  1. 创建映射类:
data class CategoryRecordJoin(
    @ColumnInfo(name = "category_id") val categoryId: Long,
    @ColumnInfo(name = "category_name") val categoryName: String,
    @ColumnInfo(name = "parent") val parent: Long,
    @ColumnInfo(name = "record_id") val recordId: Long?,
    @ColumnInfo(name = "title") val title: String?,
    @ColumnInfo(name = "amount") val amount: BigDecimal?,
    @ColumnInfo(name = "category") val category: Long?
)
  1. 修改DAO查询SQL,指定字段别名避免冲突:
@Query("SELECT fc.id as category_id, fc.name as category_name, fc.parent, fr.id as record_id, fr.title, fr.amount, fr.category " +
       "FROM FinancialCategories fc " +
       "LEFT JOIN FinancialRecords fr ON fc.id = fr.category " +
       "WHERE fc.parent = (SELECT id FROM FinancialCategories WHERE name = :categoryName)")
fun getCategoryRecordJoins(categoryName: String): Flow<List<CategoryRecordJoin>>
  1. 在仓库层手动分组结果:
fun getFinancialData(category: FinancialCategories): Flow<Map<FinancialCategory, List<FinancialRecord>>> {
    return categoryDao.getCategoryRecordJoins(category.displayName).map { joins ->
        joins.groupBy(
            keySelector = { join ->
                FinancialCategory(
                    id = join.categoryId,
                    name = join.categoryName,
                    parent = join.parent
                )
            },
            valueTransform = { joinsForCategory ->
                joinsForCategory.mapNotNull { join ->
                    join.recordId?.let {
                        FinancialRecord(
                            id = it,
                            title = join.title!!,
                            amount = join.amount!!,
                            category = join.category!!
                        )
                    }
                }
            }
        )
    }
}

额外验证点

确认存储环节的storeCategory方法正确:DAO的插入方法应该用@Insert(onConflict = OnConflictStrategy.REPLACE)并返回Long类型的ID,确保插入后的实体ID被正确赋值,避免后续查询时ID为默认值0。


内容的提问来源于stack exchange,提问作者WinterMute

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 08:29:56