Best practice when interacting with Room and SharedPreferences - android

I'm using a Room table that takes an entity that has a "Favorited" boolean field, which is by default False, but can be made True through user input. The database needs to be wiped periodically and repopulated by a network call. Doing so naturally reverts all "Favorited" fields to False.
Using SharedPreferences, I can keep a second, persistent list of Favorited entities, but what's the cleanest way to repopulate Room and hold on to the favorites?
Using RxKotlin here for clarity, if not accuracy,
should it be something like
saveEntities()
.flatMapIterable { it }
.map{
if (sharedPrefs.contains(it.id))
it.apply{favorite}
else it }
.toList()
.subscribe()
Or should I serialize a list of favorites, save that to SharedPreferences, and then
val favoritesList = PrefSerializer().getFavorites
saveEntities()
.map{ it.forEach{
if (favoritesList.contains(it))
it.apply{favorite}
else it}
}
.subscribe()
Or does it even make sense to store Favorite information in Room?
Referencing SharedPreferences every time an entity is called feels like bad practice.

I would recommend using 2 tables in Room because that way you can use a database view to combine them. Room can monitor the tables and notify the UI via LiveData. You can technically monitor SharedPreferences, but I believe it is in fact more tedious.
For example, this is a table where data is periodically overwritten by sync.
#Entity
data class Cheese(
#PrimaryKey
val id: Long,
val name: String
)
And you have a separate table to hold the "favorite" status.
#Entity
data class CheeseFavorite(
#PrimaryKey
val id: Long,
val favorite: Boolean
)
Then you can create a database view like this to combine 2 tables.
#DatabaseView(
"""
SELECT c.id, c.name, f.favorite
FROM Cheese AS c
LEFT OUTER JOIN CheeseFavorite AS f
ON c.id = f.id
"""
)
data class CheeseDetail(
val id: Long,
val name: String,
val favorite: Boolean?
)
The view can be queried just like a table. You can of course use LiveData or Rx data types as the return type.
#Query("SELECT * FROM CheeseDetail")
fun all(): List<CheeseDetail>

Related

How do I create a new table (which should have a LocalDate variable) by migration?

That is my current dao
#PrimaryKey(autoGenerate = true)
val id: Int,
val name: String,
val date: LocalDate,
val amount: Int,
val uri: String,
val tag: String,
val toList: Boolean,
val inUse: Boolean,
val listValue: Int
now I have the problem that in a previous version of that dao there is a variable in that table that I now want to remove.
I found a 4 step guid:
1.) create new table 2.) insert from the old table 3.) drop the old table 4.) alter new table name back to old table name
that's fine but my problem is that I have a variable with a LocalDate which uses a DateTypeConverter to function properly.
How do I insert that LocalDate into the new table? I just know of TEXT and INTEGER
Step 2 use the SupportSQliteDatabase's execSQL method to execeute a query based upon the SQL
INSERT INTO <the_table> SELECT <the_columns> FROM <the_old_table>;
Where:-
anything enclosed within <> needs to be altered accordingly as per:-
<the_table> should be replaced with the new table name.
<the_columns> should be replaced with the column names, separated by commas, LESS THE DROPPED COLUMN NAME
<the_old_table> should be replaced with the old/original table name.
Note that a variable name will be the same as the variable name.
The above will copy the values, whatever they are, as stored in the database, from the old to the new table.
The TypeConverters are only used to convert the data to or from the respective object (LocalDate in your case) when storing or retrieving the stored data.
A type converter should consist of two functions:-
1 to convert the object to a type that can be stored in an SQLite database (SQLite is a universal database that has no concept of a programming languages objects). The SQLite types being
INTEGER (not necessarily a Kotlin Int, could be a Long, Byte even a Boolean ....).
TEXT (a Kotlin String ....)
REAL (Kolin Double, Float ....)
BLOB (Kotlin ByteArray ....)
NULL
2 to convert the stored type into the object when retrieving data from the database. As such it is no issue at all for the INSERT INTO table SELECT ....; to copy the existing data from one table to another irrespective of Room's handling of the data when it stores and retrieves the data.
The result being that the data is stored in the database as either one of the 5 types. As such it is no is
If the "current dao" (it is not a dao, it is an entity that should be annotated with #Entity, which equates to a table) is after the removal of the dropped variable then you would use:-
INSERT INTO <the_new_table> SELECT id,name,date,amount,uri,tag,toList,inUse,listValue FROM <the_old_table>;
You may wish to refer to 2. INSERT INTO table SELECT ...;
If you want to add data into date column of old table data.
fun updateData() {
val list = dao.getAllData()
list.forEach {
//update data
}
dao.saveData(list)
}

Changing primaryKeys order changes the order of the data saved in ROOM

I have a situation where I need some information about my results, in my code I have this data class/Room's entity.
#Entity(primaryKeys = ["searchId","page","pr_id"])
data class ProductResponse(
var searchId: Int,
val page: Int,
#Embedded(prefix = "pr_")
val products: ProductSearchFormatted
)
Where you can see in the Android Studios App Inspector:
The data is saved as spected in ROOM, when I try to load it from ROOM:
#Query(
"SELECT * FROM PagedSearchResponse WHERE searchId ==:input"
)
fun loadPagedSearchResponse(input: Int): PagingSource<Int, ProductResponse>
I just need the data in the same order that was previously saved, and got the data in different order (ordered by pr_id):
I found out that if I change the primaryKeys order, like
#Entity(primaryKeys = ["pr_id","searchId","page"])
data class ProductResponse(
var searchId: Int,
val page: Int,
#Embedded(prefix = "pr_")
val products: ProductSearchFormatted
)
Now the data's order from ROOM is correct.
Why does this happen? Does the primaryKeys order matter?
Changing primaryKeys order changes the order of the data saved in ROOM
NO it does not, the data is ALWAYS saved in the order in which it is inserted. What is changing in your case, is the ORDER in which the data is extracted. That is because you aren't saying in what ORDER you want the data to be extracted and are leaving that choice to the query planner.
Why is this happened?, does the primaryKeys order matter?.
Yes it can do, especially in the absence of other indexes. Certainly pr_id before search_id will make a difference as the order within the index will be different and that as the WHERE clause is on the search_id then it is likely that the primary key index will be used (as in both cases search_id is an initial column (see the links below))
The query planner is an AI that tries to pick the fastest and most efficient algorithm for each SQL statement.
see :-
https://www.sqlite.org/queryplanner.html
https://www.sqlite.org/optoverview.html
https://www.sqlite.org/queryplanner-ng.html
If you want data to be in ORDER then you should specify an ORDER clause (ORDER BY ....). That is the only way to guarantee an ORDER. Assuming an ORDER without an ORDER clause will very likely result in issues.
Saying that using pr_id prior to search_id makes the composite (multiplte column) index likely to be more beneficial as the pr_id (according to the data shown) is less repeated than the search_id.
The way I fix it, it was simpler than I expected.
In the entity, I added a field called "order", and in my Mediator (I'm using Paging 3) basically did this:
list.mapIndex { s,t ->
ProductResponse(
...
order = s
...
)
}
That way, I'm following the EXACT order from Backend without the need to modify any primaryKeys.

Android room, map to existing model

I have a room database that stores a list of vacation resorts, which includes fields for State and Country, and am trying to get a distinct list of the State/Country combinations.
So far I have:
#Query("SELECT DISTINCT state,country from ResortdatabaseModel")
List<StateModel> getAllStates();
However, StateModel is not an entity within the database, it's a model defined elsewhere in the project, and I'm getting an error "Not sure how to convert a Cursor to this method's return type "
How do I map the query results to the exiting model?
The class StateModel needs to have the properties state and country with the same types as those two columns in Resortdatabasemodel, e.g.:
data class StateModel(
val state: String,
val country: String,
)
(String is just an example here, I don't know the actual types in your project)

How to insert a duplicate row in Room Db?

While searching for this, I only came across people asking how to Avoid inserting duplicate rows using room db. But my app has a feature where the user may tap a copy button and the list item will get inserted again in the db. I could have simply achieved this if my table didn't have a primary key set on one of its fields. While I found this solution for SQLite, I don't know how I can achieve this in Room Db. Because while writing an insert query with custom queries in room would defeat the purpose of using room in the first place.
Let's say you have some entity
#Entity(tableName = "foo_table")
data class Foo (
#PrimaryKey(autoGenerate = true) var id: Int,
// or without autogeneration
// #PrimaryKey var id: Int = 0,
var bar:String
)
and you have some Dao with insert:
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(foo: Foo)
Then to copy your existing value (copiedValue: Foo) you need in some way to manage your primary key:
Scenario 1. Your Primary Key is autogenerated, you have to set it to default value to get new autogenerated one:
copiedValue.id = 0
yourDao.insert(copiedValue)
Scenario 2. Your Primary Key is not autogenerated, you have to set new primary key manually:
copiedValue.id = ... // some code to set new unique id
yourDao.insert(copiedValue)

Room - SELECT query, get or default

In SQL Brite, there's a method mapToOneOrDefault. Is there a similar thing in Room?
Say for Model
#Entity(tableName = "users")
data class User(#PrimaryKey val name: String)
and Dao
#Dao
interface UserDao {
#Query("SELECT FROM users where name = :name")
fun getUserByName(name: String): Flowable<User>
}
Not the stream returns nothing for getUserByName("John") if there's no John in DataBase. Is there a way to get a default value, say User("")?
Not the stream returns nothing for getUserByName("John") if there's no
John in DataBase. Is there a way to get a default value, say User("")
There is no default mechanism.
You could change from Flowable<User> to Flowable<List<User>>. In case of no user you will get an empty list back. You can use a map to check and return a default value or filter+switchIfEmpty.
Or you could change from Flowable to Single. With Single, in case of no rows, matching your query, onError will be triggered. You can then implement onErrorReturn or onErrorResumeNext to return a default value
You can use Maybe instead of the Flowable in this case.
Maybe: Conceptually, it is a union of Single and Completable providing the means to capture an emission pattern where there could be 0 or 1 item or an error signaled by some reactive source.
You can then use the operator defaultIfEmpty to map to a new object if the query didn't return a value.
Reference

Categories

Resources