DEV Community

Cover image for How to migrate your database in android with Room while also testing your migrations
Peter Chege
Peter Chege

Posted on

How to migrate your database in android with Room while also testing your migrations

Introduction

Hello guys 👋👋, welcome back to another article, in our previous article we discussed
how to run instrumented tests on CI(Github Actions) with firebase test lab.
In this article, we'll learn how to make migrations to our database when our data schema changes

If you're a backend developer or rather have interacted with databases at scale, you're probably familiar
with the concept of migrations especially with relational databases like MySQL or PostgresSQL.

What are migrations

Database migrations, also known as schema migrations, database schema migrations, or simply
migrations, are controlled sets of changes developed to modify the structure of the
objects within a relational database. Migrations help transition database schemas from
their current state to a new desired state, whether that involves adding tables and
columns, removing elements, splitting fields, or changing types and constraints.

Why migrations

When you building app that stores its data on a remote server, the data is probably stored on a database.
As you continue building your app, the needs of the app change over time which i.e more data needs to be stored or
removed. Whatever the case, migrations help us make us adjust our database to keep it updated with the latest data needs

Room in android

In android, we usually use the room library to interact with an SQLite database to model our data for offline
capabilities.As an android developer when you're working on a project then you make changes to an entity data class and
run the app it will probably crash that if you had installed a previous version of the app that had some data in the
database. You will probably see an error message saying that room cannot validate the data integrity .....etc.
A solution to this is to delete the app on your emulator/physical device and install it again. This will automatically
delete the data and the crash will go away. This works but what if your app has active users who have data stored,if you ship
the app to production and the users download the new version they will experience crashes before even they are able to
interact with the app. Migrations will help us avoid such scenarios

How to get started with migrations

For this article I prepared a small project to demostrate this concept
You can find it on GitHub here.

The project has 2 branches

  • before-migration - This is before we apply the migration
  • after-migration - This is after we apply the migration

The project is a simple app that lists people.The project uses Koin for DI as it is lightweight. For the sake of simplicity, I added a button that
will automatically generate the data for us and display it to simulate the addition of a person
The data class of the Person entity is as follows

@Entity(tableName = "person")
data class PersonEntity(
    @PrimaryKey
    val personId:String,
    val firstName:String,
    val lastName:String
)
Enter fullscreen mode Exit fullscreen mode

Suppose we would like to add a field called email, we could add it then run our app but as mentioned earlier
the app will crash if there was existing data. To modify the data class we will just add the email field as shown
below

@Entity(tableName = "person")
data class PersonEntity(
    @PrimaryKey
    val personId:String,
    val firstName:String,
    val lastName:String,
    @ColumnInfo(defaultValue = "N/A")
    val email:String,
)
Enter fullscreen mode Exit fullscreen mode

We add the ColumnInfo annotation to define a default value since this is a new field so that room can resolve it to the
default value when the email is not found.

To start applying the migration, we modify the Database class as follows


// Database before migration 
@Database(
    entities = [
        PersonEntity::class,
    ],
    version = 1,
    exportSchema = true
)
abstract class RoomMigrationAppDatabase : RoomDatabase() {
    abstract val personDao: PersonDao;
}

// Database after migration
@Database(
    entities = [
        PersonEntity::class,
    ],
    version = 2,
    exportSchema = true,
    autoMigrations = [
        AutoMigration (from = 1, to = 2)
    ]
)
abstract class RoomMigrationAppDatabase : RoomDatabase() {
    abstract val personDao: PersonDao;
}


Enter fullscreen mode Exit fullscreen mode

As you can see we increment the database version, then we add the autoMigrations block to migrate the database
N/B: Make sure you have exportSchemas=true and have a schemas directory under your app module so that room can
generate the necessary SQL for your database.The schemas will help us when testing the database migrations

That's just about it really.

Note that this is a simple migration, for a real world app you'd probably need more complex migrations.
Check the android documentation here
for such.

Testing our migrations

Migrations are sensitive concept and if not done right could lead to a lot of crashes amongst your users if not done
correctly. That is why we will test our migrations to ensure they run correctly

Before we start make sure you add the following the build.gradle.kts (App Module)

android {
    ....
    sourceSets {
        // Adds exported schema location as test app assets.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

Enter fullscreen mode Exit fullscreen mode

Note I'm using kotlin DSL, if you are using groovy make sure to confirm the syntax.
Above, we are just making sure the exported schemas are used test assets in the androidTest sourceSet

Then we add the room testing dependency

    dependencies {
        ..... Other dependencies
        androidTestImplementation("androidx.room:room-testing:2.5.1")
    }
Enter fullscreen mode Exit fullscreen mode

Then create a Migration class in the normal source set and add the following

class Migrations {

    companion object {
        val M1_2: Migration = Migration(startVersion = 1, endVersion = 2) { database ->
            database.execSQL("ALTER TABLE person ADD COLUMN email TEXT")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This will be used to test the migration by executing the given SQL, in the example above we are adding
the email field to the person table

After that we create MigrationsTest.kt under the androidTest sourceSet and add the following code

@RunWith(AndroidJUnit4::class)
class MigrationsTest {


    private val TEST_DB = "room-migration-app-test"

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
        InstrumentationRegistry.getInstrumentation(), RoomMigrationAppDatabase::class.java
    )


    @Test
    fun migrate1To2() {
        val db = helper.createDatabase(TEST_DB, 1)
        db.close()
        helper.runMigrationsAndValidate(TEST_DB, 1, true, Migrations.M1_2)
    }
}


Enter fullscreen mode Exit fullscreen mode

Above, we first declare a migrationTestHelper rule for the test case. The room testing library has a MigrationTestHelper
class to help us test migrations.In the test case, we define we want to test the migration from version 1 to 2
The test case will first create the database at version 1 then migrate it to version 2 as per the SQL we define above.

If the test case passes, your migration was successful.You can now proceed with your app

Its worth noting that when you modify your entity data classes, you should just keep on incrementing the database version
while also adding tests to validate those migrations before shipping your app to users

For more information, check the official documentation here

Also don't forget the code from the snippets above is available here

I hope you enjoyed the article and learnt something new.

I hope to see you next time, bye. 👋👋👋

Top comments (0)