Tuesday, April 12, 2011

Android persistence framework

Lately I've been doing a large amount of development work on the Android platform and have found with as many things standardizing at a lower level is a great way to enforce a concern. One of the things I've noticed is that many people have a difficult time dealing with entity relationships as well as the sqlite database in general. So, what I did was develop a genericized DAO structure as well as introduce a dao manager that can be grown by the applicaiton.

Unfortunately this strategy has a few limitations.
1) The sqlite api in Android does not support a datatype retrieval mechanism as does JDBC
2) There are a few tweaks that still need to occur in this to support CLOB and BLOB formats
3) The foreign key / entity relationship mapping still needs some work done on it for lazy loading
3) ... and some other random things

The basic grammar is simple.

An Entity has a Dao
An Entity has an ID which is a java.lang.Long
An Entity is persistable (save and delete)
An Entity can have relationships to other entities.
An Entity can be derived and its DAO can be derived as well.

Largely this is a light weight implementation of JPA for Android to simplify database persistence and remove the need for developers to have to hand code everything.

Largely the genericized DAO is a port of an implementation I've done for several other technologies including NHibernate, Hibernate and JPA and a lot of the following annotations and enumeration types are simply there to guide the engine.

Column.java:
@Retention(RetentionPolicy.RUNTIME)  
public @interface Column {
 String name() default "";
 
 /**
  * Should return a String, Long, Integer, BLOB
  * @return
  */
 EColumnType type() default EColumnType.String;
 
 /**
  * which direction are we going in relation to the {@link Entity}
  * @return
  */
 EOperation op() default EOperation.Inflate;
}

EColumnType.java:
public enum EColumnType {

 Integer(),
 Long(),
 String(),
 Blob(),
 Double(),
 Float(),
 Short();

 EColumnType() {  
 }
}


EOperation.java:
public enum EOperation {
 
 Inflate(),
 Deflate();

 EOperation() {  
 }
}

IGenericDao.java:
public interface IGenericDao<E extends IEntity> {

    public E findById(long id) throws Exception;

    public void save(E entity) throws Exception;
    
    public void delete(E entity) throws Exception;
    
    public List findAll() throws Exception; 
}

And all of the magic really happens in the Entity and GenericDao classes. The entity for all intensive purposes is a template that provides the ability to proxy to its concrete DAO and invoke the save method.

Entity.java
public abstract class Entity implements IEntity {

 static final String TAG = "Entity";

 protected Integer m_id;

 static Map inflationMap = null;

 static Map deflationMap = null;

 static Map methodAnnotationMap = null;
 
 static Gson gson = new Gson();

 /**
  * Default constructor for the generic. Will additionally normalize all
  * input methods for the column names to upper case
  */
 public Entity() {

  // load all of the column annotations related to a method
  synchronized (this.getClass()) {
   if (inflationMap == null && deflationMap == null) {
    inflationMap = new ConcurrentHashMap();
    deflationMap = new ConcurrentHashMap();
    methodAnnotationMap = new ConcurrentHashMap();

    Method[] methods = this.getClass().getMethods();

    for (Method m : methods) {
     if (m.isAnnotationPresent(Column.class)) {
      Column c = m.getAnnotation(Column.class);

      if (c.op() == EOperation.Inflate) {
       inflationMap.put(c.name().toUpperCase(), m);
       methodAnnotationMap.put(m, c);
      }

      if (c.op() == EOperation.Deflate) {
       deflationMap.put(c.name().toUpperCase(), m);
       methodAnnotationMap.put(m, c);
      }
     }
    }
   }
  }
 }

 public Entity(Integer id) {
  this();
  m_id = id;
 }

 public Integer getId() {
  return m_id;
 }

 public void setId(Integer id) {
  m_id = id;
 }

 private java.util.Date m_lastModifiedDate;

 /**
  * lastModifiedDate is exactly that. Will have column def of
  * last_modified_date
  */
 // @Override
 public java.util.Date getLastModifiedDate() {
  return m_lastModifiedDate;
 }

