Samstag, 19. Oktober 2013

BARACUS from Scratch : Part 3 - Persistence Mapping Basics

Table of Contents

Previous Tutorial : Dependency Injection and IOC

In this tutorial I am going to explain You the basic fundamentals You need to know for storing and loading data in the application's database. There are four steps necessary to define an entity after defining the OpenHelper : Define the entity class and wire the fields, define a DAO taking care of the mapping, define a migr8-Implementation creating the table and add it to the migration steps associated with a step number indicating the target database version.

Download the sourcecode to this tutorial from Github

Step 1 : The OpenHelper

If You followed the tutorial from the prior chapter, You can skip this step. Otherwise, just follow the instructions of this step.

You need an Implementation of the BaracusOpenHelper to manage Your database. Simply derive the class and fit it with a constructor taking the Android context. The other two constructor parameters need to be set by Your implementation and will define the database filename and the target version of the database:


/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
public class OpenHelper extends BaracusOpenHelper {

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

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

This class needs to be registered to the application context :

package org.baracus.application;

import org.baracus.context.BaracusApplicationContext;
import org.baracus.dao.BaracusOpenHelper;
import org.baracus.service.CustomerService;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
public class ApplicationContext extends BaracusApplicationContext {

    static {
        registerBeanClass(OpenHelper.class);
        registerBeanClass(CustomerService.class);
    }

}

That's all. Now we can proceed to the next step, defining the entity class.

Step 2 : Defining the entity class

To keep the entity management as easy as as possible, all entity classes derive from ModelBase class. The first constant You'll define will be the table name inside of the database:


package org.baracus.model;

import org.baracus.orm.ModelBase;
import org.baracus.orm.Field;
import org.baracus.orm.FieldList;

import java.util.List;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
public class BankAccount extends ModelBase {

    public static final String TABLE_BANK_ACCOUNT = "bank_account";


Every persistent class has an indexed list of all fields, in hibernate terms you would call this metadata. The major difference is, that the metadata is held statically inside of the persistence class. Therefore You need to define a Fieldlist carrying the type. To inherit the primary key surrogate field (a generated, unique Long number), You must add the superclass's FieldList to the classses field list and then add the class fields. This stuff should be static; using static and final is a major performance thing inside of a mobile devices. To make the indexing easier, I use an iterator int to keep on track:


public class BankAccount extends ModelBase {

    public static final String TABLE_BANK_ACCOUNT = "bank_account";

    private static int columnIndex= ModelBase.fieldList.size();

    private String bankName;
    private String iban;

    public static final FieldList fieldList = new FieldList(BankAccount.class.getSimpleName());
    public static final Field bankNameCol = new Field("bank_name", columnIndex++);
    public static final Field ibanCol = new Field("iban", columnIndex++);


    static {
        fieldList.add(ModelBase.fieldList);
        fieldList.add(bankNameCol);
        fieldList.add(ibanCol);
    }


As You can see, I added the bank name and the international bank number to my entity class. Then, I define the fields (all static final) and then add the fields inside of a static init block to the fields.

Finally, I define getters and setters and my entity definition is ready to use:


package org.baracus.model;

import org.baracus.orm.ModelBase;
import org.baracus.orm.Field;
import org.baracus.orm.FieldList;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
public class BankAccount extends ModelBase {

    public static final String TABLE_BANK_ACCOUNT = "bank_account";

    private static int columnIndex= ModelBase.fieldList.size();

    private String bankName;
    private String iban;

    public static final FieldList fieldList = new FieldList(BankAccount.class.getSimpleName());
    public static final Field bankNameCol = new Field("bank_name", columnIndex++);
    public static final Field ibanCol = new Field("iban", columnIndex++);


    static {
        fieldList.add(ModelBase.fieldList);
        fieldList.add(bankNameCol);
        fieldList.add(ibanCol);
    }

    public BankAccount() {
        super(TABLE_BANK_ACCOUNT);
    }

    public String getBankName() {
        return bankName;
    }

    public void setBankName(String bankName) {
        this.bankName = bankName;
    }

    public String getIban() {
        return iban;
    }

