Android 房间:交叉引用不止一次或有计数
Android Room: cross-referencing more than once or with a count
在我的应用程序中,我有 NutritionDate
和 FoodItems
的概念。
@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)
}
}
在我的应用程序中,我有 NutritionDate
和 FoodItems
的概念。
@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)
}
}