Android Room - How to migrate when a nested object model is changed? - android

I have a database schema set up using Android Room, Dao, and Entity classes set up as POJOs. Except the POJO entity isn't so "plain" and that it actually holds a reference to another object. I thought this was a great idea at the time as it allowed me more flexibility in changing the object and using it in other places in the app and only saving to the database as needed.
The problem I'm facing now is that the migration guideline only mentions how to migrate the database by altering the SQL, but I changed the object itself. My typeconverter class simply converts the object to and from a string.
Because it's being saved as a long string I know I essentially have to do a simple REPLACE(string, old_string, new_string) in the SQL
migration code block with the updated object being the new string. How can I retrieve the old objects and update values before running the replace SQL command in the migration block?
UPDATE: I'm using GSON in my typeconverter class to change the object to a string, so the solution that comes to mind is to simply download the old object and upload the new one with the added fields. Only problem is that you can't access the database and download the json, convert it to the object, add the new data fields, then reconvert to a new json string.
I'm lucky I'm not at scale yet because this would be a tricky thing to do for so many users. (So I recommend that anyone reading this not do what I did and implement object nesting. It's easier to convert the Entry objects to the other portable objects instead of nesting when it comes to updating the data you want saved.)
I think if you already did what I did and can't go back, the best bet is to simply create the new portable object and make new typeconverter functions for that one and add the SQL COLUMN for the new object. The problem then lies in how you then retrieve those objects from the Entry Dao, which will cause a lot more code to write and possible errors to debug if not done carefully.
Long story short, if anyone is reading this, DO NOT nest objects in Room DBs on Android unless you are 100% sure it's a final form of your model... but is there such a thing anyways?

I just ran into this issue, but fortunately I only needed to add a new key/value pair to a "flat" object model. So hopefully my answer can be expanded on to fully answer #Mr.Drew question.
Assuming you have a table town with a column star_citizen that is the object model being typeconverted:
{"name":"John", "age":30, "car":false}
and you want to update the object to have an extra property "house": true
you could add a migration to your App's Room Database class like this (Kotlin example):
#Database(entities = [Town::class], version = 2, exportSchema = true)
#TypeConverters(DataConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract val sharedDao : SharedDao
companion object {
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
val cursor = database.query("SELECT * FROM `town`")
// iterate through each row in `town`, and update the json
// of the StarCitizen object model
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val colIdIdx = cursor.getColumnIndex("id")
val id = cursor.getInt(colIdIdx)
val colStarCitizenIdx = cursor.getColumnIndex("star_citizen")
val rawJson = cursor.getString(colStarCitizenIdx)
val updatedRawJson = starCitizenModelV1ToV2(rawJson)
database.execSQL("""UPDATE town SET star_citizen ='${updatedRawJson}' WHERE ID = $id""")
cursor.moveToNext()
}
}
}
//[...]
private fun starCitizenModelV1ToV2(rawJson: String): String {
val rawJsonOpenEnded = rawJson.dropLast(1)
val newProperty = "\"house\":true"
return "$rawJsonOpenEnded,$newProperty}"
}
}
}

Related

Using .addOnSuccessListener to return a value for a private method

Good day. Is there some way that I could implement this one?
val db = Firebase.firestore
val userID = Firebase.auth.currentUser!!.uid
val infoRef = db.collection("user").document(userID).collection("profile").document("info")
infoRef.get()
.addOnSuccessListener {document ->
if(document != null){
//get the data as cast to hashmap
val data = document.data as HashMap<*, *>
//get the username field and set text for greet user as the same value inside firestore
val username = data["username"] as String
tv_greet_user.text = "Hello, $username"
}
}
//extract the code above as a new method called "getUsername()"
val username : String = getUsername()
tv_greet_user.text = "Hello, $username"
Yes, you can create a function that looks like this:
fun getUsername(data: HashMap<String, Any>) = data["username"] as String
And inside your callback simply call:
val username = getUsername(data)
Is there a way to extract the whole block into a separate method? So that in the onCreate method, I could simply change the TextView into something like: val username = getUsername() tv_greet_user.text = "Hello, $username"
Edit:
As also Frank van Puffelen mentioned in his you cannot return the result of an asynchronous operation as a result of a method. Since you are using Kotlin programming, please note that there is actually o solution. I wrote an article called:
How to read data from Cloud Firestore using get()?
In which I explained four ways in which you can get data from Firestore. So if you are willing to use Kotlin Coroutines, then things will be much simpler.
Data is loaded from Firestore (and most modern web/cloud APIs) asynchronously. Since it may take some time before the data is available, your main code continues running while the data is being loaded. Then when the data is available, your success listener callback is called with that data.
This unfortunately means that it is impossible to return the value from the database in a function, because by the time the return statement runs, the data hasn't been loaded yet.
And that's also precisely why infoRef.get() in your code doesn't simply return the value from the database, but requires that you pass in a callback function that it invokes when the database is available. Sure, your code would be a lot simpler if get() would immediately return the value from the database, but it can't do that because the data needs to be loaded from the network.
I recommend reading:
The Kotlin docs on asynchronous programming techniques
Why does my function that calls an API return an empty or null value?
How to return a DocumentSnapShot as a result of a method?
How to check a certain data already exists in firestore or not

