Android Room 如何实现单日关联多膳食、多饮水记录的表关系配置
Android Room 一对多关联数据库实现方案
Room是关系型数据库,不支持直接存储嵌套数组结构,需要将你预期的嵌套结构拆分为3张独立表,通过外键做关联,再通过Room提供的@Relation注解实现嵌套对象的自动映射,完全匹配你需要的返回结构。
1. 实体类定义(对应3张数据库表)
- Day表实体:存储每日基础数据,对
date字段加唯一约束,避免同一天生成重复记录
@Entity( tableName = "days", indices = [Index(value = ["date"], unique = true)] ) data class Day( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo(name = "date") val date: String, @ColumnInfo(name = "weight") val weight: Float? = null, @ColumnInfo(name = "height") val height: Int? = null, @ColumnInfo(name = "waist_circumference") val waistCircumference: Int? = null, @ColumnInfo(name = "neck_circumference") val neckCircumference: Int? = null, @ColumnInfo(name = "hip_circumference") val hipCircumference: Int? = null, @ColumnInfo(name = "steps") val steps: Long = 0, @ColumnInfo(name = "taken_kcal") val takenKcal: Float = 0f, @ColumnInfo(name = "burned_kcal") val burnedKcal: Float = 0f, @ColumnInfo(name = "carb") val carb: Float = 0f, @ColumnInfo(name = "protein") val protein: Float = 0f, @ColumnInfo(name = "fat") val fat: Float = 0f, @ColumnInfo(name = "water_ml") val waterMl: Int = 0, @ColumnInfo(name = "water_percent") val waterPercent: Float = 0f )
- DailyMeal表实体:存储每日膳食记录,通过
day_id外键关联Day表,配置级联删除规则,删除某天记录时自动删除对应膳食数据,外键字段必须加索引避免性能问题
@Entity( tableName = "meals", foreignKeys = [ ForeignKey( entity = Day::class, parentColumns = ["id"], childColumns = ["day_id"], onDelete = ForeignKey.CASCADE ) ], indices = [Index("day_id")] ) data class DailyMeal( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo(name = "day_id") val dayId: Int, @ColumnInfo(name = "food_meal_time") val foodMealTime: String, @ColumnInfo(name = "food_name") val foodName: String, @ColumnInfo(name = "food_image") val foodImage: String, @ColumnInfo(name = "food_kcal") val foodKcal: Float, @ColumnInfo(name = "food_portion") val foodPortion: String, @ColumnInfo(name = "food_carb") val foodCarb: Float, @ColumnInfo(name = "food_protein") val foodProtein: Float, @ColumnInfo(name = "food_fat") val foodFat: Float )
- DailyWater表实体:存储每日饮水记录,外键关联规则和膳食表一致
@Entity( tableName = "water", foreignKeys = [ ForeignKey( entity = Day::class, parentColumns = ["id"], childColumns = ["day_id"], onDelete = ForeignKey.CASCADE ) ], indices = [Index("day_id")] ) data class DailyWater( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo(name = "day_id") val dayId: Int, @ColumnInfo(name = "water_ml") val waterMl: Float, @ColumnInfo(name = "water_percent") val waterPercent: Float, @ColumnInfo(name = "water_time") val waterTime: Long )
2. 定义关联映射类
单独定义关系映射类,通过@Embedded和@Relation注解实现自动关联查询,返回结构和你预期的嵌套JSON完全一致,不需要手动做数据转换:
data class DayWithRelations( @Embedded val day: Day, @Relation( parentColumn = "id", entityColumn = "day_id" ) val meals: List<DailyMeal>, @Relation( parentColumn = "id", entityColumn = "day_id" ) val water: List<DailyWater> )
3. DAO接口定义
@Dao interface HealthDao { // 插入Day记录,冲突策略为IGNORE,已存在同日记录时直接跳过,不会重复创建 @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertDayIfNotExist(day: Day) // 根据日期查询对应日期的全量关联数据,必须加@Transaction保证数据一致性 @Transaction @Query("SELECT * FROM days WHERE date = :date") suspend fun getDayByDate(date: String): DayWithRelations? // 新增膳食记录 @Insert suspend fun insertMeal(meal: DailyMeal) // 新增饮水记录 @Insert suspend fun insertWater(water: DailyWater) // 更新Day表的统计字段(总摄入、总饮水量、步数等) @Update suspend fun updateDay(day: Day) }
4. 业务逻辑实现
- 应用启动自动创建当日记录:在应用启动协程中执行以下逻辑,重复启动也不会生成重复数据
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) val todayStr = dateFormat.format(Date()) healthDao.insertDayIfNotExist(Day(date = todayStr))
- 日期切换逻辑:用户切换日期时,先调用
getDayByDate传入格式化后的目标日期字符串,如果返回null说明该日期无记录,先插入一条空的对应日期Day记录,再查询返回即可。 - 新增膳食/饮水逻辑:获取当前选中日期对应的Day主键id,构造
DailyMeal/DailyWater对象时将dayId赋值为该id,调用对应插入方法即可;插入完成后重新统计当日的总卡路里、三大营养素占比、总饮水量等数据,更新Day表对应统计字段。
注意事项
- 日期存储必须统一格式,建议固定使用
yyyy-MM-dd字符串格式,方便排序、比较和查询,不要混用不同格式。 - 所有数据库操作必须在协程或后台线程执行,禁止在主线程直接调用,否则会触发Room的主线程访问异常。
- 新增、删除膳食或饮水记录后,必须同步更新Day表的统计字段,保证首页展示的汇总数据准确。
内容的提问来源于stack exchange,提问作者user18504539
相关产品推荐
相关产品推荐

