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
)
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,
)
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;
}
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")
}
}
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
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
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)