如何使用Room Relationships存储包含Books的Lists复杂对象?
解决Room一对多关系存储问题的正确实现方式
1. 调整实体类,建立外键关联
Room不支持在实体中直接嵌套对象列表,必须将Lists和Books拆分为独立实体,通过外键建立关联:
修改后的Lists实体
@Entity(tableName = "lists_table") data class Lists( @PrimaryKey @ColumnInfo(name = "display_name") @Json(name = "display_name") val displayName: String )
调整后的Books实体(添加外键)
注意:原代码用author作为主键存在逻辑漏洞(同一作者可能有多本不同书籍),这里改用author+title作为联合主键避免重复,同时新增外键字段关联对应List:
@Entity( tableName = "books_table", foreignKeys = [ ForeignKey( entity = Lists::class, parentColumns = ["display_name"], childColumns = ["list_display_name"], onDelete = ForeignKey.CASCADE // 删除List时自动删除关联的Books ) ], primaryKeys = ["author", "title"] ) data class Books( @ColumnInfo(name = "author") val author: String, @ColumnInfo(name = "book_image") @Json(name = "book_image") val bookImage: String, @ColumnInfo(name = "width") @Json(name = "book_image_width") val imageWidth: Int, @ColumnInfo(name = "height") @Json(name = "book_image_height") val imageHeight: Int, @ColumnInfo(name = "contributor") val contributor: String, @ColumnInfo(name = "description") val description: String, @ColumnInfo(name = "publisher") val publisher: String, @ColumnInfo(name = "rank") val rank: Int, @ColumnInfo(name = "rank_last_week") @Json(name = "rank_last_week") val rankLastWeek: Int, @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "weeks_on_list") @Json(name = "weeks_on_list") val weeksOnList: Int, // 新增外键字段,关联所属List的displayName @ColumnInfo(name = "list_display_name") val listDisplayName: String )
2. 创建关系类,实现一对多关联查询
通过Room的@Relation注解,定义包含Books的List包装类:
data class ListWithBooks( @Embedded val list: Lists, @Relation( parentColumn = "display_name", entityColumn = "list_display_name" ) val books: List<Books> )
3. 编写Dao接口,实现数据操作
在Dao中添加插入和关联查询方法,注意关联查询必须加上@Transaction保证原子性:
@Dao interface LibraryDao { // 插入List,忽略重复数据 @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertList(list: Lists) // 插入单本Book,忽略重复数据 @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertBook(book: Books) // 批量插入Books @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertBooks(books: List<Books>) // 查询所有包含Books的List @Transaction @Query("SELECT * FROM lists_table") suspend fun getAllListsWithBooks(): List<ListWithBooks> // 根据displayName查询指定List及其关联的Books @Transaction @Query("SELECT * FROM lists_table WHERE display_name = :displayName") suspend fun getListWithBooks(displayName: String): ListWithBooks? }
4. 插入数据的注意事项
插入时需先插入List,再插入关联的Books(避免外键约束报错):
// 示例插入逻辑 suspend fun addListAndBooks(list: Lists, books: List<Books>) { // 给每本Book添加上所属List的关联字段 val linkedBooks = books.map { it.copy(listDisplayName = list.displayName) } libraryDao.insertList(list) libraryDao.insertBooks(linkedBooks) }
关键要点
- 必须遵循数据库范式拆分实体,不能在实体中直接嵌套对象列表
- 主键选择要符合业务逻辑,避免用可能重复的字段作为单一主键
- 关联查询必须添加
@Transaction,确保查询过程中数据不会发生变更
内容的提问来源于stack exchange,提问作者John Njuki
相关产品推荐
相关产品推荐

