Article image Using Room for Database Management: Room Database Migrations

59.6. Using Room for Database Management: Room Database Migrations

Page 83 | Listen in audio

In the realm of Android app development, managing databases efficiently is crucial for maintaining data integrity and providing a seamless user experience. As your application evolves, so does its database schema. This evolution necessitates a robust mechanism for handling database migrations, ensuring that users' data remains intact and accessible across app updates. In Kotlin for Android app development, the Room persistence library offers a powerful framework to manage SQLite databases, including a well-structured approach to handle database migrations.

Room is part of Android's Jetpack suite of libraries, designed to simplify database management by providing an abstraction layer over SQLite. It allows developers to interact with the database using data access objects (DAOs) and entities, thus reducing boilerplate code and minimizing the risk of runtime errors. However, as your app's database schema changes over time, managing these changes becomes a critical task. This is where Room's database migration feature comes into play.

Understanding Database Migrations

Database migrations are essential when altering the schema of your database. Changes such as adding new tables, modifying existing ones, or updating data types can break your app if not handled correctly. A migration is essentially a set of instructions that dictate how to transition from one version of a database schema to another. Room provides a structured way to define these migrations, ensuring that your database evolves smoothly without data loss.

Setting Up Room with Migrations

To use Room with migrations, you first need to define your database entities and DAOs. Once your initial database setup is complete, you can focus on implementing migrations. Room requires you to specify a version number for your database schema, starting with version 1. Each time you make changes to the schema, you increment this version number.

Here’s a simple example of setting up a Room database with an initial schema:


@Entity(tableName = "users")
data class User(
    @PrimaryKey val userId: Int,
    val userName: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): List<User>

    @Insert
    fun insertAll(vararg users: User)
}

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

In this setup, we have a simple User entity and a corresponding DAO. The database version is set to 1. As your application grows, you might need to modify the User table or add new tables. This requires a migration.

Implementing a Migration

Suppose you want to add a new column email to the User table. This change requires a migration from version 1 to version 2. Here’s how you can implement this migration:


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

In this example, we define a MIGRATION_1_2 object that extends Room's Migration class. The migrate method contains the SQL statement to alter the users table by adding the new email column.

Next, you need to integrate this migration into your Room database setup:


val db = Room.databaseBuilder(
    applicationContext,
    AppDatabase::class.java, "database-name"
).addMigrations(MIGRATION_1_2)
 .build()

By calling addMigrations(MIGRATION_1_2), you instruct Room to apply this migration when upgrading the database from version 1 to 2.

Handling Multiple Migrations

As your application continues to evolve, you might need to implement multiple migrations. Room allows you to chain migrations sequentially. Suppose you now want to add another table called orders in version 3. You would define another migration:


val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE orders (orderId INTEGER PRIMARY KEY, userId INTEGER, orderDate TEXT)")
    }
}

Now, include both migrations when building your database:


val db = Room.databaseBuilder(
    applicationContext,
    AppDatabase::class.java, "database-name"
).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
 .build()

By specifying multiple migrations, Room ensures that each migration is applied in sequence, maintaining the integrity of your database schema across versions.

Testing Migrations

Testing your migrations is crucial to ensure that they work as expected and that no data is lost during the process. Room provides a MigrationTestHelper class to facilitate testing. Here’s an example of how you can test the migration from version 1 to 2:


@RunWith(AndroidJUnit4::class)
class MigrationTest {

    private val TEST_DB = "migration-test"

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

    @Test
    fun migrate1To2() {
        var db = helper.createDatabase(TEST_DB, 1)
        // Insert data using version 1 schema
        db.execSQL("INSERT INTO users (userId, userName) VALUES (1, 'John Doe')")

        // Prepare for the next version
        db.close()

        // Re-open the database with version 2 and apply the migration
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)

        // Validate the data
        val cursor = db.query("SELECT * FROM users WHERE userId = 1")
        assertTrue(cursor.moveToFirst())
        assertEquals(cursor.getInt(cursor.getColumnIndex("userId")), 1)
        assertEquals(cursor.getString(cursor.getColumnIndex("userName")), "John Doe")
        cursor.close()
    }
}

In this test, we create a database with the initial version (1), insert some test data, and then apply the migration to version 2, validating that the data remains intact.

Best Practices for Room Migrations

  • Plan Ahead: Design your database schema with future changes in mind. Anticipate potential schema modifications and structure your entities to accommodate growth.
  • Keep Migrations Simple: Each migration should perform a single, straightforward change. This approach reduces complexity and makes testing easier.
  • Test Thoroughly: Always test migrations to ensure data integrity. Use both unit tests and integration tests to cover different scenarios.
  • Backup Data: Before applying migrations in a production environment, ensure you have a backup of the existing data. This precaution helps recover from unforeseen issues.
  • Document Changes: Maintain clear documentation of each migration, including the reason for the change and any potential impacts on the application.

Conclusion

Database migrations are a critical aspect of managing evolving applications. Room's migration framework provides a structured and reliable way to handle schema changes, ensuring that your app's data remains consistent and accessible. By following best practices and thoroughly testing your migrations, you can confidently manage your database's evolution, providing users with a seamless experience as your application grows and improves.

Now answer the exercise about the content:

What is the primary purpose of using Room's migration feature in Android app development?

You are right! Congratulations, now go to the next page

You missed! Try again.

Article image Using Room for Database Management: Using Room with LiveData

Next page of the Free Ebook:

84Using Room for Database Management: Using Room with LiveData

8 minutes

Earn your Certificate for this Course for Free! by downloading the Cursa app and reading the ebook there. Available on Google Play or App Store!

Get it on Google Play Get it on App Store

+ 6.5 million
students

Free and Valid
Certificate with QR Code

48 thousand free
exercises

4.8/5 rating in
app stores

Free courses in
video, audio and text