如何使用Room实现含嵌套结构的JSON API响应缓存?
解决Room存储嵌套列表与复杂实体的方案
Room本身不支持直接存储List类型和嵌套实体,针对你的场景,我们可以分两种情况处理:
1. 处理简单字符串列表(如keywords):使用TypeConverter
Room允许通过**类型转换器(TypeConverter)**将自定义类型(比如List<String>)转换为它支持的基本类型(比如String),存储时转成JSON字符串,读取时再解析回列表。
实现TypeConverter
import androidx.room.TypeConverter import com.google.gson.Gson import com.google.gson.reflect.TypeToken class ListTypeConverter { private val gson = Gson() @TypeConverter fun fromStringList(list: List<String>): String { return gson.toJson(list) } @TypeConverter fun toStringList(json: String): List<String> { val type = object : TypeToken<List<String>>() {}.type return gson.fromJson(json, type) } }
在Room数据库类中注册转换器
import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters @Database(entities = [AlbumEntity::class, PicEntity::class], version = 1) @TypeConverters(ListTypeConverter::class) abstract class AppDatabase : RoomDatabase() { abstract fun albumDao(): AlbumDao }
2. 处理嵌套实体列表(如pics):使用一对多关系
PicResponse包含自己的列表(emojis),所以需要将它定义为独立的Room实体,通过外键关联到对应的AlbumEntity,再用@Relation注解在查询时关联两个实体。
定义PicEntity实体
import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey @Entity( tableName = "pics", foreignKeys = [ ForeignKey( entity = AlbumEntity::class, parentColumns = ["id"], childColumns = ["albumId"], onDelete = ForeignKey.CASCADE // 当Album被删除时,关联的Pic也自动删除 ) ] ) data class PicEntity( @PrimaryKey(autoGenerate = true) val picId: Long = 0, // 给Pic加自增主键,避免picUrl重复导致的冲突 val albumId: String, // 关联Album的id val picUrl: String, val emojis: List<String> // 已通过TypeConverter支持存储 )
修改AlbumEntity实体
去掉原来的pics字段,保留核心字段:
import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "albums") data class AlbumEntity( @PrimaryKey(autoGenerate = false) val id: String, val title: String, val createdBy: String, val enabled: Boolean, val keywords: List<String> // 已通过TypeConverter支持 )
创建关联查询的数据类
为了一次性查询Album和它对应的所有Pic,定义包含@Relation的数据类:
import androidx.room.Relation data class AlbumWithPics( val album: AlbumEntity, @Relation( parentColumn = "id", entityColumn = "albumId" ) val pics: List<PicEntity> )
3. 实现DAO层
定义包含插入、查询操作的DAO,用@Transaction保证关联操作的原子性:
import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import androidx.room.Transaction @Dao interface AlbumDao { @Transaction suspend fun insertAlbumWithPics(album: AlbumEntity, pics: List<PicEntity>) { insertAlbum(album) insertPics(pics) } @Insert suspend fun insertAlbum(album: AlbumEntity) @Insert suspend fun insertPics(pics: List<PicEntity>) @Transaction @Query("SELECT * FROM albums") suspend fun getAllAlbumsWithPics(): List<AlbumWithPics> @Transaction @Query("SELECT * FROM albums WHERE id = :albumId") suspend fun getAlbumWithPicsById(albumId: String): AlbumWithPics? }
4. 数据转换:API响应 ↔ Room实体 ↔ 视图层数据类
在数据源层完成三次转换,隔离各层数据结构:
// API响应 → Room实体 fun AlbumResponse.toEntity(): AlbumEntity { return AlbumEntity( id = id, title = title, createdBy = createdBy, enabled = enabled, keywords = keywords ) } fun PicResponse.toEntity(albumId: String): PicEntity { return PicEntity( albumId = albumId, picUrl = picUrl, emojis = emojis ) } // Room查询结果 → 视图层纯数据类 fun AlbumWithPics.toDomain(): Album { return Album( id = album.id, title = album.title, createdBy = album.createdBy, enabled = album.enabled, keywords = album.keywords, pics = pics.map { it.toDomain() } ) } fun PicEntity.toDomain(): Pic { return Pic( picUrl = picUrl, emojis = emojis ) }
关键注意事项
- 若偏好
kotlinx.serialization替代Gson,只需修改TypeConverter的序列化/反序列化逻辑即可。 - 外键的
onDelete策略可根据业务调整,比如RESTRICT可阻止删除关联了Pic的Album。 - 插入关联数据必须用
@Transaction,避免出现Album插入成功但Pic插入失败的不一致情况。
内容的提问来源于stack exchange,提问作者Alan
相关产品推荐
相关产品推荐

