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
)
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'm trying to make a post with retrofit and moshi but keep getting the error mess
com.squareup.moshi.JsonDataException: Expected BEGIN_OBJECT but was STRING at path $
I can't seem to understand why this is so. This is a sample of the json tested on postman:
{
"customerName": "Name",
"customerPhoneNo": "090000000",
"customerAddress": "Address",
"note": "Please",
"items" : [{
"productUid": "5633e1f1-8b00-46de-b73e-43799245a4e8",
"quantity" : "3"
},{
"ProductUid": "fca3ffb1-0130-4e47-b499-721d046c1e32",
"Quantity" : "5"
},
{
"ProductUid": "6a7f3e24-03ff-408a-b67e-8530d411390c",
"Quantity" : "2"
}]
}
My data classes are set up like so:
#Parcelize
data class Order(
val items: List<Item>?,
val customerName: String,
val customerPhoneNo: String,
val customerAddress: String,
val note: String
) : Parcelable
and
#Parcelize
data class Item(
val productUid: String,
var quantity: Int
) : Parcelable
Service utils look like:
interface ProductService {
#Headers("Content-Type: application/json")
#POST("/api/order/saveorder")
suspend fun postProds(#Body order: Order
): Response<Order>
#GET("/api/product/allproducts")
suspend fun getProds(): Response<List<ProdsItem>>
}
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
object Network {
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi)
.asLenient()
)
.build()
object ProdsApi {
val retrofitService: ProductService by lazy {
retrofit.create(ProductService::class.java)
}
}
}
The sendOrder function is set up like so:
suspend fun sendOrder(order: Order) {
withContext(Dispatchers.Main){
try {
val orderResponse = Network.ProdsApi.retrofitService.postProds(
order )
}
catch (e: Exception) {
Timber.e(e)
}
}
}
The GET request works perfectly.
Any help on this would be appreciated please.
In your Item Data Class you are using quantity as an Int but if you see the Postman JSON response it is a String.
So your class should be like:
data class Item(
#Json(name = "productUid")
val productUid: String?,
#Json(name = "quantity")
var quantity: String
)
Also as I see the key in your JSON response are written in two different ways.
For example your "Product ID" is written as "productUid" in one of the object and is written as "ProductUid" in another object.
So your complete Item Data Class should more look like this :
data class Item(
#Json(name = "productUid")
val productUid: String?,
#Json(name = "ProductUid")
val productUid: String?,
#Json(name = "quantity")
val quantity: String?,
#Json(name = "Quantity")
val quantity: String?
)
Add to app/build.gradle
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
Refactor your data class
check the key in your item and replace with right one
if productUid or ProductUid
quantity or Quantity
#JsonClass(generateAdapter = true)
data class Item(
#Json(name = "productUid")
val productUid: String,
#Json(name = "quantity")
var quantity: String
)
#JsonClass(generateAdapter = true)
data class Order(
#Json(name = "items")
val items: List<Item>,
#Json(name = "customerName")
val customerName: String,
#Json(name = "customerPhoneNo")
val customerPhoneNo: String,
#Json(name = "customerAddress")
val customerAddress: String,
#Json(name = "note")
val note: String
)
and try it again
I have a user table like this
#Entity(tableName = "users")
data class Users(
#ColumnInfo(name = "id") #PrimaryKey val id: String,
#ColumnInfo(name = "fullName") val fullName: String
...
)
Now I have event information from REST
{
"id": 1,
"eventName": "Event name",
...
"participantList": [
{
"userId": 1,
"status": "going"
},
{
"userId": 2,
"status": "interested"
}
]
}
How can I save this in Room and what should be the entities and relations so that I can have a Event enitity with list of users and each users status. for example
data class Event(
val id:Int,
val eventName: String,
val participantList: List<UserWithStatus>
)
data class UserWithStatus(
val user:User,
val status:String
)
It seems you should use nested relationships in your case.
Try next schema (you can add Foreign Keys if you wish):
Entities
#Entity(tableName = "user")
data class User(
#PrimaryKey var id: Int,
var fullName: String
)
#Entity(tableName = "event")
data class Event(
#PrimaryKey val id:Int,
val eventName: String
)
#Entity(tableName = "event_details")
data class EventDetails(
#PrimaryKey val id:Int,
val eventId: Int, // make it Foreign Key if you want
val userId: Int, // make it Foreign Key if you want
val status:String
)
Auxiliary classes
data class UserWithStatus(
val userId:Int,
val status: String,
#Relation(
entity = User::class,
parentColumn = "userId",
entityColumn = "id"
)
val user: User
)
data class EventWithUser(
#Embedded
val event: Event,
#Relation(
entity = EventDetails::class,
parentColumn = "id",
entityColumn = "eventId"
)
val participantList: List<UserWithStatus>
)
DAO
#Query("select * from event")
fun getEventsWithUsers(): List<EventWithUser>
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.
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.