android architecture components + retrofit - android

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.

Related

cascade delete in android room database KOTLIN

There are a bunch of questions like this in StackOverflow but most of that arent about room database, so I had to ask a new question.
I have an app that uses room database and that has near 4 tables and a big relationship between those tables, so for instance when I delete a user in user list fragment, that user delete(only userName and some personal info) but the user's TRANSACTIONS and LOANS hadn't been deleted.
Someone told me I have to use Cascade delete but I didn't find much info about it.
My User class model:
#Entity(tableName = "user_info")
data class UserInfo(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "user_id")
var userId: Long =0L,
#ColumnInfo(name = "full_name")
var fullName:String?,
#ColumnInfo(name= "account_id")
var accountId: String?,
#ColumnInfo(name = "mobile_number")
var mobileNumber:String?,
#ColumnInfo(name = "phone_number")
var phoneNumber:String?,
#ColumnInfo(name = "date_of_creation")
var dateOfCreation:String?,
#ColumnInfo(name = "address")
var address:String?,
)
Transactions model class:
#Entity(tableName = "transactions")
data class Transactions(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "trans_id")
var transId: Long = 0L,
#ColumnInfo(name = "user_id")
var userId: Long?,
#ColumnInfo(name = "create_date")
var createDate: String?,
#ColumnInfo(name = "bank_id")
var bankId: Long?,
#ColumnInfo(name = "description")
var description: String?,
#ColumnInfo(name = "increase")
var increase: String?,
#ColumnInfo(name = "decrease")
var decrease: String?,
#ColumnInfo(name = "loan_number")
var loanNumber: String?,
#ColumnInfo(name = "total")
var total: Long?,
#ColumnInfo(name = "type")
var type: String?
)
User DAO:
#Insert
suspend fun insert(ui: UserInfo): Long
#Update
suspend fun update(ui: UserInfo)
#Insert
suspend fun insertList(ui: MutableList<UserInfo>)
#Delete
suspend fun deleteUser(ui: UserInfo)
#Query("DELETE FROM user_info")
fun deleteAllUser()
#Query("SELECT user_info.user_id, user_info.full_name, transactions.total From user_info JOIN transactions ")
fun joinTable(): LiveData<List<UserAndMoney>>?
#Query("SELECT * from user_info WHERE user_id = :key")
fun get(key: Long): LiveData<UserInfo>?
#Query("SELECT * FROM user_info ORDER BY full_name DESC")
fun getAllUserInfo(): LiveData<List<UserInfo>>
#Query("SELECT * FROM user_info where full_name like '%' || :fullName || '%' ORDER BY full_name ASC")
fun searchUserName(fullName: String): LiveData<List<UserInfo>>
If It was not clear for you till now, let me makes it easy for you:
I need cascade delete that delets every thing about user and a record.
CASCADE is an option of a Foreign Key constraint. So you would need to define Foreign Key constraints. You define Foreign Key constraints in Room via the #Entity annotation.
As an example, as it would appear that a Transactions is related to a UserInfo via var userId: Long?, (column name user_id) you could have :-
#Entity(tableName = "transactions",
foreignKeys = [
ForeignKey(
entity = UserInfo::class,
parentColumns = ["user_id"],
childColumns = ["user_id"],
onDelete = ForeignKey.CASCADE, //<<<<<
onUpdate = ForeignKey.CASCADE // Optional
)
]
)
data class Transactions(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "trans_id")
var transId: Long = 0L,
#ColumnInfo(name = "user_id", index = true) // <<<<< best to have an index on the column, not required
var userId: Long?,
#ColumnInfo(name = "create_date")
var createDate: String?,
#ColumnInfo(name = "bank_id")
var bankId: Long?,
#ColumnInfo(name = "description")
var description: String?,
#ColumnInfo(name = "increase")
var increase: String?,
#ColumnInfo(name = "decrease")
var decrease: String?,
#ColumnInfo(name = "loan_number")
var loanNumber: String?,
#ColumnInfo(name = "total")
var total: Long?,
#ColumnInfo(name = "type")
var type: String?
)
Note
The constraint enforces referential integrity, that is a transaction can not be inserted/updated if the user_id value is not a value that exists in the user_id column of the user_info table.
The CASCADE onUpdate will cascade a change to the user_id value in the user_info table to the respective transactions.
Additional
Someone told me I have to use Cascade delete but I didn't find much info about it.
What you have been told is incorrect. You could replicate the functionality without ON DELETE CASCADE or without the Foreign Key constraint.
You could use
#Query("DELETE FROM transaction WHERE user_id=:userId")
fun cascadeDeletionsFromUser(userId: Long)
noting that if the Foreign Key constraint exists in the transactions table but didn't have an onDelete action specified, then the cascadeDeletionsFromUser function would have to be run before the user_info row is deleted. Otherwise the user_info row could not be deleted as the FK constraint would inhibit the deletion.
If you had an abstract class rather than interface then you could have:-
#Query("DELETE FROM user_info WHERE user_id=:userId")
abstract fun deleteUserById(userId: Long)
#Query("DELETE FROM transactions WHERE user_id=:userId")
abstract fun cascadeDeletionsFromUser(userId: Long)
#Transaction
#Query("")
fun deleteUserWithCascade(userId: Long) {
cascadeDeletionsFromUser(userId)
deleteUserById(userId)
}
and use the deleteUserWithCascade function to delete the transactions and user in one go.
It is more convenient to use ON DELETE CASCADE, and especially so if you have multiple depths of relationships (when it gets a little more complex ascertaining children)

Roomdb - Update #Embedded object within an Entity

I faced a problem when updating values of #Embedded object within an Entity.
Here is my Entity class:
#Entity
data class ReplyData(
#PrimaryKey val id: String,
#ColumnInfo(name = "sequence") val sequence: Int?,
#Embedded(prefix = "from") val from: From? <--- I want to update this #Embedded object within entity
)
#Entity
data class From(
#ColumnInfo(name = "id") val id: String? = "",
#ColumnInfo(name = "name") val name: String? = "",
#ColumnInfo(name = "avatar") val avatar: String? = "",
#ColumnInfo(name = "kind") val kind: String? = ""
)
I want to update these 3 values in 1 shot instead of updating them one-by-one with this query below.
#Query("UPDATE replydata.from SET name = :name, avatar = :avatar, kind = :kind WHERE id = :id")
fun updateReplyData(id: String, name: String, avatar: String, kind: String)
How can I achieve that without affecting the original entity (ReplyData)?
I tried this solution, but it is not working at all:
#Query("SELECT * FROM message WHERE id = :id")
suspend fun getReplyMessage(id: String): Message
#Update(entity = From::class)
suspend fun updateReply(msg: Message)
suspend fun updatePinMessage(id: String, from: From) {
val msg = getReplyMessage(id)
msg.from?.avatar = from.avatar
msg.from?.name = from.name
msg.from?.kind= from.kind
updateReply(msg)
}
Some notes:
#Embedded just shares own fields in the parent.
For instance the data table columns are:
[id | sequence | fromid | fromname | fromavatar | fromkind ]
NB: Better to use "from_" instead "from"
you can update these fields directly in your queries.
Maybe so late but ...
You need create support class
#Entity
data class ReplyData(
#PrimaryKey val id: String,
#ColumnInfo(name = "sequence") val sequence: Int?,
#Embedded(prefix = "from") val from: FromItem <- here
)
#Parcelize
data class FromItem(val item: From)
data class From(
val id: String? = "",
val name: String? = "",
val avatar: String? = "",
val kind: String? = ""
)
and update from
#Query("Update ReplyData set fromitem = :item where id = :id")
fun update(id: Long, item: From)
P.S:
I didn't check this code, maybe it has some errors

Moshi and room - mapping relationships

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
)

Room database loses data

I'm working on an Android project with Kotlin language and I'm using Room database from android architecture components. These are all my Room database stuff. I'm trying to let the user save information about a book and it works fine while I'm in the app, but when I restart the app, everything has been deleted. I need to store data in memory and because of that I'm using inMemoryDatabaseBuilder but it doesn't seem to work. Any help is appreciated.
Dependencies:
dependencies {
def room_version = "1.1.1"
implementation "android.arch.persistence.room:runtime:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
}
apply plugin: 'kotlin-kapt'
Entity:
#Entity(tableName = "tblBooks")
data class BookData(#PrimaryKey(autoGenerate = true) var id: Long?,
#ColumnInfo(name = "name") var name: String,
#ColumnInfo(name = "author") var author: String,
#ColumnInfo(name = "translator") var translator: String,
#ColumnInfo(name = "publisher") var publisher: String,
#ColumnInfo(name = "publication_year") var publicationYear: Int?,
#ColumnInfo(name = "price") var price: Int?,
#ColumnInfo(name = "description") var description: String,
#ColumnInfo(name = "category") var category: String,
#ColumnInfo(name = "shelf_number") var shelfNumber: Int?,
#ColumnInfo(name = "front_cover") var frontCover: String?,
#ColumnInfo(name = "back_cover") var backCover: String?
){
constructor():this(null, "", "", "", "", 0, 0, "", "",
0, "", "")
}
Dao:
#Dao
interface BookDataDao {
#Query("SELECT * FROM tblBooks")
fun getAll(): List<BookData>
#Insert(onConflict = REPLACE)
fun insert(bookData: BookData)
}
Database:
#Database(entities = [BookData::class], version = 1)
abstract class BookDatabase: RoomDatabase(){
abstract fun bookDataDao(): BookDataDao
companion object {
private var INSTANCE: BookDatabase? = null
fun getInstance(context: Context): BookDatabase? {
if (INSTANCE == null){
synchronized(BookDatabase::class){
INSTANCE =
Room.inMemoryDatabaseBuilder(context.applicationContext,
BookDatabase::class.java).build()
}
}
return INSTANCE
}
}
}
Usage:
inserting data:
val database = BookDatabase.getInstance(this) !!
database.bookDataDao().insert(bookData)
retrieving data:
val database = BookDatabase.getInstance(this) !!
val booksList = database.bookDataDao().getAll()
If you are only storing on memory, it is normal your database does not persist between launches of the app. According to the documentation:
Information stored in an in memory database disappears when the process is killed
Meaning everytime you kill the application, the database is closed as well. If you want it to be persistant you should use standard databaseBuilder instead, it will be put on the device storage.

Many to Many relations with Room & LiveData

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.

Categories

Resources