How to find invalid Kotlin object generated by Gson (from reflection)?

{
"data":[{"compulsory_field": 1}, {"compulsory_field": 2}, {}]
}
converts into object by gson
data class Something(val compulsory_field: Int)
val somethingList = //gson parse the data
println(somethingList)
//[
// Something(compulsory_field = 1),
// Something(compulsory_field = 2),
// Something(compulsory_field = null) //Should not exists
//]
and I want to get rid of the 3rd item. Is it possible to do it AFTER it has been converted to object? Or can it only be done when it's String/InputStream? And how can I do that?
Thanks!
Edit: clarify that the constructor works, but gson failed to understand kotlin rules and injected objects that I can't check in Kotlin
I came up with a ugly "solution"/workaround, but I am still searching for a better answer (or get the project to switch to moshi codegen or something else, whichever comes first)
Basically I just copy each object again to make sure it goes through all the null-safety checking kotlin provides
val somethingList = //gson parse the data
val fixedSomethingList = somethingList.mapNotNull {
try {
it.copy(compulsory_field = it.compulsory_field)
} catch (e: IllegalArgumentException) { //if gson inserted a null to a non-null field, this will make it surface
null //set it to null so that they can be remove by mapNotNull above
}
}
Now the fixedSomethingList should be clean. Again, very hacky, but it works......
If you don't like empty objects then just remove them. You should always can do it after parsing. However please be aware that in Kotlin lists can be mutable or not. It you received an inmutable one (built with "listOf") then you will have to build a new list including only the elements you want.
https://kotlinlang.org/docs/reference/collections.html
Edited: Ok, I understand you can't even parse the json in first place. In this case maybe you can try this:
To allow nulls: change type of compulsory_field from Int to Int? at declaration
Fix your json string before parsing: In your case you can replace all {} with {"compulsory_field": null}
Now gson parser should obtain a valid list.

Insert or update realm content based on objects inner attribute changes

My data model looks like this:
Dummy:
id: String
title: String
content: List
object1
object2
object3
...
Lets image I've a table "Dummy" in realm, where I have 4 Dummy objects. I do a sync from backend and receive 4 Dummy objects, but this time, the first Dummy object has different content - for instance 2 more items. I would like to update local DB with new content. I tried to use insertOrUpdate and copyOrUpdate, but they did not work. Then I thought - maybe I should just delete Dummy table and insert new data - turns out, I can't even do that (commented code does not work). Any ideas on how to succeed and what am I missing?
private fun storeDataToLocalDB(data: ArrayList<Dummy>) {
Realm.getDefaultInstance().use { realmInstance ->
realmInstance.executeTransaction {
realm ->
//val result: RealmResults<Dummy> = realm
//.where(Dummy::class.java)
//.findAll()
//result.deleteAllFromRealm()
realm.insertOrUpdate(data)
}
}
}

Return Map object by DAO method

