I have a rest api that returns a list of places, which have a list of categories:
{
"id": "35fds-45sdgk-fsd87",
"name" : "My awesome place",
"categories" : [
{
"id": "cat1",
"name" : "Category 1"
},
{
"id": "cat2",
"name" : "Category 2"
},
{
"id": "cat3",
"name" : "Category 3"
}
]
}
So using retrofit I get these from the remote server with these model classes:
data class Category(var id: String, var name: String)
data class Place(
var id: String,
var name: String,
var categories: List<Category>
)
Problem is -- I want the viewModel to always retrieve from a local Room Database returning Flowables and just trigger refresh actions that will update the database and thus the view.
DAO method example:
@Query("select * from Places where placeId = :id")
fun getPlace(id: String): Flowable<Place>
So I tried modeling those two classes like this:
@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>
)
But of course Room is not able to process relations on its own. I have seen this post which just retrieves from the local database the previous list of cities, but this case doesnt match that one.
Only option I could think of is to save the categories in the database as a JSON string but this is losing the relational quality of the database...
This seems like a pretty common use case but I haven't found much info about it.
A many-to-many relationship exists when one or more items in one table can have a relationship to one or more items in another table. For example: Your Order table contains orders placed by multiple customers (who are listed in the Customers table), and a customer may place more than one order.
When you need to establish a many-to-many relationship between two or more tables, the simplest way is to use a Junction Table. A Junction table in a database, also referred to as a Bridge table or Associative Table, bridges the tables together by referencing the primary keys of each data table.
Graphically, the many to many relationship is usually represented in a logical diagram with crow's foot notation. In a relational database, this relationship is then usually implemented using a join table, otherwise known as a junction or associative table with two one-to-many relationships.
Many to Many(M:N) Relationship Many to many relationships create uncertainty and duplications on data that will eventually result in wrong statements for queries(2). In the below example; Each person can use many banks and each bank can have many customers.
It's possible in Room to have many to many relationship.
First add @Ignore
annotation to your Place
class. It will tell Room to ignore this property, because it can't save the list of objects without converter.
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>
)
Then create a class that will represent the connection between this two 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
)
As you can see I used foreign keys.
Now you can specify special DAO for getting list of categories for a place.
@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)
}
And the last important thing is to insert join object each time you insert new Place
.
val id = placeDao().insert(place)
for (place in place.categories) {
val join = CategoryPlaceJoin(id, category.id)
placeCategoryJoinDao().insert(join)
}
Now when you get places from placeDao()
they have empty category list. In order to add categories you can use this part of code:
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
}
To see more details see this article.
I had a similar use case. As Room doesn't manage relations, I ended up with this solution following the blog you mentioned :/
@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>
}
Not a big fan but I didn't find a better way for the moment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With