Sunday 24 June 2012

Android SQLite Database Design Pattern - Extensible, Blocking and Simple

This post is in response to my need to have an Android Database that will:

A) Never ever get into an illegal state, i.e. errors occur if you create/write/read tables at the same time.
B) Split up management of Database tables by creating classes for each one, but still using one single Database.
C) Reuse common Database functionality by creating helper methods.
D) Works at least in Android 2.1+.

Here is that solution - tested using multiple threads. I use a global application context to create the database, namely 'MyApplication.java' which is copied at the bottom of this post.

AbstractDBAdapter.java - Base class holding Database and helper methods.
public class AbstractDBAdapter
{

   private static final String DB_NAME = "CountriesDatabase";
   private static final String DB_TAG = "Countries Database Tag";
   private static final int DB_VERSION = 1;
   
   private volatile static DatabaseHelper mDbHelper;
   
   private static final Object lock = new Object();

   protected static final String COUNTRIES_TABLE_NAME = "CountriesTable";
   private static final String COUNTRIES_TABLE_CREATE = "CREATE TABLE " + COUNTRIES_TABLE_NAME
         + " (COUNTRY_NAME TEXT, COUNTRY_CAPITAL TEXT);";
   private static final String COUNTRIES_TABLE_DROP = "DROP TABLE IF EXISTS " + COUNTRIES_TABLE_NAME + ";";

   /**
    * Constructor - takes the context to allow the Database to be opened/created
    * 
    * @param context the Context within which to work
    */
   protected AbstractDBAdapter(Context context)
   {
      if (mDbHelper == null)
      {
         synchronized (AbstractDBAdapter.class)
         {
            if (mDbHelper == null)
            {
               if (context == null)
               {
                  throw new RuntimeException("Context is null!");
               }
               mDbHelper = new DatabaseHelper(context);
            }
         }
      }
   }

   /**
    * Helper Methods
    */
   private Cursor getCursor(String tableName, String args)
   {
      String SQL_STATEMENT = "";
      if (args != null)
      {
         SQL_STATEMENT = "SELECT * FROM " + tableName + " WHERE " + args;
      }
      else
      {
         SQL_STATEMENT = "SELECT * FROM " + tableName;
      }
      Cursor c = mDbHelper.getReadableDatabase().rawQuery(SQL_STATEMENT, null);
      return c;
   }

   protected String getString(String tableName, String args, String columnIndex)
   {
      synchronized (lock)
      {
         String text = "";
         Cursor c = getCursor(tableName, args);
         if (c.moveToFirst())
         {
            text = c.getString(c.getColumnIndex(columnIndex));
         }
         c.close();
         mDbHelper.getReadableDatabase().close();
         return text;
      }
   }

   protected String insertRow(String tableName, ContentValues cv)
   {
      synchronized (lock)
      {
         long result = mDbHelper.getWritableDatabase().insertOrThrow(tableName, null, cv);
         mDbHelper.getWritableDatabase().close();
         if (result == -1)
         {
            // String stringResult = (result == -1) ? "Row Already Exists" : "Row Added";
         }
         return Long.toString(result);
      }
   }
   
   /**
    * Internal Database
    */
   private static class DatabaseHelper extends SQLiteOpenHelper
   {

      DatabaseHelper(Context context)
      {
         super(context, DB_NAME, null, DB_VERSION);
         Log.v(DB_TAG, "DatabaseHelper() - Database " + DB_NAME + " Ready - Version: " + DB_VERSION);
      }

      @Override
      public void onCreate(SQLiteDatabase db)
      {
         db.execSQL(COUNTRIES_TABLE_CREATE);
         Log.v(DB_TAG, "onCreate() - Created New Database - DatabaseName: " + DB_NAME);
      }

      @Override
      public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
      {
         db.execSQL(COUNTRIES_TABLE_DROP);
         onCreate(db);
         Log.v(DB_TAG, "onUpgrade() - Upgrading Database - DatabaseName: " + DB_NAME);
      }
   }

}
DBAdapterCountry.java - Class that manages the table 'CountriesTable'. Extends AbstractDBAdapter.java.
public class DBAdapterCountry extends AbstractDBAdapter
{

   /**
    * Singleton Instance Creation
    */
   private volatile static DBAdapterCountry INSTANCE;

   static DBAdapterCountry getInstance()
   {
      if (INSTANCE == null)
      {
         synchronized (DBAdapterCountry.class)
         {
            if (INSTANCE == null)
            {
               INSTANCE = new DBAdapterCountry(MyApplication.getAppContext());
            }
         }
      }
      return INSTANCE;
   }

   private DBAdapterCountry(Context context)
   {
      super(context);
   }

   /**
    * Class
    */
   public void addCountry(String countryName, String countryCapital)
   {
      ContentValues cv = new ContentValues();
      cv.put("COUNTRY_NAME", countryName);
      cv.put("COUNTRY_CAPITAL", countryCapital);
      insertRow(COUNTRIES_TABLE_NAME, cv);
   }

   public String getCountryCapital(String countryName)
   {
      String args = "COUNTRY_NAME='" + countryName + "'";
      return getString(COUNTRIES_TABLE_NAME, args, "COUNTRY_CAPITAL");
   }
   
}
DatabaseActivity.java - Activity used to test the Database.
public class DatabaseActivity extends Activity
{
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      
      // Thread 1 
      Thread thread1 = new Thread(new Runnable()
      {
         public void run()
         {
            DBAdapterCountry.getInstance().addCountry("England", "London");
            Log.v("MyTag", DBAdapterCountry.getInstance().getCountryCapital("England"));
         }
      });
      
      // Thread 2
      Thread thread2 = new Thread(new Runnable()
      {
         public void run()
         {
            DBAdapterCountry.getInstance().addCountry("France", "Paris");
            Log.v("MyTag", DBAdapterCountry.getInstance().getCountryCapital("France"));
         }
      });
      
      // Thread 3
      Thread thread3 = new Thread(new Runnable()
      {
         public void run()
         {
            DBAdapterCountry.getInstance().addCountry("Spain", "Madrid");
            Log.v("MyTag", DBAdapterCountry.getInstance().getCountryCapital("Spain"));
         }
      });

      thread1.start();
      thread2.start();
      thread3.start();
   }
}
MyApplcation.java - Started when application is loaded - contains global application context. The line android:name=".MyApplication also needs to be added in your AndroidManifest.xml to make it start when your application starts.
public class MyApplication extends Application
{

   private static Context context;

   public void onCreate()
   {
      super.onCreate();
      MyApplication.context = getApplicationContext();
   }

   public static Context getAppContext()
   {
      return context;
   }

}
If you stick to this pattern of Database creation you can simply add a new adapter class, along with a create/drop SQL statement whenever you want to add a new table. And if you need any other helper methods like getInt you can add them to AbstractDBAdapter.java.

I believe this approach makes Database code management a lot simpler, cohesive, cuts down lines of code and most importantly does not cause the Database to get into an illegal state.

Any comments on this approach are welcome! :)

No comments:

Post a Comment