I would like to have method like this in my dao object
#Query("SELECT c.name, sum(p.value) FROM payments p, paymentCategories c WHERE p.categoryId = c.id GROUP BY c.name")
fun getCategoryStats(): Map<String, Float>
but i get the error
error: Not sure how to convert a Cursor to this method's return type
public abstract java.util.Map
Is it possible to change it to working version?
So it can be different type to return but the main conditions are
It must be only one query in db
I would like to avoid extra code like creating additional data structure only for this method
I'm little late for a party but maybe someone will still search for it.
From Room 2.4 we are able to use multimap return type with annotation #MapInfo which allow us to define the mapping for key and value.
In your case it will be sth like this:
#MapInfo(keyColumn = "name", valueColumn = "sum")
#Query("SELECT c.name AS name, sum(p.value) AS sum FROM payments p, paymentCategories c WHERE p.categoryId = c.id GROUP BY c.name")
fun getCategoryStats(): Map<String, Float>
More info:
https://developer.android.com/training/data-storage/room/accessing-data#multimap
While I don't think this can be done in the Dao it can easily be done when querying the LiveData in the repository or the viewModel (or where ever you are querying the list) using a transformation. Since I don't know your datanames I'm using made up ones:
val categoryStatsMap: LiveData<Map<String, Float>> =
Transformations.map(
database.categoryStatsDao.getCategoryStats()) {it ->
it.map {it.key to it.value}.toMap()
}
'it.key' and 'it.value' are the fields in the entity object that you want to use as... key and value pairs in the map.
Transformations give you a live data object based on another live data object. I don't know what the overhead is, but I assume it shouldn't be too big.

Save complex objects and their sons entities ORM Lite

I'm using ORM Lite on a project , I decided to use the facility to make the persistence part of the Web service once and can reuse it on Android.
But I am suffering a lot because possou complex objects that have multiple ForeignCollectionField and Foreign object , and at the hour of perssistir temenda these data is a headache, because I have to enter one by one of their children , I think the idea of ​​an ORM is make life easier , ie you have to persist the object and father and all the rest is done behind the scenes ...
Well, it is now too late to give up lite ORM , I wonder if there is a way to do what sitei above ..
I found a piece of code here
tried to implement but it seems not work , just keeps saving the parent object .
follows the function I'm trying to use , but do not know whether imports are correct because the code I found in the link above did not have this data
public int create(Object entity, Context context) throws IllegalArgumentException, IllegalAccessException, SQLException, SQLException {
try{
if (entity!=null){
// Class type of entity used for reflection
Class clazz = entity.getClass();
// Search declared fields and save child entities before saving parent.
for(Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
// Inspect annotations
Annotation[] annotations = field.getDeclaredAnnotations();
try{
for(Annotation annotation : annotations) {
// Only consider fields with the DatabaseField annotation
if(annotation instanceof DatabaseField) {
// Check for foreign attribute
DatabaseField databaseField = (DatabaseField)annotation;
if(databaseField.foreign()) {
// Check for instance of Entity
Object object = field.get(entity);
Dao gDao = getDatabase(context).getDao(object.getClass());
gDao.create(object);
}
}else if (annotation instanceof ForeignCollectionField){
Object object = field.get(entity);
for(Object obj : new ArrayList<Object>((Collection<?>)object)){
Class c = obj.getClass();
Dao gDao = getDatabase(context).getDao(obj.getClass());
gDao.create(obj);
}
}
}
}catch (NullPointerException e){
e.printStackTrace();
}
}
// Retrieve the common DAO for the entity class
Dao dao = getDatabase(context).getDao(entity.getClass());
// Persist the entity to the database
return dao.create(entity);
}else
return 0;
}finally {
if (database != null) {
OpenHelperManager.releaseHelper();
database = null;
}
}
}
Leveraging the same post, also need a colução to delete cascade, imagine a situation where I have the following tables:
Company > Category> person> contact> Phone and email
Deleting and now I do as described in the documentation:
public int deleteCascade(Prefeitura prefeitura, Context context){
try{
Dao<Prefeitura, Integer> dao = getDatabase(context).getDao(Prefeitura.class);
DeleteBuilder db = dao.deleteBuilder();
db.where().eq("prefeitura_id", prefeitura.getId());
dao.delete(db.prepare());
// then call the super to delete the city
return dao.delete(prefeitura);
}catch (SQLException e){
e.printStackTrace();
}
return 0;
}
But the objects that are not directly linked the company would still be in the database, how could I do?
But without hacks, I want a clean code ...
I know ORM Lite really is lite, but one that saves the children create and delete cascade is essential for any ORM, hopefully for the next versions it is implemented, it is regrettable not have these features, for simple projects is very good, but in a complex project because a lot of headaches, I'm feeling on the skin.
Any help is welcome!

Categories

Resources