Android 房间:交叉引用不止一次或有计数

Android Room: cross-referencing more than once or with a count

在我的应用程序中,我有 NutritionDateFoodItems 的概念。

@Entity
data class NutritionDateEntity(
    @PrimaryKey val epochDay: Long,
    val year: Int,
    val month: Int,
)
@Entity
data class FoodItemEntity(
    @PrimaryKey
    val createdAt: Long = Instant.now().toEpochMilli(), // guaranteed to be unique, ascending
    val forEpochDay: Long = 0L,
    val name: String = "",
    val fact: String = ""
)

我还有一个交叉引用 table 来跟踪哪些 FoodItem 包含在哪些 NutritionDate 中:

@Entity(primaryKeys = ["epochDay", "foodItemCreatedAt"])
data class NutritionDateAndFoodItemCrossRefEntity(
    val epochDay: Long,
    val foodItemCreatedAt: Long,
)

我遇到了一个问题,即我无法多次将某项食物添加到列表中(比如我吃了两个墨西哥卷饼,想添加两次)。

我想在交叉引用 table 中添加一个 count 属性,或者尝试进行某种查找 table 以进行交叉引用 + 计数,但是交叉引用条目的主键是复合的。也许尝试向交叉引用条目添加一个自动递增的主键以使其唯一?也找不到任何关于这样做的好信息。

如果可能的话,我想以惯用的方式来做这件事。有没有一种好的/标准的方法可以用 Room 做到这一点?

您可以使用任何一种方式。但是,您可能更愿意将数量列添加到 NutritionDateAndFoodItemCrossRefEntity。如果试图确定数量(不需要 POJO 或单独的函数和具有 GROUP BY 子句的查询、计数表达式(POJO(带有 field/member/variable 用于计数)或单独的函数来获取计数))。

but the primary key for the cross-reference entries are composite.

您应该知道组成复合键的值,所以这应该不是问题。

您不妨考虑为复合键的第二列添加索引。您可能还希望添加 ForeignKey 子句以帮助确保参照完整性(无孤儿)。因此,也许可以考虑:-

