Modular Migrations for Android Applications

July 14, 2015

With Android applications, there are two cases where the internal database for the app will need to be changed:

A user has just installed the app for the first time (or has cleared their application data), so no database currently exists.
A user has just upgraded their app to a new version that uses a newer database structure.

Using database migrations, we can handle both of these cases seamlessly. Database migrations are a convenient and consistent way to manage the structure of your database over time. You might think of them as a version control system (like Git) where migration “patches” are applied in a sequence to bring the target database up to date with the current version. This sequence of migrations can be applied to a target database of any version because each migration knows how to upgrade the target from its current version to the next version in the sequence, eventually reaching the most recent version. For the first case where the user has no database, this can be considered version 0. From that perspective, our two use cases become essentially the same.
In this example app, we will be using the OrmLiteSqliteOpenHelper abstract class from ORMLite which we extend to create a DatabaseHelper class that will handle running migrations and interacting with the application. OrmLiteSqliteOpenHelper defines two abstract methods we must implement:

public abstract class OrmLiteSqliteOpenHelper {
 // What to do when your database needs to be created.
 // This is the version=0 case
 public abstract void onCreate(SQLiteDatabase db, ConnectionSource connectionSource);

 // What to do when your database needs to be updated.
 // This is the version=x..N case
 public abstract void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion);
}

Since we are thinking about our “no database” case as just version=0, we can implement the onCreate method easily by handing off the task to the onUpgrade method:

@Override
public void onCreate(SQLiteDatabase db, ConnectionSource connectionSource) {
 onUpgrade(db, connectionSource, 0, DATABASE_VERSION);
}

Upgrading from version=X to version=N case is just applying a linear sequence of migration “patches”, so the onUpgrade method is also very simple:

@Override
public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
 while (oldVersion++ < newVersion) {
 Migration m = VersionManager.getMigration(oldVersion);
 if (!m.doUpgrade(db, connectionSource)) {
 // Unsuccessful, stop migrations
 return;
 }
 }
}

The Migration class defines the steps neccesary to alter the database from one version to the next. VersionManager simply contains a list of all the migrations in the order they should be run.
To create a new migration, subclass Migration and implement its abstract methods:

public class V1_AddExampleTable extends Migration {
 public String getDescription() {
 return "Add sample table";
 }

 public int getVersion() {
 return 1;
 }

 public void upgrade(SQLiteDatabase db, ConnectionSource connectionSource) throws SQLException {
 TableUtils.createTable(connectionSource, Example.class);
 }
}

Next, add the migration to the VersionManager‘s list of migrations:

public class VersionManager {
 private static Migration[] migrations = {
 new V1_AddExampleTable(),
 };
}

You may notice that DatabaseHelper.onUpgrade calls Migration.doUpgrade instead of the upgrade method we defined. This is because the Migration base class wraps the upgrade steps in a transaction which allows any changes made during the migration to be rolled-back in case something goes wrong.
When the application connects to the database, you should see the migration process logging its steps to logcat:

W//DatabaseHelper.java:35: main Creating new database using migrations
W//DatabaseHelper.java:41: main Upgrading db from v0 to v1...
I//Migration.java:12: main Running migration to v1: "Add example table"
I/TableUtils: creating table `example`
I/TableUtils: executed create table statement changed 1 rows: CREATE TABLE `example` (`id` INTEGER PRIMARY KEY AUTOINCREMENT )
I//Migration.java:32: main Migration to v1 OK!
I//DatabaseHelper.java:49: main Database upgrade complete!

Let’s add a column to the example table using the migration method:

public class V2_AddTextToExample extends Migration {
 public String getDescription() {
 return "Add text column to example table";
 }

 public int getVersion() {
 return 2;
 }

 public void upgrade(SQLiteDatabase db, ConnectionSource connectionSource) throws SQLException {
 try {
 DaoManager.lookupDao(connectionSource, Example.class)
 .executeRaw(
 "ALTER TABLE `" + DatabaseTableConfig.extractTableName(Example.class) + "`"
 + " ADD COLUMN " + Example.TEXT + " VARCHAR;"
 );
 } catch (SQLException e) {
 // Field might have been added by V1 migration when creating a new database
 Ln.i("example.text field already exists");
 }
 }
}

When the new version of the application is run, OrmLite will detect that the version of the database on the device does not match the current DATABASE_VERSION defined by the application code and will call DatabaseHelper.onUpgrade:

W//DatabaseHelper.java:34: main Upgrading db from v1 to v2...
I//Migration.java:12: main Running migration to v2: "Add text column to example table"
I//Migration.java:32: main Migration to v2 OK!
I//DatabaseHelper.java:42: main Database upgrade complete!

The final result is an implementation of DatabaseHelper that is cleaner, easier to understand, and more modular than the example given in the OrmLite docs, which suggests putting all the upgrade code inside the onUpgrade method.

Ready to get started?

Call us at 616-594-0269 or send us a note below.
Visit our office @ 452 Ada Drive SE Suite 300 Ada, Michigan 49301
Send us an e-mail @ info@michiganlabs.com