How to migrate your database in android with Room while also testing your migrations
Published: 2023-05-23 20:10:06


## 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](https://medium.com/@peterkagure/running-instrumented-tests-on-github-actions-with-firebase-test-lab-ac17bb36ae9c).
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](https://github.com/chege4179/RoomMigrationApp).

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 

```angular2html
@Entity(tableName = "person")
data class PersonEntity(
    @PrimaryKey
    val personId:String,
    val firstName:String,
    val lastName:String
)
```
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 

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

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

```angular2html

// 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;
}


```
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](https://developer.android.com/training/data-storage/room/migrating-db-versions#kts) 
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)
```angular2html
android {
    ....
    sourceSets {
        // Adds exported schema location as test app assets.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

```
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")
    }
```

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

```angular2html
class Migrations {

    companion object {
        val M1_2: Migration = Migration(startVersion = 1, endVersion = 2) { database ->
            database.execSQL("ALTER TABLE person ADD COLUMN email TEXT")
        }
    }
}
```
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)
    }
}


```
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](https://developer.android.com/training/data-storage/room/migrating-db-versions)

Also don't forget the code from the snippets above is available [here](https://github.com/chege4179/RoomMigrationApp)


I hope you enjoyed the article and learnt something new.

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