This is the fourth post in my series about saving data in Android applications. Here are the other posts:
The previous posts described how to save files to the file system and to the preferences files. This can be enough if for a simple application, but if your data has a complex structure or if you have a lot of data to save, using a database is a better option. Managing a database requires more knowledge and setup, but it comes with many validations and performance optimizations. The Android SDK includes the open source SQLite database engine and the classes needed to access it.
SQLite is a self-contained relational database that requires no server to work. The database itself is saved to a file in the internal storage of your application, so each application has its own private database that is not accessible to other applications. You can learn more about the SQLite project itself and its implementation of the SQL query language at http://www.sqlite.org.
New to databases? A relational database saves data to tables. Each table is made of columns, and for each column, you must choose a name and the type of data that can be saved in it. Each table should also have a column or many column that are set as the key of the table so each row of data can be uniquely identified. Relationships can also be defined between tables.
The basics of databases and the SQL query language used by most databases could take many articles to explain. If you don’t know how to use a database, this is a subject worth learning more about since databases are used in almost all applications to store data.
To demonstrate how to create a database and interact with it, I created a small sample application, which is available here. The application is a row counter for knitting projects: the user can create a knitting project containing one or many counters used to track the current number of rows done and to show the total amount of rows to reach. The structure of the database is as follows, with a project
table in relation with a row_counter
table:
First, to be able to create the database, we need a contract
class for each table that describes the name of the elements of the table. This class should be used each time the name of elements in the database is required. To describe the name of each column, the contract
class also contains a subclass with an implementation of the android.provider.BaseColumn
, which automatically adds the name of an_ID
and of a _COUNT
column. I also like to put the CREATE TABLE SQL
query in the contract
class so all the string
s used in SQL queries are at the same place. Here is the contract
class for the row_counter
table in the example:
public final class RowCounterContract {
public static final String TABLE_NAME = "row_counter";
public static final String SQL_CREATE_TABLE = "CREATE TABLE "
+ RowCounterContract.TABLE_NAME + " ("
+ RowCounterContract.RowCounterEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ RowCounterContract.RowCounterEntry.COLUMN_NAME_PROJECT_ID + " INTEGER,"
+ RowCounterContract.RowCounterEntry.COLUMN_NAME_CURRENT_AMOUNT + " INTEGER DEFAULT 0,"
+ RowCounterContract.RowCounterEntry.COLUMN_NAME_FINAL_AMOUNT + " INTEGER,"
+ "FOREIGN KEY (" + RowCounterContract.RowCounterEntry.COLUMN_NAME_PROJECT_ID + ") "
+ "REFERENCES projects(" + ProjectContract.ProjectEntry._ID + "));";
public static abstract class RowCounterEntry implements BaseColumns {
public static final String COLUMN_NAME_PROJECT_ID = "project_id";
public static final String COLUMN_NAME_FINAL_AMOUNT = "final_amount";
public static final String COLUMN_NAME_CURRENT_AMOUNT = "current_amount";
}
}
To create the tables that store the data described by the contracts, you must implement the android.database.sqllite.SQLLiteOpenHelper
class that manages the access to the database. The following methods should be implemented as needed:
onCreate
: This method is called the first time the database is opened by your application. You should setup the database for use in that method by creating the tables and initializing any data you need. onUpdate
: This method is called when your application is upgraded and the version number has changed. You don’t need to do anything for your first version, but in the following versions you must provide queries to modify the database from the old version to the new structure as needed so your users don’t lose their data during the upgrade. onDowngrade
(optional): You may implement this method if you want to handle the case where your application is downgraded to a version requiring an older version. The default implementation will throw a SQLiteException
and will not modify the database. onOpen
(optional): This method is called after the database has been created, upgraded to a newer version or downgraded to an older version.
Here is a basic implementation of the android.database.sqllite.SQLLiteOpenHelper
for the example that executes an SQL CREATE TABLE
query for each table of the database in the onCreate
method. There is no method available in the android.database.sqlite.SQLiteDatabase
class to create a table, so you must use the execSQL
method to execute the query.
public class ProjectsDatabaseHelper extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "Projects.db";
public ProjectsDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(ProjectContract.SQL_CREATE_TABLE);
db.execSQL(RowCounterContract.SQL_CREATE_TABLE);
initializeExampleData(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(ProjectsDatabaseHelper.class.getSimpleName(),
"Upgrading database from version " + oldVersion + " to " + newVersion);
}
}
Once the android.database.sqllite.SQLLiteOpenHelper
is implemented, you can get an instance of the database object android.database.sqlite.SQLiteDatabas
e using the getReadableDatabase
method of the helper if you only need to read data or the getWritableDatabase
method if you need to read and write data. There are four kinds of basic operations that can be done with the data, and modifications can not be undone like in all databases.
- Inserting a new row: The
insert
method of the android.database.sqlite.SQLiteDatabase
object inserts a new row of data in a table. Data can be inserted with a SQL INSERT
query using the execSQL
method, but using insert
is recommended to avoid SQL injection: only one database row can be created by the insert
method and nothing else, regardless of the input. In the following example, a few test projects are initialized in the database of the application by the onCreate
method of the database helper after the creation of the table:
private void initializeExampleData(SQLiteDatabase db) {
long projectId;
ContentValues firstProjectValues = new ContentValues();
firstProjectValues.put(ProjectContract.ProjectEntry.COLUMN_NAME_TITLE,
"Flashy Scarf");
projectId = db.insert(ProjectContract.TABLE_NAME, null, firstProjectValues);
ContentValues firstProjectCounterValues = new ContentValues();
firstProjectCounterValues.put(RowCounterContract
.RowCounterEntry.COLUMN_NAME_PROJECT_ID, projectId);
firstProjectCounterValues.put(RowCounterContract
.RowCounterEntry.COLUMN_NAME_FINAL_AMOUNT, 120);
db.insert(RowCounterContract.TABLE_NAME, null, firstProjectCounterValues);
ContentValues secondProjectValues = new ContentValues();
secondProjectValues.put(ProjectContract.ProjectEntry.COLUMN_NAME_TITLE,
"Simple Socks");
projectId = db.insert(ProjectContract.TABLE_NAME, null, secondProjectValues);
ContentValues secondProjectCounterValues = new ContentValues();
secondProjectCounterValues.put(RowCounterContract
.RowCounterEntry.COLUMN_NAME_PROJECT_ID, projectId);
secondProjectCounterValues.put(RowCounterContract
.RowCounterEntry.COLUMN_NAME_FINAL_AMOUNT, 80);
db.insert(RowCounterContract.TABLE_NAME, null, secondProjectCounterValues);
db.insert(RowCounterContract.TABLE_NAME, null, secondProjectCounterValues);
}
- Reading existing rows: The
query
method from the android.database.sqlite.SQLiteDatabase
class retrieves the data that was previously inserted in the database. This method will return a cursor that points to the collection of rows returned by your request, if any. You can then convert the data fetched from the database table to an object can be used in your application: in the example, the rows from the project
table are converted to Project
objects.
public ArrayList getProjects() {
ArrayList projects = new ArrayList();
SQLiteDatabase db = getReadableDatabase();
Cursor projCursor = db.query(ProjectContract.TABLE_NAME, null, null,
null, null, null, null);
while (projCursor.moveToNext()) {
Project project = new Project();
int idColIndex = projCursor.getColumnIndex(ProjectContract.ProjectEntry._ID);
long projectId = projCursor.getLong(idColIndex);
project.setId(projCursor.getLong(projectId);
int nameColIndex = projCursor.getColumnIndex(ProjectContract
.ProjectEntry.COLUMN_NAME_TITLE);
project.setName(projCursor.getString(nameColIndex));
project.setRowCounters(getRowCounters(projectId));
projects.add(project);
}
projCursor.close();
return (projects);
}
- Updating existing rows: The
update
method of an instance of the android.database.sqlite.SQLiteDatabase
class updates the data in a row or in multiple rows of a database table. Like with the insert
method, you could use the execSQL
query to run a SQL UPDATE
query, but using the update
method is safer. In the following example, the current row counter value for the row counter in the row_counter
table is updated with the new value. According to the condition specified, only the row counter with the identifier passed as a parameter is updated but with another condition you could update many rows, so you should always make sure that the condition only selects the rows you need.
public void updateRowCounterCurrentAmount(RowCounter rowCounter) {
SQLiteDatabase db = getWritableDatabase();
ContentValues currentAmountValue = new ContentValues();
currentAmountValue.put(RowCounterContract.RowCounterEntry.COLUMN_NAME_CURRENT_AMOUNT,
rowCounter.getCurrentAmount());
db.update(RowCounterContract.TABLE_NAME,
currentAmountValue,
RowCounterContract.RowCounterEntry._ID +"=?",
new String[] { String.valueOf(rowCounter.getId()) });
}
- Deleting existing rows: The
delete
method of an instance of the android.database.sqlite.SQLiteDatabase
class deletes a row or in multiple rows of a database table. Like with the insert
method, you could use the execSQL
query to run a SQL UPDATE
query, but using the delete
method is safer. In the following example, a row counter in the row_counter
table is deleted. According to the condition specified, only the row counter with the identifier passed as a parameter is deleted but with another condition, you could delete many rows, so you should always make sure that the condition only selects the rows you need so you don’t delete too much data.
public void deleteRowCounter(RowCounter rowCounter) {
SQLiteDatabase db = getWritableDatabase();
db.delete(RowCounterContract.TABLE_NAME,
RowCounterContract.RowCounterEntry._ID +"=?",
new String[] { String.valueOf(rowCounter.getId()) });
}
Finally, if you want to encapsulate access to the data in your database to avoid calling the database helper directly in your activity, you can also implement the android.content.ContentProvider
class from the Android SDK. This is only required if your application must share data with other applications: you do not need one to get started, but you should consider using it as your data gets more complex.