One topic that I believe is not given much importance, but is should, is the usage of database in day-by-day Android applications. People believe that database should only be used to provide Content Providers. This could not be further from the truth.
Databases should be used in great part of the Android applications. The reason is simple: you need to save the state of your activities in 99% of you applications, and if you have complex relationships among the data being stored, databases are the correct way to do so. In the last topic, it was described how to handle Activities minimally. If you read it, you noticed that state persistence was mentioned several times.
Thus, here it will be described how to implement what Google calls a Database Adapter. For the Enterprise Application developers, this is the DAO (Data Access Object) pattern abstracting the persistence layer. It means that this layer access the database and provides the resulting data nicely to the above layers. This access consist of fetching, updating saving and deleting data.
The example displayed here is based on the Android docs example: Notepad. I found it quite illustrative, therefore I am using it.
Below is the whole code of the Database Adapter. Now it will be explained all the relevant part so you can have a better understanding of it. However, you should browse through it because I believe most of it is self-explanatory for people who are experienced with databases.
/*
* Copyright (C) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.demo.notepad3;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
/**
* Simple notes database access helper class. Defines the basic CRUD operations
* for the notepad example, and gives the ability to list all notes as well as
* retrieve or modify a specific note.
*
* This has been improved from the first version of this tutorial through the
* addition of better error handling and also using returning a Cursor instead
* of using a collection of inner classes (which is less scalable and not
* recommended).
*/
public class NotesDbAdapter {
public static final String KEY_TITLE = "title";
public static final String KEY_BODY = "body";
public static final String KEY_ROWID = "_id";
private static final String TAG = "NotesDbAdapter";
private DatabaseHelper mDbHelper;
private SQLiteDatabase mDb;
/**
* Database creation sql statement
*/
private static final String DATABASE_CREATE =
"create table notes (_id integer primary key autoincrement, "
+ "title text not null, body text not null);";
private static final String DATABASE_NAME = "data";
private static final String DATABASE_TABLE = "notes";
private static final int DATABASE_VERSION = 2;
private final Context mCtx;
private static class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS notes");
onCreate(db);
}
}
/**
* Constructor - takes the context to allow the database to be
* opened/created
*
* @param ctx the Context within which to work
*/
public NotesDbAdapter(Context ctx) {
this.mCtx = ctx;
}
/**
* Open the notes database. If it cannot be opened, try to create a new
* instance of the database. If it cannot be created, throw an exception to
* signal the failure
*
* @return this (self reference, allowing this to be chained in an
* initialization call)
* @throws SQLException if the database could be neither opened or created
*/
public NotesDbAdapter open() throws SQLException {
mDbHelper = new DatabaseHelper(mCtx);
mDb = mDbHelper.getWritableDatabase();
return this;
}
public void close() {
mDbHelper.close();
}
/**
* Create a new note using the title and body provided. If the note is
* successfully created return the new rowId for that note, otherwise return
* a -1 to indicate failure.
*
* @param title the title of the note
* @param body the body of the note
* @return rowId or -1 if failed
*/
public long createNote(String title, String body) {
ContentValues initialValues = new ContentValues();
initialValues.put(KEY_TITLE, title);
initialValues.put(KEY_BODY, body);
return mDb.insert(DATABASE_TABLE, null, initialValues);
}
/**
* Delete the note with the given rowId
*
* @param rowId id of note to delete
* @return true if deleted, false otherwise
*/
public boolean deleteNote(long rowId) {
return mDb.delete(DATABASE_TABLE, KEY_ROWID + "=" + rowId, null) > 0;
}
/**
* Return a Cursor over the list of all notes in the database
*
* @return Cursor over all notes
*/
public Cursor fetchAllNotes() {
return mDb.query(DATABASE_TABLE, new String[] {KEY_ROWID, KEY_TITLE,
KEY_BODY}, null, null, null, null, null);
}
/**
* Return a Cursor positioned at the note that matches the given rowId
*
* @param rowId id of note to retrieve
* @return Cursor positioned to matching note, if found
* @throws SQLException if note could not be found/retrieved
*/
public Cursor fetchNote(long rowId) throws SQLException {
Cursor mCursor =
mDb.query(true, DATABASE_TABLE, new String[] {KEY_ROWID,
KEY_TITLE, KEY_BODY}, KEY_ROWID + "=" + rowId, null,
null, null, null, null);
if (mCursor != null) {
mCursor.moveToFirst();
}
return mCursor;
}
/**
* Update the note using the details provided. The note to be updated is
* specified using the rowId, and it is altered to use the title and body
* values passed in
*
* @param rowId id of note to update
* @param title value to set note title to
* @param body value to set note body to
* @return true if the note was successfully updated, false otherwise
*/
public boolean updateNote(long rowId, String title, String body) {
ContentValues args = new ContentValues();
args.put(KEY_TITLE, title);
args.put(KEY_BODY, body);
return mDb.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0;
}
}
The first important thing to note is the creation of the DatabaseHelper as an inner class. This class aims two things: manage the creation and update of the database model, and provide the context for accessing the database. See the class below:
private static class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS notes");
onCreate(db);
}
}
The former, as you can see is done by the methods onCreate() and onUpgrade(). The onCreate() method creates (and populates if needed) all the database tables for the initial version. The onUpgrade() modifies the model (and alters data if needed) every time such thing is required. The latter is done in the constructor when passing the context within the super().
Note that the version of the database is controlled by its version, when the constructor is called. Therefore, in order to alter the database version, simply modify this number when an upgrade is needed.
Observation:
Some people consider the usage of inner classes unnecessary and a bad coding practice. There is no need for this class to be, as to say inner. It can be easily implemented as a public or default one outside the Database Adapter.
Next, check the constructor. It simply assigns the context. This is necessary in order to retrieve the database instance. Se the method below:
/**
* Constructor - takes the context to allow the database to be
* opened/created
*
* @param ctx the Context within which to work
*/
public NotesDbAdapter(Context ctx) {
this.mCtx = ctx;
}
Now, observe the method open(), displayed below. It initiates the database as a writable instance. You could also initialise as an readable instance using mDbHelper.getReadableDatabase().
/**
* Open the notes database. If it cannot be opened, try to create a new
* instance of the database. If it cannot be created, throw an exception to
* signal the failure
*
* @return this (self reference, allowing this to be chained in an
* initialization call)
* @throws SQLException if the database could be neither opened or created
*/
public NotesDbAdapter open() throws SQLException {
mDbHelper = new DatabaseHelper(mCtx);
mDb = mDbHelper.getWritableDatabase();
return this;
}
The close() method (shown below) is straightforward, is simply closes the Helper instance.
public void close() {
mDbHelper.close();
}
Observation:
Note that you have two important instances here, the DatabaseHelper mDbHelper and the SQLiteDatabase mDb. The first deals with Database management operations, such as closing the database and retrieving its instance. The second one handles database data operations, like providing and fetching data from the database.
Now, we shall examine the created table note, in order to understand how data is managed. It has three columns: _id, title and body. It is strongly recommended that every table has a column called _id (with this exact name) which represents the primary key of the table. The reason for that is that Android is prepared to handle tables with a primary key called _id. Moreover, it is quite interesting that this column has the property of auto-increment (as this does) because the developer does not need to worry about setting its value correctly. But more importantly the reason is also because Android is prepared to deal with _id's which have the property of auto-increment. The other two columns represent ordinary values and have nothing special.
Since a introduction of the data model was given, let's take a look at the method createNode(). This method is responsible for inserting a new value in a database table. In order to do so, values are added into a special Hashtable called ContentValues. Each element of this Hashtable has as a key, the column name of the table, and as the value to be inserted, the associated value of the key. Note in the method createNote() that the _id is not managed at all. The reason is simple: since it is an auto-increment value, the next insertion will automatically provide this _id's value. It will be the previous one plus one. Therefore, nothing is to be done here regarding the _id. This is the way Android deals with insertions. The return value of the method createNote() is the newly-created _id, should no error happen, otherwise -1 is returned. Note that for the insertion to take place, the SQLiteDatabase object has the method insert() called.
/**
* Create a new note using the title and body provided. If the note is
* successfully created return the new rowId for that note, otherwise return
* a -1 to indicate failure.
*
* @param title the title of the note
* @param body the body of the note
* @return rowId or -1 if failed
*/
public long createNote(String title, String body) {
ContentValues initialValues = new ContentValues();
initialValues.put(KEY_TITLE, title);
initialValues.put(KEY_BODY, body);
return mDb.insert(DATABASE_TABLE, null, initialValues);
}
The next step is to observe the delete() method. This one is quite simple, given the _id column (row identifier), the column associated with it will be deleted. For the deletion to happen, the SQLiteDatabase object calls the method delete().
/**
* Delete the note with the given rowId
*
* @param rowId id of note to delete
* @return true if deleted, false otherwise
*/
public boolean deleteNote(long rowId) {
return mDb.delete(DATABASE_TABLE, KEY_ROWID + "=" + rowId, null) > 0;
}
One of the most used methods are the ones which perform select's. Here, the fetchAllNotes() returns all values from the table notes. For that, the method query() is called from the object SQLiteDatabase. The parameters used are the table name and the columns which will be returned. The data is returned in an object called Cursor. This object can be easily manipulated in order to retrieve the data needed. Now see the method fetchNode(). It has as a parameter the row id, which will be used to identify the row desired to be selected. This method aims to return a certain row (Note data). In order to retrieve the specific row, the expression
KEY_ROWID + "=" + rowId is used. In the Cursor, only one row is returned, the one ideified by the rowId.
/**
* Return a Cursor over the list of all notes in the database
*
* @return Cursor over all notes
*/
public Cursor fetchAllNotes() {
return mDb.query(DATABASE_TABLE, new String[] {KEY_ROWID, KEY_TITLE,
KEY_BODY}, null, null, null, null, null);
}
/**
* Return a Cursor positioned at the note that matches the given rowId
*
* @param rowId id of note to retrieve
* @return Cursor positioned to matching note, if found
* @throws SQLException if note could not be found/retrieved
*/
public Cursor fetchNote(long rowId) throws SQLException {
Cursor mCursor =
mDb.query(true, DATABASE_TABLE, new String[] {KEY_ROWID,
KEY_TITLE, KEY_BODY}, KEY_ROWID + "=" + rowId, null,
null, null, null, null);
if (mCursor != null) {
mCursor.moveToFirst();
}
return mCursor;
}
Finally, the last method to be described is the updateNote(). It is very simliar to the insertNote(). Note that it also uses the ContentValues object to hold object values. In the pair inserted, there is the values to update the row. The desired row is identified by the expression K
EY_ROWID + "=" + rowId. The updateNote() returns true in case the operation is successful, or false if it fails.
/**
* Update the note using the details provided. The note to be updated is
* specified using the rowId, and it is altered to use the title and body
* values passed in
*
* @param rowId id of note to update
* @param title value to set note title to
* @param body value to set note body to
* @return true if the note was successfully updated, false otherwise
*/
public boolean updateNote(long rowId, String title, String body) {
ContentValues args = new ContentValues();
args.put(KEY_TITLE, title);
args.put(KEY_BODY, body);
return mDb.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0;
}
}
Observation:
The methods used by the SQLiteDatabase are not explained in details because this is not the aim of this article. Here it is desired to give an overview of how best to manipulate data using database, implementing the Database Adapter layer. Besides, they are quite simple to understand. Moreover, There are no explanation on how to use the Cursor object. Here the focus is just on how to return it fulfilled and not how to use it. It is important however, to know its usage. Fortunately a good documentation is provided by Google. There will be an article entirely dedicated to it.
Concluding, here an overview of how to implement a Database Adapater layer was shown. Note that with these methods, one can perform most of operations necessary with databases.
There is another concept which is quite important for Android which is ContentProviders. They also access data, and when they use for databases for that, the manner in which data is manipulated is quite similar to Database Adapters.