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,而是自定义包含所有所需字段的数据类,手动处理结果映射:
- 创建映射类:
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? )
- 修改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>>
- 在仓库层手动分组结果:
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