    public void setIban(String iban) {
        this.iban = iban;
    }

}
That's it.

Step 3 : Defining the Table

If You want to store and load data from the database, You have to create a table inside of the app's DB. Therefore, You'll have to define a MigrationStep. For the beginning, let us simply define a single migration step for the single table we have.

In a later tutorial, I am going to explain You migr8 and the concept of database model versioning detailled.

So, in order to keep the java model and the database model synchronously and to avoid naming clashes, we simply reuse the table and the column name identifiers.

Our database metamodel class (which is an implementation of MigrationStep)  is going to look like this :

package org.baracus.migr8;

import android.database.sqlite.SQLiteDatabase;
import org.baracus.model.BankAccount;
import org.baracus.util.Logger;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 * To change this template use File | Settings | File Templates.
 */
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;
    }
}

For myself, I discovered starting with version 100 (=1.0.0) as a good common sense, because it allows you to have your model versioned having fixed length (=3 chars) integer number with much space for migration (100 <= version < 1000).  

You can individually choose Your numbering; the only thing You have to regard, is the fact, that Your model version number increases with every new model.
Now we have the database ddl section covered. To make use of it, we register it in the OpenHelper using the addMigrationStep function. The implementation of the getModelVersionNumber function will take care of the correct order of the steps:

package org.baracus.application;

import android.content.Context;
import org.baracus.dao.BaracusOpenHelper;
import org.baracus.migr8.ModelVersion100;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
public class OpenHelper extends BaracusOpenHelper {

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

    static {
        addMigrationStep(new ModelVersion100());
    }

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

Finally, we have to define a DataAccessObject in order to access the entity.

Step 4 : Defining the DAO

In order to access Your data, the DAO pattern (Data Access Object) is a prooved way to perform database read and write operation. The Baracus ORM leverages DAO as the standard way for db operations.

Notice : Be sure, that the bean has got a public default constructor!

Therefore you have to inherit and implement a BaseDao. The BaseDao is a generic which needs to be parameterized :


package org.baracus.dao;

import org.baracus.model.BankAccount;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
public class BankAccountDao extends BaseDao {

    /**
     * Lock the DAO of
     */
    public BankAccountDao() {
        super(BankAccount.class);
    }

    @Override
    public RowMapper getRowMapper() {
        return null; // TODO : implement
    }
}
Finally, You have to provide a RowMapper implementation telling the Dao how to map data to objects and vice versa. Let us start the most simple methods :

getAffectedTable simply returns the BankAccount's table constant, getFieldList returns the BankAccount's FieldList constant.

If an entity has got a name attribute, You can return the column name here and be able to make use of the DAO's getByName function. So the RowMapper implementation (notice, static final and inline) looks like this :


package org.baracus.dao;

import android.content.ContentValues;
import android.database.Cursor;
import org.baracus.model.BankAccount;
import org.baracus.orm.Field;
import org.baracus.orm.FieldList;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 * To change this template use File | Settings | File Templates.
 */
public class BankAccountDao extends BaseDao<BankAccount> {

    /**
     * Lock the DAO of
     */
    protected BankAccountDao() {
        super(BankAccount.class);
    }

    private static final RowMapper<BankAccount> rowMapper = new RowMapper<BankAccount>() {
        @Override
        public BankAccount from(Cursor c) {
            return null;  //To change body of implemented methods use File | Settings | File Templates.
        }

        @Override
        public String getAffectedTable() {
            return BankAccount.TABLE_BANK_ACCOUNT;
        }

        @Override
        public FieldList getFieldList() {
            return BankAccount.fieldList;
        }

        @Override
        public Field getNameField() {
            return BankAccount.bankNameCol;
        }

        @Override
        public ContentValues getContentValues(BankAccount item) {
            return null;  //To change body of implemented methods use File | Settings | File Templates.
        }
    };

    @Override
    public RowMapper<BankAccount> getRowMapper() {
        return rowMapper;
    }
}


The crucial - and still missing - part of the class is the mapping itself. Here we have to define how the class is mapped from a row and back. Mapping the row to an object is done on the cursor object.

package org.baracus.dao;

import android.content.ContentValues;
import android.database.Cursor;
import org.baracus.model.BankAccount;
import org.baracus.orm.Field;
import org.baracus.orm.FieldList;
import org.baracus.orm.LazyCollection;

import java.util.List;

import static org.baracus.model.BankAccount.bankNameCol;
import static org.baracus.model.BankAccount.ibanCol;
import static org.baracus.orm.ModelBase.idCol;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 * To change this template use File | Settings | File Templates.
 */
public class BankAccountDao extends BaseDao<BankAccount> {

    /**
     * Lock the DAO of
     */
    protected BankAccountDao() {
        super(BankAccount.class);
    }

