Migration is always difficult stuff. (Source)

Migrations in Room

Vikram Bhati
4 min readAug 9, 2019

Most of android apps store some form of persistent data in shared preferences, sqlite databases etc. Android had recently launched Room library to easily store data in sqlite databases. Setting up databases, accessing and updating tables were very cumbersome job before Room library.

I have been working on many application where we are using Room library extensively. Biggest hurdle for me was how to write migration plans, because there are lot of limitations in sqlite while updating tables.

In this post, we will see some examples of how to write migration plans and debug issues related to this. ☺️

Example 1 : We have a student table in our database.


@Entity(tableName = “student”)
class Student {
@SerializedName(“name”)
@Expose
@PrimaryKey
var name: String = “”
}

and database class looks like

@Database(
entities = arrayOf(Student::class), version = DATABASE_VERSION, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {

abstract fun studentDao(): StudentDao

companion object {

private var INSTANCE: AppDatabase? = null

private val lock = Any()

fun getInstance(context: Context): AppDatabase {
synchronized(lock) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java, Const.DB_NAME
).build()
}
return INSTANCE!!
}
}
}

}

Now we have to add one column in Student table, so we have added it in our Entity table.

@Entity(tableName = "student")
class Student {

@SerializedName("name")
@Expose
@PrimaryKey
var name: String = ""

@SerializedName("address")
@Expose
var address: String = ""

}

But now when we run our app, then we get below error while accessing database. 🙁

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/user/0/…/databases/room_db

Reason for this error is : we have changed our table but we didn’t increase database version and we haven’t provided any migration plan. 🙄

After adding new column when we open app, then Room library internally check identity hash of database which got changed due to changes in table.

private void checkIdentity(SupportSQLiteDatabase db) {...if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ " you've changed schema but forgot to update the version number. You can"
+ " simply fix this by increasing the version number.");
}
...

Got it. We have to increase version number.. 💪🏼

Still if we directly increase version number of database then we get an error stating that we forgot to add migration plan. 😏

@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
...
throw new IllegalStateException
("A migration from " + oldVersion + " to " + newVersion + " was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");

...
}

So finally, we have to write migration plan here. I have added below migration plan for adding new column in Student table

val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database : SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE student ADD COLUMN address TEXT")
}
}

Still, when we run again app, still we get below error.. 😏

@Override
protected void validateMigration(SupportSQLiteDatabase _db) {
....if (! _infoStudent.equals(_existingStudent)) {
throw new IllegalStateException ("Migration didn't properly handle student(...db.entity.Student).
Expected:
TableInfo{name='student', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=1}, address=Column{name='address', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}

Found:
TableInfo{name='student', columns={address=Column{name='address', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}, name=Column{name='name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=1}}, foreignKeys=[], indices=[]}
.... }

After looking above error, we can see that we forgot to add not null in migration query. 😫

val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database : SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE student ADD COLUMN address TEXT NOT NULL DEFAULT ''")
}
}

Voila 🎉 when we run above then it works perfectly.

Example 2 : Now, we have to store more info related to student in this table, so we decided that it would be better if we change table name to student_info.

val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database : SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE student RENAME TO student_info")
}
}

Wow! It worked.

Example 3 : Due to some API contract changes at server, we have to store student name as : first_name and last_name. And while doing so, we don’t want to loose already stored data in table and as per contract we can consider existing name as first_name. So we will rename name to first_name and need to add one more column for last_name.

val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(database : SupportSQLiteDatabase) {
database.run {
execSQL("ALTER TABLE student_info RENAME TO student_info_old")
execSQL("CREATE TABLE student_info (id INTEGER NOT NULL, first_name TEXT NOT NULL DEFAULT '', last_name TEXT NOT NULL DEFAULT '', address TEXT NOT NULL DEFAULT '', PRIMARY KEY(id))") execSQL("INSERT INTO student_info(first_name, address) SELECT name, address FROM student_info_old") execSQL("DROP TABLE student_info_old")

}

}
}

Great, this worked as expected. In above migration we have also added new primary key.

Folks, That’s all for now. I will add more examples in next blogs. Please comment, share and tap on clap if you learned something new. ☺️

--

--