与房间和房间的多对多关系LiveData

时间:2018-06-11 13:49:48

标签: android retrofit rx-java2 android-room android-livedata

我有一个休息api,它返回一个地方列表,其中包含一个类别列表:

{
      "id": "35fds-45sdgk-fsd87",
      "name" : "My awesome place",
       "categories" : [
          {
            "id": "cat1",
            "name" : "Category 1"
          },
          {
            "id": "cat2",
            "name" : "Category 2"
          },
          {
            "id": "cat3",
            "name" : "Category 3"
          }
       ]
}

因此,使用改造我从远程服务器获取这些模型类:

data class Category(var id: String, var name: String)

data class Place(
  var id: String,
  var name: String,
  var categories: List<Category>
)

问题是 - 我希望viewModel始终从返回Flowables的本地Room数据库中检索,并且只触发将更新数据库以及视图的刷新操作。

DAO方法示例:

@Query("select * from Places where placeId = :id")
fun getPlace(id: String): Flowable<Place>

所以我尝试对这两个类进行建模:

@Entity
data class Category(var id: String, var name: String)


@Entity
data class Place(
  @PrimaryKey
  var id: String,
  var name: String,
  var categories: List<Category>
)

但当然,Room无法自行处理关系。我看过this post只是从本地数据库中检索了以前的城市列表,但是这种情况与那个不匹配。

我能想到的唯一选择是将数据库中的类别保存为JSON字符串,但这会失去数据库的关系质量......

这似乎是一个非常常见的用例,但我没有找到很多关于它的信息。

3 个答案:

答案 0 :(得分:6)

在Room中有可能有多对多的关系。

首先向@Ignore课程添加Place注释。它将告诉Room忽略此属性,因为它无法保存没有转换器的对象列表。

data class Category(
        @PrimaryKey var id: String, 
        var name: String
)

data class Place(
        @PrimaryKey var id: String,
        var name: String,
        @Ignore var categories: List<Category>
) 

然后创建一个表示这两个类之间连接的类。

@Entity(primaryKeys = ["place_id", "category_id"],
        indices = [
            Index(value = ["place_id"]),
            Index(value = ["category_id"])
        ],
        foreignKeys = [
            ForeignKey(entity = Place::class,
                    parentColumns = ["id"],
                    childColumns = ["place_id"]),
            ForeignKey(entity = Category::class,
                    parentColumns = ["id"],
                    childColumns = ["category_id"])
        ])
data class CategoryPlaceJoin(
        @ColumnInfo(name = "place_id") val placeId: String,
        @ColumnInfo(name = "category_id") val categoryId: String
)

如你所见,我使用了外键。

现在,您可以指定特殊的DAO来获取某个地点的类别列表。

@Dao
interface PlaceCategoryJoinDao {

    @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
    @Query("""
        SELECT * FROM category INNER JOIN placeCategoryJoin ON
        category.id = placeCategoryJoin.category_id WHERE
        placeCategoryJoin.place_id = :placeId
        """)
    fun getCategoriesWithPlaceId(placeId: String): List<Category>

    @Insert
    fun insert(join: PlaceCategoryJoin)
}

最后一件重要的事情是每次插入新的Place时插入连接对象。

val id = placeDao().insert(place)
for (place in place.categories) {
   val join = CategoryPlaceJoin(id, category.id)
   placeCategoryJoinDao().insert(join)
}

现在当你从placeDao()获得地点时,他们有空的类别列表。要添加类别,您可以使用以下代码:

fun getPlaces(): Flowable<List<Place>> {
    return placeDao().getAll()
            .map { it.map { place -> addCategoriesToPlace(place) } }
}

private fun addCategoriesToPlace(place: Place): Place {
    place.categories = placeCategoryJoinDao().getCategoriesWithPlaceId(place.id)
    return place
}

要查看更多详细信息,请参阅this article

答案 1 :(得分:0)

不要为您从网络中提取的EntityPlace使用相同的课程。

将类逻辑与API结构联系起来很难闻。

当您从网络中检索数据时,只需创建新的地方实体并将其保存到数据库。

答案 2 :(得分:0)

我有一个类似的用例。由于Room没有管理关系,我最后根据你提到的博客找到了这个解决方案:/

@Entity
data class Category(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    var catId: String, 
    var name: String,
    @ForeignKey(entity = Place::class, parentColumns = ["id"], childColumns = ["placeId"], onDelete = ForeignKey.CASCADE)
    var placeId: String = ""
)

@Entity
data class Place(
    @PrimaryKey
    var id: String,
    var name: String,
    @Ignore var categories: List<Category>
)

PlaceDao

@Dao
interface PlaceDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(place: Place)

    @Query("SELECT * FROM place WHERE id = :id")
    fun getPlace(id: String?): LiveData<Place>
}

fun AppDatabase.getPlace(placeId: String): LiveData<Place> {
    var placeLiveData = placeDao().getPlace(placeId)
    placeLiveData = Transformations.switchMap(placeLiveData, { place ->
        val mutableLiveData = MutableLiveData<Place>()
        Completable.fromAction { // cannot query in main thread
            place.categories = categoryDao().get(placeId)
        }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { mutableLiveData.postValue(place) }
        mutableLiveData
    })
    return placeLiveData
}

// run in transaction
fun AppDatabase.insertOrReplace(place: Place) {
    placeDao().insert(place)
    place.categories?.let {
        it.forEach {
            it.placeId = place.id
        }
        categoryDao().delete(place.id)
        categoryDao().insert(it)
    }
}

CategoryDao

@Dao
interface CategoryDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(categories: List<Category>)

    @Query("DELETE FROM category WHERE placeId = :placeId")
    fun delete(placeId: String?)

    @Query("SELECT * FROM category WHERE placeId = :placeId")
    fun get(placeId: String?): List<Category>
}

不是一个大粉丝,但我目前还没有找到更好的方法。