    private static final RowMapper<BankAccount> rowMapper = new RowMapper<BankAccount>() {
        @Override
        public BankAccount from(Cursor c) {
            BankAccount result = new BankAccount();
            result.setId(c.getLong(idCol.fieldIndex));
            result.setIban(c.getString(ibanCol.fieldIndex));
            result.setBankName(c.getString(bankNameCol.fieldIndex));
            result.setTransient(false);
            return result; 
}

        @Override
        public String getAffectedTable() {
            return BankAccount.TABLE_BANK_ACCOUNT;
        }

        @Override
        public FieldList getFieldList() {
            return BankAccount.fieldList;
        }

        @Override
        public Field getNameField() {
            return bankNameCol;
        }

        @Override
        public ContentValues getContentValues(BankAccount account) {
            ContentValues result = new ContentValues();
            if (account.getId() != null) { result.put(idCol.fieldName, account.getId()); }
            if (account.getIban() != null) { result.put(BankAccount.ibanCol.fieldName, account.getIban()); }
            if (account.getBankName() != null) { result.put(bankNameCol.fieldName, account.getBankName()); }
            return result;
        }
    };

    @Override
    public RowMapper<BankAccount> getRowMapper() {
        return rowMapper;
    }
}

Notice, since Baracus determines the DB insert and update operation out of the transient field, it is of VITAL IMPORTANCE, that the last call in Your from()-function is setTransient(false)

As You can see, the mapping is quite simple; the mapping from the cursor simply wires the field using the class field's field indexes in order to determine the correct position.

Finally, we have to register the dao bean in the application context:


package org.baracus.application;

import org.baracus.context.BaracusApplicationContext;
import org.baracus.dao.BankAccountDao;
import org.baracus.dao.BaracusOpenHelper;
import org.baracus.service.CustomerService;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
public class ApplicationContext extends BaracusApplicationContext {

    static {
        registerBeanClass(OpenHelper.class);

        registerBeanClass(BankAccountDao.class);
        registerBeanClass(CustomerService.class);
    }

}

That's all. In a later tutorial, I am going to show You how to use references and lazy collections in order to make Your model navigable.

I am completely conscious about the fact, that this is far away from the comfort of an implementation of Java Persistence Architecture. I had two major requirements to my ORM : a) no reflection if possible and b) no generated code. There exist frameworks for android supporting this, but I chose this way for the sake of control about all Your operations. However, You normally never have to map an entity never and mobile applications are far away from having dozens of database tables ;-)

Finish : Creating and retrieving data

To proof the DAO functionality, We are going to create and retrieve an Account item. Therefore we create and register the service class BankAccountService carry a createOrDumpAccount method.

So the service is actually looking like this :


package org.baracus.service;

import org.baracus.annotations.Bean;
import org.baracus.dao.BankAccountDao;
import org.baracus.model.BankAccount;
import org.baracus.util.Logger;

/**
 * Created with IntelliJ IDEA.
 * User: marcus
 */
@Bean
public class BankAccountService {

    private static final Logger logger = new Logger(BankAccountService.class);

    @Bean
    BankAccountDao bankAccountDao;

    public void createAndOrDumpAccount() {
        BankAccount account = bankAccountDao.getByName("FOOBANK");
        if (account == null) {
            logger.info("No account for FOOBANK was found. I am going to create one.");
            account = new BankAccount();
            account.setBankName("FOOBANK");
            account.setIban("MARMELADEFOO666");
            bankAccountDao.save(account);
        } else {
            logger.info("ACCOUNT FOUND. Id is $1",account.getId());
            logger.info("String value $1",account.toString());
        }
    }
}



and we are going to wire it to the button function in the MainActivity :
// ...
public class HelloAndroidActivity extends Activity {

   // ...

    @Bean
    BankAccountService bankAccountService;

   // ...

    public void onButtonTestClicked(View v) {
        customerService.testService();
        bankAccountService.createAndOrDumpAccount();
    }

}


... done! Now You can watch the results in Your logfile :


10-18 20:42:34.260: DEBUG/TUTORIAL_APP(12699): CustomerService Hooray! I have been called!
10-18 20:42:34.260: DEBUG/TUTORIAL_APP(12699): BankAccountService No account for FOOBANK was found. I am going to create one.

... second click ...

10-18 20:42:54.630: DEBUG/TUTORIAL_APP(12699): CustomerService Hooray! I have been called!
10-18 20:42:54.630: DEBUG/TUTORIAL_APP(12699): BankAccountService ACCOUNT FOUND. Id is 1
10-18 20:42:54.630: DEBUG/TUTORIAL_APP(12699): BankAccountService String value ModelBase{id=1, isTransient=false, tableName='bank_account'}

Now You can define any persistent class You want. 

One final word to the four magic steps (Model, DDL, Dao, Wire+Use)... If you have a release version of Your application, DO NOT ALTER ANY MIGRATION STEPS! All modifications to the database model HAVE to be done via a new MigrationStep implementation containing the matching ALTER TABLE's!

Follow Up : Dependency injection and IOC

Keine Kommentare:

Kommentar veröffentlichen