 // @Override
 public void setLastModifiedDate(java.util.Date date) {
  this.m_lastModifiedDate = date;
 }

 private java.util.Date m_createDate;

 /**
  * column definition create_date
  */
 public java.util.Date getCreateDate() {
  return m_createDate;
 }

 public void setCreateDate(java.util.Date date) {
  this.m_createDate = date;
 }

 @Override
 public boolean equals(Object o) {
  if (!this.getClass().equals(o.getClass()))
   return false;
  else {
   Entity e = (Entity) o;

   if (e.getId().equals(this.getId()))
    return true;
   else
    return false;
  }
 }

 public int hashCode() {
  int result;
  result = 29 * this.getClass().hashCode() + getId().hashCode();
  return result;
 }

 @Override
 public String toString() {
  return String.format("[%s#%d]", this.getClass(), getId());
 }

 public void onLoad() {
  doOnLoad();
 }

 protected void doOnLoad() {
 }

 public void save() throws Exception {
  Log.d(TAG, String.format("Save() called for: %s", this));

  doSave();
 }

 protected void doSave() throws Exception {
 }

 public void delete() throws Exception {
  Log.d(TAG, String.format("Delete() called for: {0}", this));

  doDelete();
 }

 protected void doDelete() throws Exception {
 }

 /**
  * Will generate a set of ContentValues to be persisted into the database
  * structure. This method does this by leveraging the creation of several
  * annotation driven maps based upon {@link Column}
  * 

* This method can be overriden to not leverage the reflection driven * strategy */ public ContentValues deflate() throws Exception { ContentValues values = null; Log.d("DEFLATE", String.format("deflating %s", this.toString())); if (deflationMap != null && deflationMap.size() > 0) { values = new ContentValues(); for (String s : deflationMap.keySet()) { Method m = deflationMap.get(s); Column col = methodAnnotationMap.get(m); Log.d("DEFLATE", String.format("Column identified as %s, %s, %s", col.name(), col.type(), col.op())); Object o = null; switch (col.type()) { case Integer: o = m.invoke(this, (Object[])null); Log.d("DEFLATE", String.format("Deflation value of: %s", o.toString())); values.put(col.name(), (Integer)o); break; case String: o = m.invoke(this, (Object[])null); Log.d("DEFLATE", String.format("Deflation value of: %s", o.toString())); values.put(col.name(), (String)o); break; case Short: o = m.invoke(this, (Object[])null); Log.d("DEFLATE", String.format("Deflation value of: %s", o.toString())); values.put(col.name(), (Short)o); break; case Long: o = m.invoke(this, (Object[])null); Log.d("DEFLATE", String.format("Deflation value of: %s", o.toString())); values.put(col.name(), (Long)o); break; // case Blob: // values.put(col.name(), m.invoke(this, (Object[])null)); // break; } } } return values; } /** * Will generate a set of ContentValues to be persisted into the database * structure. This method does this by leveraging the creation of several * annotation driven maps based upon {@link Column} *

* This method can be overriden to not leverage the reflection driven * strategy */ public void inflate(android.database.Cursor c) throws Exception { int idx = -1; // to find a possible permutation of id if ((idx = c.getColumnIndex("id")) != -1 || (idx = c.getColumnIndex("ID")) != -1 || (idx = c.getColumnIndex("Id")) != -1) setId(new Integer(c.getInt(idx))); if (inflationMap == null || inflationMap.size() == 0) return; String[] names = c.getColumnNames(); for (String s : names) { Method m = null; if ((m = inflationMap.get(s.toUpperCase())) != null) { int index = c.getColumnIndex(s); setValue(m, c, index); } } } /** * Leverages the information from the {@link Column} and the * {@link EColumnType} to determine which set to pass. * * @param m * the method we are setting * @param c * The data curosr * @param idx * the column index * @throws Exception */ final void setValue(Method m, Cursor c, int idx) throws Exception { Column col = methodAnnotationMap.get(m); switch (col.type()) { case Integer: m.invoke(this, new Integer(c.getInt(idx))); break; case String: m.invoke(this, c.getString(idx)); break; case Short: m.invoke(this, new Short(c.getShort(idx))); break; case Long: m.invoke(this, new Long(c.getLong(idx))); break; case Blob: m.invoke(this, c.getBlob(idx)); break; } } }



GenericDao.java
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.*;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

@SuppressWarnings("unchecked")
public abstract class GenericDao
  implements IGenericDao {

 protected Class m_persistentClass;

 static final String TAG = "GenericDao";

 SQLiteDatabase db;

 public GenericDao(SQLiteDatabase db) {
  this.db = db;
  try {
   ParameterizedType genericSuperclass = (ParameterizedType) getClass()
     .getGenericSuperclass();
   this.m_persistentClass = (Class) genericSuperclass
     .getActualTypeArguments()[0];

  } catch (Exception e) {
  }
 }
 
 
 public SQLiteDatabase getDatabase() {
  return this.db;
 }

 public void delete(E entity) throws Exception {
  
  Log.d(TAG, String.format("Deleting a %s tfrom %s", entity.getClass().getName(), m_persistentClass.getSimpleName()));
  
  db.delete(m_persistentClass.getSimpleName(), "id = ?", new String[] { entity
    .getId().toString() });
 }

 public List findAll() throws Exception {

  List retVals = null;

  Cursor c = null;

  try {
   c = db.rawQuery("select * from " + m_persistentClass.getSimpleName(), null);

   if (c.moveToFirst()) {
    retVals = new ArrayList();
    do {
     E e = newInstance();

     e.inflate(c);

     retVals.add(e);     
    } while ( c.moveToNext() );
   }
  } finally {
   if (c != null) {
    c.close();
    c = null;
   }
  }

  return retVals;
 }

 public E findById(long id) throws Exception {
  Cursor c = null;
  E e = null;
  try {
   c = db.query(m_persistentClass.getSimpleName(), null, "id = ?",
     new String[] { Long.toString(id) }, null, null, null);

   if (c.moveToFirst()) {
    c.moveToFirst();

    e = newInstance();

    e.inflate(c);
   }
  } finally {
   if (c != null) {
    c.close();
    c = null;
   }
  }

  return e;
 }

 public void save(E entity) throws Exception {
  if (entity.getId() == null) {
   ContentValues cv = entity.deflate();
   
   Log.d(TAG, String.format("Saving a %s to %s", entity.getClass().getName(), m_persistentClass.getSimpleName()));

   long id = db.insert(m_persistentClass.getSimpleName(), null, cv);
   
   Log.d(TAG, String.format("ID returned as %s", Integer.toString((int)id)));

   entity.setId(new Integer((int)id));
  } else {
   Log.d(TAG, String.format("Updating %s", this.toString()));
      
   db.update(m_persistentClass.getSimpleName(), entity.deflate(), "id = ?", new String[]{entity.getId().toString()});
  }
 }

 /**
  * Will take in a string query and execute it
  * 
  * @param query
  *            the query to execute
  * @return a instance of E
  * @note possibly create a permutation of this method which takes in a set
  *       of objects and will infer the type to be added to the query
  */
 public E queryObject(String query) throws Exception {
  
  return queryObject(query, (Object[])null);
 }

 /**
  * Is expected to return a unique object
  * 
  * @param query
  * @param params
  * @return
  * @throws Exception
  */
 public E queryObject(String query, Object... params) throws Exception {

  E e = null;

  Cursor c = null;

  try {
   String[] vars = null;

   if (params != null) {
    vars = new String[params.length];

    for (int i = 0; i < params.length; i++) {
     vars[i] = params[i].toString();
    }
   }

   c = db.rawQuery(query, vars);
   
   if (c.moveToFirst()) {
    e = newInstance();
    e.inflate(c);
   }   
  } finally {
   if (c != null) {
    c.close();
    c = null;
   }
  }

  return e;
 }

 /**
  * Will take in a string query and execute it
  * 
  * @param query
  *            the query to execute
  * @return a list of E
  * @note possibly create a permutation of this method which takes in a set
  *       of objects and will infer the type to be added to the query
  */
 public List queryObjects(String query) throws Exception {
  return queryObjects(query, (Object[]) null);
 }

 public List queryObjects(String query, Object... params)
   throws Exception {
  List retVals = null;

  Cursor c = null;

  try {

   String[] vars = null;

   if (params != null) {
    vars = new String[params.length];

    for (int i = 0; i < params.length; i++) {
     vars[i] = params[i].toString();
    }
   }

   c = db.rawQuery(query, vars);

   if (c.moveToFirst()) {
    retVals = new ArrayList();
    
    do {
     E e = newInstance();

     e.inflate(c);

     retVals.add(e);
    } while ( c.moveToNext() );
   }
  } finally {
   if (c != null) {
    c.close();
    c = null;
   }
  }

  return retVals;
 }

 /**
  * Due to java type erasure we capture the meta data as input on the ctor to
  * factory later
  * 
  * @return
  */
 E newInstance() {
  E instance = null;
  try {
   instance = (E) m_persistentClass.newInstance();
  } catch (Exception e) {
   Log.e(TAG, e.getMessage(), e);
  }
  return instance;
 }

}

An example would be simple.

DaoManager.java:
public class DaoManager {

 Context context;

 MySQLiteOpenHelper helper;

 SomeEntityDao someEntityDao = null;

 private static DaoManager instance;

 private DaoManager(Context context) {
  this.context = context;

  helper = new MySQLiteOpenHelper(context);
 }

 public static void initialize(Context context) {
  if (instance == null)
   instance = new DaoManager(context);
 }

 public static DaoManager getInstance() {
  return instance;
 }

 public synchronized SomeEntityDao getSomeEntityDao() {
  if (watchListDao == null)
   someEntityDao = new SomeEntityDao(helper.getWritableDatabase());

  return watchListDao;
 }

 public class MySQLiteOpenHelper extends SQLiteOpenHelper {

  public MySQLiteOpenHelper(Context c) {
   super(c, "TEST", null, 2);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
   Log.d("DB", "onCreate");
   String table = "Your ddl";

   db.execSQL(table);
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
   Log.d("DB", "onUpgrade");
   db.execSQL("drop table watchlist;");

   String table = "Your ddl";

   db.execSQL(table);
  }
 }
}

SomeEntity.java:
public class SomeEntity extends Entity {
 
 public SomeEntity() {
  
 }
 
 public SomeEntity(String value, int i) {
  this.value = value;
  this.i = i;
 }

 @Override
 protected void doSave() throws Exception {
  DaoManager.getInstance().getSomeEntityDao().save(this);
 }
 
 
 @Override
 protected void doDelete() throws Exception {
  DaoManager.getInstance().getSomeEntityDao().delete(this);
 }
 
 
 String value;
 
 @Column(name="value", type=EColumnType.String, op=EOperation.Deflate)
 public String getValue() {
  return value;
 }

 @Column(name="value", type=EColumnType.String, op=EOperation.Inflate)
 public void setValue(String value) {
  this.value = value;
 }
 
 Integer i;
 @Column(name="intValue", type=EColumnType.Integer, op=EOperation.Deflate)
 public Integer getIntValue() {
  return i;
 }
 
 @Column(name="intValue", type=EColumnType.Integer, op=EOperation.Inflate)
 public void setIntValue(Integer value) {
  this.i = value;
 }
}

SomeEntityDao.java:
public class SomeEntityDao extends GenericDao {

 public SomeEntityDao(SQLiteDatabase db) {
  super(db);
  // TODO Auto-generated constructor stub
 }
}

The major advantage to this strategy is that it allows you to put your genericized persistence logic in a lower level shared library and develop a data access layer that is much more clean and extensible. Of course, with mobile (and or anything Android) not being as powerful as a server you'll want a lightweight implementation which is why the developer can additionally override the default implementation of Entity#deflate or Entity#inflate to make it less dynamic.

No comments: