Dienstag, 6. Mai 2014

BARACUS from Scratch - Part 8 - Lifecycle support for your app with migr8

Table of Contents

In this tutorial I am going to show You, how to manage the database through the lifecycle of Your application. Managing the persistence model and the underlying database structures is a quite important section in the lifecycle management of every application installed on a device not reacheable by the distributor. migr8 is the proper solution for reducing the pain of this management. It leverages the android and sqlite builtins to make database migration as easy as possible. 

Previous Tutorial : Database hot backup and hot recovery

You can download the sourcecode from here

0 - Basics 


Why is the management of database backends a problem on a mobile application? Because You do not have the guarantee that all clients running an upgrade are on the same version level. So there is a simple strategy necessary to avoid problems: It takes two information elements to make an update to an update and not to a problem : The current version of the database and the designated targert version. All steps transiting from the installed version to the target versions need to be known by Your application. This is a basic feature provided by Android/Sqlite. All You need to do is to find a proper path for this transtions.

And here migr8 enters the game.

When You are dealing with persistence in Your application, the first bean you must register in Your ApplicationContext, is the OpenHelper:

public class ApplicationContext extends BaracusApplicationContext {

    static {
        registerBeanClass(OpenHelper.class);

        registerBeanClass(BankAccountDao.class);
        registerBeanClass(CustomerDao.class);
 ...
The OpenHelper is the core component of all DB Handling. So let us take a look onto it.

1 - The OpenHelper 


In order to make use of Baracus persistence, Your application has to register a customized OpenHelper. When You take a look into the tutorial-application's OpenHelper You are going to find already three model changes registered inside of it :

public class OpenHelper extends BaracusOpenHelper {

    private static final String DATABASE_NAME="tutorial-app.db";
    private static final int TARGET_VERSION=101;

    static {
        addMigrationStep(new ModelVersion100());
        addMigrationStep(new ModelVersion101());
    }

    /**
     * Open Helper for the android database
     *
     * @param mContext              - the android context
     */
    public OpenHelper(Context mContext) {
        super(mContext, DATABASE_NAME, TARGET_VERSION);
    }
}

Now let us check the three relevant sections of the OpenHelper. The first information defined is the target version and the name of the database file. These are passed within the constructor to the super constructor in the bottom of the code.
Any Upgrade to the model (which inflicts new MigrationStep implementations) results in increasing the TARGET_VERSION. Otherwise the steps are not hit and the model remains untouched.

And in the middle, three migration steps are added. A migration step contains all relevant information to run a migration to a certain version.

2 - The migration step 


So let us take a look on the ModelVersion100 Element :

public class ModelVersion100 implements MigrationStep {


    private static final Logger logger = new Logger(ModelVersion100.class);
    @Override
    public void applyVersion(SQLiteDatabase db) {

        String stmt  = "CREATE TABLE " + BankAccount.TABLE_BANK_ACCOUNT
                + "( "+ BankAccount.idCol.fieldName+" INTEGER PRIMARY KEY"
                + ", "+ BankAccount.bankNameCol.fieldName+ " TEXT"
                + ", "+ BankAccount.ibanCol.fieldName+ " TEXT"+
                  ")";
        logger.info(stmt);
        db.execSQL(stmt);

    }

    @Override
    public int getModelVersionNumber() {
        return 100;
    }
 }
In this migration step the BankAccount table is defined using the column metadata of the persistence bean. All You have to do is to define a correct sql creating the table. You remember the OpenHelper? It got an instance of all Your MigrationSteps:

        addMigrationStep(new ModelVersion100());

3 - Add a new migration step 


In order to demostrate the ease of use of migr8, let's modify the BankAccount entity. At first, we add the field, the metadata and the gettersetters for a field named comment to the BankAccount entity:

public class BankAccount extends AbstractModelBase {

    public static final String TABLE_BANK_ACCOUNT = "bank_account";

    ...
    public static final Field commentCol= new Field("comment", columnIndex++);


    static {
        ...
        fieldList.add(commentCol);
    }

    ...

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }
}

Next, we need to extend the BankAccountDao's RowMapper in order to map the new field to the bean:

public class BankAccountDao extends BaseDao<BankAccount> {

    private final RowMapper<BankAccount> rowMapper = new RowMapper<BankAccount>()    {
        @Override
        public BankAccount from(Cursor c) {
            BankAccount result = new BankAccount();
            ...  
            result.setComment(c.getString(commentCol.fieldIndex));

            return result;        
        }

        @Override
        public ContentValues getContentValues(BankAccount account) {
            ContentValues result = new ContentValues();
            ...  
            if (account.getComment() != null) {
                result.put(BankAccount.commentCol.fieldName, account.getComment());
            }

            return result;
        }
    };
}

Finally, in order to make the stuff work, we must alter the database table in order to append these column to it. Therefore we will implement a MigrationStep:

public class ModelVersion102 implements MigrationStep {


    private static final Logger logger = new Logger(ModelVersion102.class);
    @Override
    public void applyVersion(SQLiteDatabase db) {

        String stmt  = "ALTER TABLE " + BankAccount.TABLE_BANK_ACCOUNT
                + " ADD COLUMN "+BankAccount.customerIdCol.fieldName + " INTEGER";
        logger.info(stmt);
        db.execSQL(stmt);

    }

    @Override
    public int getModelVersionNumber() {
        return 102;
    }
}

In order to finish Our masterpiece, we simply have to add it to the list of migrationsteps in the OpenHelper and increase the target version number:

public class OpenHelper extends BaracusOpenHelper {

    private static final String DATABASE_NAME="tutorial-app.db";
    private static final int TARGET_VERSION=102;

    static {
        addMigrationStep(new ModelVersion100());
        addMigrationStep(new ModelVersion101());
        addMigrationStep(new ModelVersion102());
    }
    ...

That's it.

The next time You launch Your app the migration step will run and do the update job on Your data model. 

Best practice hints : Never ever modify an existing migration step once after it has gone to production! You will probably cause disfunctional versions of Your app. Always add a new migration step to Your app.

Conclusion


As You can see there is not much magic behind the migration step concept. But doing model modification in a stream of upgrades enables You to upgrade any version of the model of Your app without having any pain.

Follow Up : Writing Custom Validators