与 Room 和 LiveData 的多对多关系
Many to Many relations with Room & LiveData
我休息api那个returns一个地方列表,里面有一个类别列表:
{
"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 字符串,但这会失去数据库的关系质量...
这似乎是一个很常见的用例,但我还没有找到太多关于它的信息。
只是不要对您的 Entity
和从网络获取的 Place
使用相同的 class。
将您的 class 逻辑与 API 结构联系起来很难闻。
当您从网络中检索数据时,只需创建新的 Places 实体并将其保存到数据库中。
我有一个类似的用例。由于 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>
}
不太喜欢,但我暂时没有找到更好的方法。
在 Room 中可以有多对多的关系。
首先将 @Ignore
注释添加到您的 Place
class。它会告诉 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>
)
然后创建一个 class 来代表这两个 classes 之间的联系。
@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。
我休息api那个returns一个地方列表,里面有一个类别列表:
{
"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 字符串,但这会失去数据库的关系质量...
这似乎是一个很常见的用例,但我还没有找到太多关于它的信息。
只是不要对您的 Entity
和从网络获取的 Place
使用相同的 class。
将您的 class 逻辑与 API 结构联系起来很难闻。
当您从网络中检索数据时,只需创建新的 Places 实体并将其保存到数据库中。
我有一个类似的用例。由于 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>
}
不太喜欢,但我暂时没有找到更好的方法。
在 Room 中可以有多对多的关系。
首先将 @Ignore
注释添加到您的 Place
class。它会告诉 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>
)
然后创建一个 class 来代表这两个 classes 之间的联系。
@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。