Room migrations: Upgrading to versions with breaking changes

Summary

If you started using Room when it was in version below 1.0.0-alpha8, and you wrote it in Kotlin or used the NonNull annotation, you will have to migrate your DB.

This use case has been upgraded from 1.0.0-alpha3 to 1.0.0-alpha8.

Let’s see how we can migrate our Room DB.

Why do we need the migration?

In the new version, Room adds support for the NOT NULL constraint in primitive types or columns annotated with NonNull. See release notes.

That is going to change the schema that Room generates. Because it changes the schema, it also changes the identityHash of the DB and that is used by Room to uniquely identify every DB version. Therefore, we need a migration.

Let’s take a look at an example in Kotlin. This is applicable in Java when you have the field annotated with NonNull. Basically, our entity Game has an attribute that can never be null called gameName:

data class Game(
@PrimaryKey
@ColumnInfo(name = "game_name") var gameName: String
...
)

If you upgrade the Room library to version 1.0.0-alpha8, you can see how the schema has changed (You can see the changes in bold in the code below):

  • It changes the createSql sentence adding it the NOT NULL attribute to all NonNulls fields.
"entities": [
{
"tableName": "Game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`game_name` TEXT NOT NULL, PRIMARY KEY(`game_name`))",
...
}
  • Adds the NOT NULL attribute to the fields of that entity.
"fields": [
{
"fieldPath": "gameName",
"columnName": "game_name",
"affinity": "TEXT",
"notNull": true
}

As we said before, because the schema has changed, the identityHash of the database is going to change as well.

"database": [
{
"version": "1",
"identityHash": "c292d7b9b02c8a402a63cb313b4a1e2",
"entities": {...}
}

Note: You don’t know where the schema is or how to export it? Check out the documentation in the “Exporting schemas” section.

Let’s migrate it

To migrate the Database we have to go to our Database object and increase the version number:

@Database(entities = arrayOf(Game::class), version = 2)
abstract class MyRoomDatabase : RoomDatabase() {
abstract fun gameDao(): GameDao
}

Now we have to add a migration to the Room DB builder. Whenever we create the instance of the DB, we have to add the migration as follows:

@Provides
@Singleton
public MyRoomDatabase providesMyRoomDatabase(Context context) {
return Room.databaseBuilder(context, MyRoomDatabase.class, DB_NAME)
.addMigrations(MyRoomDatabase.MIGRATION_1_2)
.build();
}

We create an object of the migration in the RoomDB class.

@Database(entities = arrayOf(Game::class), version = 2)
abstract class MyRoomDatabase : RoomDatabase() {
abstract fun gameDao(): GameDao

companion object {
@JvmField
val MIGRATION_1_2 = Migration1To2()

}
}

Let’s take a look at how that Migration1To2 object is implemented:

The Migration object

First attempt

First thing that comes to our minds is: “We might not need to do anything! Let’s just provide an empty migration and because the columns are the same, all should work for free”. Wrong! I tried that and it didn’t work, the app crashed when reading the DB.

Right attempt

We need to provide a proper migration and recreate the table. Steps to follow:

  1. Create a temp Table with the new schema. Check the code below, we have to include the NOT NULL here.
  2. Copy everything from the old table to the new one
  3. Drop the old table
  4. Rename the new table to the old one
class Migration1To2 : Migration(1,2) {
override fun migrate(database: SupportSQLiteDatabase) {
val TABLE_NAME_TEMP = "GameNew"

// 1. Create new table
database.execSQL("CREATE TABLE IF NOT EXISTS `$TABLE_NAME_TEMP` " +
"(`game_name` TEXT NOT NULL, " +
"PRIMARY KEY(`game_name`))")

// 2. Copy the data
database.execSQL("INSERT INTO $TABLE_NAME_TEMP (game_name) "
+ "SELECT game_name "
+ "FROM $TABLE_NAME")

// 3. Remove the old table
database.execSQL("DROP TABLE $TABLE_NAME")

// 4. Change the table name to the correct one
database.execSQL("ALTER TABLE $TABLE_NAME_TEMP RENAME TO $TABLE_NAME")
}
}

Note: The class extends from Migration which is provided by the Room library. Notice that we are passing two arguments to the parent constructor. Those numbers indicate the old version of the DB and the new version. In our example, we are going to migrate from version 1 to version 2. You can read more about it in the Room documentation, but basically, the migrate method that we override is going to be executed whenever the migration needs to happen.

Note: Just to make the first step easier, let Android Studio create the new version of the schema and copy it from there.

Testing your Migration

There are great articles explaining how to test your Room migrations. Check out this article by Florina Muntenescu: Link to article here.

Thanks for reading,

Manuel Vicente Vivo

--

--

Android DevRel @ Google

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store