@Entity(
    primaryKeys = ["epochDay","foodItemCreatedAt"],
    /* Room will issue warning if no index on foodItemCreatedAt */
    /* MUST NOT BE unique index */
    indices = [
        Index(value = ["foodItemCreatedAt"])
              ],
    /* Adding Foreign Key Definitions will enforce referential integrity */
    foreignKeys = [
        ForeignKey(
            entity = NutritionDateEntity::class,
            parentColumns = ["epochDay"],
            childColumns = ["epochDay"],
            /* Optional what to do if a parent is deleted/updated */
            /* CASCADE (the most commonly used option) propogates the action to the children */
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = FoodItemEntity::class,
            parentColumns = ["createdAt"],
            childColumns = ["foodItemCreatedAt"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class NutritionDateAndFoodItemCrossRefEntity(
    val epochDay: Long,
    val foodItemCreatedAt: Long,
    val quantity: Long
)

你@Dao class 可以,例如,be/include:-

@Dao
abstract class NutritionDao {

    @Insert
    abstract fun insert(nutritionDateEntity: NutritionDateEntity): Long
    @Insert
    abstract fun insert(foodItemEntity: FoodItemEntity): Long
    /* IGNORE if conflict (duplicate) used for insertOrInCrement below*/
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract fun insert(nutritionDateAndFoodItemCrossRefEntity: NutritionDateAndFoodItemCrossRefEntity): Long
    @Query("UPDATE nutritiondateandfooditemcrossrefentity SET quantity = quantity + :adjustment WHERE epochDay=:epochDay AND foodItemCreatedAt=:foodItemCreatedAt")
    abstract fun adjustNutritionAndFoodItemCrossRefEntity(adjustment: Long, epochDay: Long, foodItemCreatedAt: Long)

    /* Signature that uses the actual objects */
    fun insertOrIncrementNutritionFoodItemCrossRefEntity(nutritionDateEntity: NutritionDateEntity, foodItemEntity: FoodItemEntity) {
        if (insert(NutritionDateAndFoodItemCrossRefEntity(nutritionDateEntity.epochDay,foodItemEntity.createdAt,1)) < 1) {
            adjustNutritionAndFoodItemCrossRefEntity(1,nutritionDateEntity.epochDay,foodItemEntity.createdAt)
        }
    }

    /* Signature that uses the values */
    fun insertOrIncrementNutritionFoodItemCrossRefEntity(epochDay: Long, createdAt: Long) {
        if (insert(NutritionDateAndFoodItemCrossRefEntity(epochDay,createdAt,1)) < 1) {
            adjustNutritionAndFoodItemCrossRefEntity(1,epochDay,createdAt)
        }
    }
}
  • 请注意,使用抽象 class 而不是接口,这允许具有主体的非抽象函数,因此 insertOrIncrement... 函数

演示

考虑到上述内容和您提供的其他代码,然后在 activity 中添加以下内容(运行 在 brevity/convenience 的主线程上):-

    db = TheDatabase.getInstance(this)
    nutritiondao = db.getNutritionDao()

    var f1crtd = System.currentTimeMillis()
    var f2crtd = f1crtd + 100
    var f3crtd = f2crtd + 100

    nutritiondao.insert(FoodItemEntity(forEpochDay = 1,name = "F1",fact = "Fact for F1",createdAt = f1crtd))
    nutritiondao.insert(FoodItemEntity(forEpochDay = 2,name = "F2", fact = "Fact for F2", createdAt = f2crtd))
    nutritiondao.insert(FoodItemEntity(forEpochDay = 3, name = "F3", fact = "Fact for F3", createdAt = f3crtd))

    nutritiondao.insert(NutritionDateEntity(10,2021,1))
    nutritiondao.insert(NutritionDateEntity(11,2021,1))

    nutritiondao.insertOrIncrementNutritionFoodItemCrossRefEntity(10,f1crtd)
    nutritiondao.insertOrIncrementNutritionFoodItemCrossRefEntity(10,f1crtd)
    nutritiondao.insertOrIncrementNutritionFoodItemCrossRefEntity(10,f2crtd)
    nutritiondao.insertOrIncrementNutritionFoodItemCrossRefEntity(10,f3crtd)
    nutritiondao.insertOrIncrementNutritionFoodItemCrossRefEntity(10,f3crtd)
    nutritiondao.insertOrIncrementNutritionFoodItemCrossRefEntity(10,f3crtd)

    /* Adjust example */
    nutritiondao.adjustNutritionAndFoodItemCrossRefEntity(-100,10,f2crtd)
  • 添加了 3 个 FoodItemEntity。
    • 正在指定和存储主键。
  • 然后是 2 个 NutritiondateEntity 的。
    • 正在指定和存储主键。
  • 第一个 FoodItem 被添加到交叉引用中。
  • 再次添加第一个 FoodItem(但不是因为检测到重复冲突,因此更新增加了原始的,所以现在数量是 2)
  • 第二个 FoodItem 添加到交叉引用(因此数量为 1)
  • 第三种食品添加了 3 次(因此数量最终为 3)
  • 第二个 FoodItem 调整为添加 -100,因此数量变为 -99。

根据 AndroidStudio 的 App Inspector(以前称为 Database Inspector)运行 完成以上操作后的数据库:-

在尝试了很多不同的策略之后,我最终通过一些我本应该首先尝试的相对简单的方法让它运行良好:

@Entity
data class NutritionDateEntity(
    @PrimaryKey 
    val epochDay: Long,
    val year: Int,
    val month: Int,
) 
@Entity
data class FoodItemEntity(
    @PrimaryKey
    val createdAt: Long = Instant.now().toEpochMilli(), // guaranteed to be unique, ascending
    val forEpochDay: Long = 0L,
    val name: String = "",
    val fact: String = ""
)
@Entity(
    foreignKeys = [
        ForeignKey(
            entity = NutritionDateEntity::class,
            parentColumns = arrayOf("epochDay"),
            childColumns = arrayOf("epochDay"),
            onDelete = CASCADE,
            onUpdate = CASCADE
        ),
        ForeignKey(
            entity = FoodItemEntity::class,
            parentColumns = arrayOf("createdAt"),
            childColumns = arrayOf("createdAt"),
            onDelete = CASCADE,
            onUpdate = CASCADE
        )
    ]
)
data class FoodEntryEntity( // formerly CrossRef
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val epochDay: Long,
    val createdAt: Long,
)

我没有使用复合主键作为交叉引用 table,而是决定将其表示为“食物条目”并为其提供自动递增 @PrimaryKey。当我查询 NutritionDates:

时,我仍然可以在 @Relation 注释中使用它来创建以下内容
data class NutritionDateWithFoodItems(
    @Embedded
    val nutritionDate: NutritionDateEntity,

    @Relation(
        parentColumn = "epochDay",
        entityColumn = "createdAt",
        associateBy = Junction(FoodEntryEntity::class)
    )
    val foodItems: List<FoodItemEntity>
)

还需要一个 upsert 实现来防止 @ForeignKey 在调用 @Insert(onConflictStrategy = REPLACE) 方法时删除级联(注意 REPLACE 现在是 IGNORE):

@Dao
interface FoodItemEntityDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(foodItemEntity: FoodItemEntity): Long

    @Update(onConflict = OnConflictStrategy.IGNORE)
    suspend fun update(foodItemEntity: FoodItemEntity)

    @Transaction
    suspend fun upsert(foodItemEntity: FoodItemEntity) {
        val result = insert(foodItemEntity)
        if (result == -1L) update(foodItemEntity)
    }
}