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.
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.
Just don't use the same class for your Entity and the Place that you fetch from network.
It's smell bad to tie your class logic with API structure.
When you're retrieving data from network, just create new Places entities and persist it to the DB.
Related
error: Cannot find getter for field.
private final com.kbb.webviewolacakmi.model.content icerik = null;
I didn't manage to add the subparts of the json to the room.
Thanks to everyone who helped.
I would be very happy if you could write a clear code example.
Json File :
{
"date": "xxx",
"title": {
"rendered": "Title"
},
"content": {
"rendered": "content",
"protected": false
},
}
Data Class :
#Entity
data class Icerik(
#ColumnInfo(name="title")
#SerializedName("title")
val baslik:title?,
#ColumnInfo(name="content")
#SerializedName("content")
public val icerik:content?,
#ColumnInfo(name="date")
#SerializedName("date")
val tarih:String?,
#ColumnInfo(name="jetpack_featured_media_url")
#SerializedName("jetpack_featured_media_url")
val gorsel:String?,) {
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
fun getIcerik(){
}
}
data class content(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
public val content: String?,
#ColumnInfo(name="protected")
#SerializedName("protected")
val bool: Boolean?,
){
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
}
data class title(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
val ytitle:String?
){
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
}
IcerikDatabase Class
#TypeConverters(value = [RoomTypeConverters::class])
#Database(entities = arrayOf(Icerik::class), version = 1)
abstract class IcerikDatabase:RoomDatabase() {
abstract fun icerikDao(): IcerikDAO
companion object {
#Volatile private var instance:IcerikDatabase? = null
private val lock=Any()
operator fun invoke(context: Context)= instance?: synchronized(lock){
instance?: databaseOlustur(context).also {
instance=it
}
}
private fun databaseOlustur(context: Context) = Room.databaseBuilder(
context.applicationContext, IcerikDatabase::class.java,
"icerikdatabase"
).build()
}
}
IcerikDao
interface IcerikDAO {
#Insert
suspend fun instertAll(vararg icerik:Icerik):List<Long>
#Query("SELECT * FROM icerik")
suspend fun getAllIcerik():List<Icerik>
#Query("SELECT * FROM icerik WHERE uuid=:icerikId ")
suspend fun getIcerik(icerikId:Int):Icerik
#Query("DELETE FROM icerik")
suspend fun deleteAllIcerik()
}
TypeConverter
class RoomTypeConverters {
#TypeConverter
fun fromTitleToJSONString(title: title?): String? {
return Gson().toJson(title)
}
#TypeConverter
fun toTitleFromJSONString(jsonString: String?): title? {
return Gson().fromJson(jsonString, title::class.java)
}
#TypeConverter
fun fromIcerikToJSONString(content: content?): String? {
return Gson().toJson(content)
}
#TypeConverter
fun toIcrerikFromJSONString(jsonString: String?): content? {
return Gson().fromJson(jsonString, content::class.java)
}
}
I believe that your issue is in regard, not to room, but with the JSON handling.
The JSON file that you have shown cannot directly build an Icerik object.
Rather you need to have an intermediate class that can be built with the JSON and then use that intermediate class to then build the IceRik object.
So you want an intermediate class something along the lines of:-
data class JsonIceRik(
val content: content,
val title: title,
val date: String
)
If the JSON is then amended to be:-
val myjson = "{\"date\": \"xxx\",\"title\": {\"rendered\": \"Title\"},\"content\": {\"rendered\": \"content\",\"protected\": false}}"
note the omission of the comma between the two closing braces
Then you could use:-
val m5 = Gson().fromJson(myjson,JsonIceRik::class.java)
To build the intermediate JsonIceRik object.
And then you could use:-
val i5 = Icerik(baslik = m5.title, icerik = m5.content, tarih = m5.date,gorsel = "whatever")
To build the Icerik from the intermediate JsonIceRik.
The result in the database would be:-
uuid in the title and content serve no purpose and will always be 0 if obtaining the data from JSON
the #PrimaryKey annotation only serves to introduce warnings -
A Table can only have 1 Primary Key ( a composite Primary Key can include multiple columns though (but you cannot use the #PrimaryKey annotation, you have to instead use the primarykeys parameter of the #Entity annotation) )
You might as well have :-
data class content(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
val content: String?,
#ColumnInfo(name="protected")
#SerializedName("protected")
val bool: Boolean?,
)
data class title(
#ColumnInfo(name="rendered")
#SerializedName("rendered")
val ytitle:String?
)
Otherwise, as you can see, the data according to the JSON has been correctly stored.
I have that Json that I would like to map with Moshi and store with Room
{
"name": "My Group",
"members": [
{
"id": "119075",
"invitedUser": {
"id": 97375,
"email": "xxx#gmail.com"
},
"inviting_user": {
"id": 323915,
"email": "yyy#gmail.com"
}
},
{
"id": "395387",
"invitedUser": {
"id": 323915,
"email": "aaa#gmail.com"
},
"inviting_user": {
"id": 323915,
"email": "bbb",
}
}
]
}
I prepared my models
#Entity(tableName = "groups")
data class Group(
#PrimaryKey
val id: Long,
val members: List<Member>
)
#Entity(tableName = "members")
data class Member(
#PrimaryKey
val id: Long,
#Json(name = "invited_user")
#ColumnInfo(name = "invited_user")
val invitedUser: User,
#Json(name = "inviting_user")
#ColumnInfo(name = "inviting_user")
val invitingUser: User
)
#Entity(tableName = "users")
data class User(
#PrimaryKey
val id: Int,
val email: String
)
And currently, I have error: Cannot figure out how to save this field into database.
I read this https://developer.android.com/training/data-storage/room/relationships. However, if I will model relationships like in documentation I don't know how to let Moshi map the relations? Have you found the simplest solution for that problem?
You have 2 options in my opinion.
You split the group and users in to individual tables and insert them separately.
You use TypeConverters to store the members as a field of group.
Your implementation is going to be dependent on your use-case.
Finally, I stored it by using TypeConverters
private val membersType = Types.newParameterizedType(List::class.java, Member::class.java)
private val membersAdapter = moshi.adapter<List<Member>>(membersType)
#TypeConverter
fun stringToMembers(string: String): List<Member> {
return membersAdapter.fromJson(string).orEmpty()
}
#TypeConverter
fun membersToString(members: List<Member>): String {
return membersAdapter.toJson(members)
}
And that are my models
#TypeConverters(Converters::class)
#Entity(tableName = "groups")
data class Group(
#PrimaryKey
val id: Long,
val name: String
) {
companion object {
data class Member(
val id: Long,
val invitedUser: User,
val invitingUser: User
)
data class User(
val id: Long,
val email: String
)
}
}
Does it look good for you?
Probably cleaner would be to have only ids and store somewhere else users, but I like that this solution is so simple.
You use TypeConverters to store the members as a field of group.
I believe this is the Implementation you need.
open class UserRequestConverter {
private val moshi = Moshi.Builder().build()
#TypeConverter
fun fromJson(string: String): User? {
if (TextUtils.isEmpty(string))
return null
val jsonAdapter = moshi.adapter(User::class.java)
return jsonAdapter.fromJson(string)
}
#TypeConverter
fun toJson(user: User): String {
val jsonAdapter = moshi.adapter(User::class.java)
return jsonAdapter.toJson(user)
}
}
#Entity(tableName = "members")
data class Member(
#PrimaryKey
val id: Long,
#Json(name = "invited_user")
#ColumnInfo(name = "invited_user")
#TypeConverters(UserRequestConverter::class)
val invitedUser: User,
#Json(name = "inviting_user")
#ColumnInfo(name = "inviting_user")
#TypeConverters(UserRequestConverter::class)
val invitingUser: User
)
I'm new to using Room on Android & I have a difficulty. I have a class named "Employee" with String name and Object EmployeeType which has it's attributes. I need to represent both in Room entity table, also to check for nulls as they could be null.
data class Employee(
#field:SerializedName("name")
val name: String? = null,
#field:SerializedName("employee_type")
val employeeType: EmployeeType? = null,
)
data class EmployeeType(
#field:SerializedName("full_staff")
val fullStaff: String? = null,
#field:SerializedName("contract_staff")
val contractStaff: String? = null
)
#Entity
class EmployeeTable (
#PrimaryKey(autoGenerate = true)
val id: Int,
#ColumnInfo(name = "name")
val name: String
//.. represent EmployeeType here for both full_staff and contract_staff
//.. also EmployeeTypes could be null, how do I handle null pointers
)
// My DOA
#Dao
interface EmployeeTableDao {
#Query("select * from EmployeeTable order by id DESC")
fun findAll(): LiveData<List<EmployeeTable>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(employeeTable: List<EmployeeTable>)
}
// API sample
{
"staff": [
{
"name": "Jeff",
"employee_type": {
"full_staff": "No",
"contract_staff": "Yes"
}
},
//...
],
}
Please how do I achieve this?
For storing employee data in the same table and later to access the same in Kotlin, create a variable of type EmployeeType in EmployeeTable entity class and create a TypeConvertor and register it to the Database class. The TypeConvertor will convert the EmployeeType to JSON string using GSON to store in DB and back to EmployeeType when accessed from DB.
#Entity
class EmployeeTable (
#PrimaryKey(autoGenerate = true)
val id: Int,
#ColumnInfo(name = "name")
val name: String
#ColumnInfo(name = "employee_type")
val emmployeeType: EmployeeType
)
Convertor class
class EmployeeTypeConvertor {
#TypeConverter
fun fromEmployeeType(employeeType: EmployeeType): String {
return /* JSON string using GSON or something */
}
#TypeConverter
fun toEmployeeType(employeeType: String): EmployeeType {
return /* EmployeeType from JSON string using GSON or something */
}
}
I've followed the GithubBrowserSample from Google to get started with Android Architecture Components and Retrofit. Everything works fine but I have troubles in my own data model because of foreign keys.
Let's say I have a place :
#Entity(tableName = "place",
foreignKeys = [
ForeignKey(entity = User::class, parentColumns = ["user_id"], childColumns = ["place_created_by_user_id"])
],
indices = [
Index(value = ["place_created_by_user_id"], name = "place_created_by_user_index")
])
data class Place(
#SerializedName("id")
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "place_id")
var id: Long,
#SerializedName("name")
#ColumnInfo(name = "place_name")
var name: String?,
#SerializedName("created_by_user_id")
#ColumnInfo(name = "place_created_by_user_id")
var createdByUserId: Long?,
)
And a user :
#Entity(tableName = "user")
data class User(
#SerializedName("id")
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "user_id")
var id: Long,
#SerializedName("first_name")
#ColumnInfo(name = "user_first_name")
var firstName: String,
#SerializedName("last_name")
#ColumnInfo(name = "user_last_name")
var lastName: String,
)
Following the sample of Google, the method to fetch the places in the repository is :
fun loadPlaces(): LiveData<Resource<List<Place>>> {
return object : NetworkBoundResource<List<Place>, List<Place>>(appExecutors) {
override fun saveCallResult(item: List<Place>) {
placeDao.insert(item)
}
override fun shouldFetch(data: List<Place>?): Boolean = true
override fun loadFromDb() = placeDao.getAll()
override fun createCall() = service.getPlaces()
override fun onFetchFailed() {
//repoListRateLimit.reset(owner)
}
}.asLiveData()
}
So normally, it would simply work (I tried with an entity without foreign key) but it failed because of the foreign constraint :
android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787)
Indeed, the user is not loaded yet.
So before placeDao.insert(item), I have to load each users to make sure the place will find his user. And it's the same for each entities and each foreign keys.
Any ideas of how can I achieve this following this architecture?
The point is when I call loadPlaces() in my ViewModel like this :
class PlacesViewModel(application: Application) : BaseViewModel(application) {
val places: LiveData<Resource<List<Place>>> = repository.loadPlaces()
}
The repository would intrinsically load the users attached to the places...
Thanks for your help.
I'm trying to load entity sublist, but i would like to avoid to do 2 queries.
I'm thinking about do query inside TypeConverter, but i really don't know if it's good idea.
My entities:
#Entity
class Region(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
var name: String = "",
var locales: List<Locale> = listOf())
#Entity(foreignKeys = arrayOf(ForeignKey(
entity = Region::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("regionId"),
onDelete = CASCADE,
onUpdate = CASCADE
)))
class Locale(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
var regionId: Int = 0,
var name: String = "")
DAOs :
#Dao
interface RoomRegionDao{
#Insert
fun insert(region: Region)
#Delete
fun delete(region: Region)
#Query("select * from region")
fun selectAll(): Flowable<List<Region>>
}
#Dao
interface RoomLocaleDao{
#Insert
fun insert(locale: Locale)
#Query("select * from locale where regionId = :arg0")
fun selectAll(regionId: Int): List<Locale>
}
Database:
#Database(entities = arrayOf(Region::class, Locale::class), version = 1)
#TypeConverters(RoomAppDatabase.Converters::class)
abstract class RoomAppDatabase : RoomDatabase() {
abstract fun regionDao(): RoomRegionDao
abstract fun localeDao(): RoomLocaleDao
inner class Converters {
#TypeConverter
fun toLocales(regionId: Int): List<Locale> {
return localeDao().selectAll(regionId)
}
#TypeConverter
fun fromLocales(locales: List<Locale>?): Int {
locales ?: return 0
if (locales.isEmpty()) return 0
return locales.first().regionId
}
}
}
It's not working because can't use inner class as converter class.
Is it a good way?
How can I load "locales list" automatically in region entity when I do RoomRegionDao.selectAll?
I think that TypeConverter only works for static methods.
I say this based on the example from here and here
From the relationships section here:
"Because SQLite is a relational database, you can specify relationships between objects. Even though most ORM libraries allow entity objects to reference each other, Room explicitly forbids this."
So I guess it's best if you add #Ignore on your locales property and make a method on RoomLocaleDao thats insert List<Locale> and call it after insert Region.
The method that inserts a Region can return the inserted id.
If the #Insert method receives only 1 parameter, it can return a long, which is the new rowId for the inserted item. If the parameter is an array or a collection, it should return long[] or List instead.
(https://developer.android.com/topic/libraries/architecture/room.html#daos-convenience)
You can get all your Regions wit a list of Locales inside each Region just with one Query.
All you need to do is create a new Relation like this:
data class RegionWithLocale(
#Embedded
val region: Region,
#Relation(
parentColumn = "id",
entity = Locale::class,
entityColumn = "regionId"
)
val locales: List<Locale>
)
And latter get that relation in your Dao like this:
#Query("SELECT * FROM region WHERE id IS :regionId")
fun getRegions(regionId: Int): LiveData<RegionWithLocale>
#Query("SELECT * FROM region")
fun getAllRegions(): LiveData<List<RegionWithLocale>>
This solution worked for me
ListConvert.kt
class ListConverter
{
companion object
{
var gson = Gson()
#TypeConverter
#JvmStatic
fun fromTimestamp(data: String?): List<Weather>?
{
if (data == null)
{
return Collections.emptyList()
}
val listType = object : TypeToken<List<String>>() {}.type
return gson.fromJson(data, listType)
}
#TypeConverter
#JvmStatic
fun someObjectListToString(someObjects: List<Weather>?): String?
{
return gson.toJson(someObjects)
}
}
}
The property inside of my Entity
#SerializedName("weather")
#TypeConverters(ListConverter::class)
val weather: List<Weather>,