Scenario 4: version increased, migration provided ? data is kept
To keep the user’s data, we need to implement a migration. Since the schema doesn’t change, we just need to provide an
empty migration implementation
and tell Room to use it.
@Database(entities = {User.class},
version = 2)
public abstract class UsersDatabase extends RoomDatabase {
…
static final Migration
MIGRATION_1_2
= new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Since we didn't alter the table, there's nothing else to do here.
}
};
…
database = Room.
databaseBuilder
(context.getApplicationContext(),
UsersDatabase.class, "Sample.db")
.addMigrations(
MIGRATION_1_2
)
.build();
When running the app, Room does the following:
Step 1: Try to upgrade from version 1 (installed on device) to version 2
- Trigger the empty migration ?
- Update the identity hash in the
room_master_table
?
Step 2: Try to open the database
- Identity hash of the current version and the one saved in the
room_master_table
are the same. ?
So now, our app opens, and the user’s data is migrated! ??
Migration with simple schema changes
Let’s add another column:
last_update
, to our
users
table, by modifying the
User
class. In the
UsersDatabase
class we need to do the following changes:
1. Increase the version to 3
@Database(entities = {User.class},
version = 3
)
public abstract class UsersDatabase extends RoomDatabase
2. Add a Migration from version 2 to version 3
static final Migration
MIGRATION_2_3
= new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE users "
+ " ADD COLUMN last_update INTEGER");
}
};
3. Add the migration to the Room database builder:
database = Room.
databaseBuilder
(context.getApplicationContext(),
UsersDatabase.class, "Sample.db")
.addMigrations(
MIGRATION_1_2
,
MIGRATION_2_3
)
.build();
When running the app, the following steps are done:
Step 1: Try to upgrade from version 2 (installed on device) to version 3
- Trigger the migration and alter the table, keeping user’s data ?
- Update the identity hash in the
room_master_table
?
Step 2: Try to open the database
- Identity hash of the current version and the one saved in the
room_master_table
are the same. ?
Migrations with complex schema changes
SQLite’s
ALTER TABLE…
command is
quite limited
. For example, changing the id of the user from an
int
to a
String
takes several steps:
- create a new temporary table with the new schema,
- copy the data from the
users
table to the temporary table,
- drop the
users
table
- rename the temporary table to
users
Using Room, the Migration implementation looks like this:
static final Migration
MIGRATION_3_4
= new Migration(3, 4) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Create the new table
database.execSQL(
"CREATE TABLE users_new (userid TEXT, username TEXT, last_update INTEGER, PRIMARY KEY(userid))");
// Copy the data
database.execSQL(
"INSERT INTO users_new (userid, username, last_update) SELECT userid, username, last_update FROM users");
// Remove the old table
database.execSQL("DROP TABLE users");
// Change the table name to the correct one
database.execSQL("ALTER TABLE users_new RENAME TO users");
}
};
Multiple database version increments
What if your users have an old version of your app, running database version 1, and want to upgrade to version 4? So far, we have defined the following migrations: version 1 to 2, version 2 to 3, version 3 to 4, so Room will trigger all migrations, one after another.
Room can handle more than one version increment: we can define a migration that goes from version 1 to 4 in a single step, making the migration process faster.
static final Migration
MIGRATION_1_4
= new Migration(1, 4) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Create the new table
database.execSQL(
"CREATE TABLE users_new (userid TEXT, username TEXT, last_update INTEGER, PRIMARY KEY(userid))");
// Copy the data
database.execSQL(
"INSERT INTO users_new (userid, username, last_update) SELECT userid, username, last_update FROM users");
// Remove the old table
database.execSQL("DROP TABLE users");
// Change the table name to the correct one
database.execSQL("ALTER TABLE users_new RENAME TO users");
}
};
Next, we just add it to the list of migrations:
database = Room.
databaseBuilder
(context.getApplicationContext(),
UsersDatabase.class, "Sample.db")
.
addMigrations
(
MIGRATION_1_2
,
MIGRATION_2_3
,
MIGRATION_3_4
,
MIGRATION_1_4
)
.build();
Note that the queries you write in the
Migration.migrate
implementation are not compiled at run time, unlike the queries from your DAOs. Make sure that you’re
implementing tests for your migrations
.
Show me the code
You can check out the implementation in
this sample app
. To ease the comparison every database version was implemented in its own flavor:
- sqlite
? Uses SQLiteOpenHelper and traditional SQLite interfaces.
- room
? Replaces implementation with Room and provides migration to version 2
- room2
? Updates the DB to a new schema, version 3
- room3
? Updates the DB to a new, version 4. Provides migration paths to go from version 2 to 3, version 3 to 4 and version 1 to 4.
Conclusion
Has your schema changed? Just increase the database version and write a new
Migration
implementation. You’ll ensure that your app won’t crash and your user’s data won’t be lost. It’s as easy as flipping a switch!
But, how do you know if you flipped the right switch? How do you test that you migrated from your
SQLiteDatabase
implementation to Room correctly, that you implemented the correct migration between different database versions, or that your database is indeed starting as it should in a specific version? We talked about
testing migrations
in detail, covering several different scenarios here: