Persisting data to a database One nice convenience that the Android platform
5.3 Persisting data to a database One nice convenience that the Android platform
provides is the fact that a relational database is built in. SQLite doesn’t have all of the features of larger client/server database products, but it does cover just about anything you might need for local data storage, while being easy to deal with and quick.
In this section we are going to cover working with the built-in SQLite database system, from cre- ating and querying a database to upgrading and working with the sqlite3 tool that is available in the Android Debug Bridge (adb) shell. Once again we will do this in the context of the WeatherReporter application we began in chapter 4. This application uses a database to store the user’s saved locations and persists user preferences for each location. The screen shot shown in figure 5.4 displays this saved data for the user to select from; when the user selects a location, data is retrieved from the data- base and a location weather report is shown.
To see how this comes together we will begin
Figure 5.4 The WeatherReporter
with what it takes to create the database Weather-
Saved Locations screen, which pulls
Reporter uses.
data from a SQLite database
5.3.1 Building and accessing a database To use SQLite you have to know a bit about SQL usage in general. If you need to brush
up on the background of the basic commands—CREATE, INSERT, UPDATE, DELETE, and SELECT —then you may want to take a quick look at the SQLite documentation ( http: //www.sqlite.org/lang.html ).
For our purposes we are going to jump right in and build a database helper class that our application will use. We are creating a helper class so that the details concerning cre- ating and upgrading our database, opening and closing connections, and running
C HAPTER 5 Storing and retrieving data
through specific queries are all encapsulated in one place and not otherwise exposed or repeated in our application code. This is so our Activity and Service classes can later use simple get and insert methods, with specific bean objects representing our model, or Collections rather than database-specific abstractions (such as the Android Cursor object that represents a query result set). You can think of this class as a miniature Data Access Layer (DAL).
The first part of our DBHelper class, which includes a few inner classes you will learn about, is shown in listing 5.10.
Listing 5.10 Portion of the DBHelper class showing the DBOpenHelper inner class
public class DBHelper { public static final String DEVICE_ALERT_ENABLED_ZIP = "DAEZ99";
public static final String DB_NAME = "w_alert"; public static final String DB_TABLE = "w_alert_loc";
Use constants for B
public static final int DB_VERSION = 3;
database properties
private static final String CLASSNAME = DBHelper.class.getSimpleName(); private static final String[] COLS = new String[]
{ "_id", "zip", "city", "region", "lastalert", "alertenabled" }; private SQLiteDatabase db;
private final DBOpenHelper dbOpenHelper; public static class Location {
Define inner
public long id;
C Location bean
public long lastalert; public int alertenabled; public String zip; public String city; public String region;
. . . Location constructors and toString omitted for brevity
private static class DBOpenHelper extends
D Define inner
SQLiteOpenHelper {
DBOpenHelper class
private static final String DB_CREATE = "CREATE TABLE "
+ DBHelper.DB_TABLE
E Define SQL
+ " (_id INTEGER PRIMARY KEY, zip TEXT UNIQUE NOT NULL,”
query for
+ “city TEXT, region TEXT, lastalert INTEGER, “
database
+ “alertenabled INTEGER);";
creation
public DBOpenHelper(Context context, String dbName, int version) { super(context, DBHelper.DB_NAME, null, DBHelper.DB_VERSION); }
@Override
F Override helper
public void onCreate(SQLiteDatabase db) {
callbacks
try {
db.execSQL(DBOpenHelper.DB_CREATE); } catch (SQLException e) {
Persisting data to a database
Log.e(Constants.LOGTAG, DBHelper.CLASSNAME, e); } }
@Override public void onOpen(SQLiteDatabase db) {
super.onOpen(db); }
F Override
@Override
helper
public void onUpgrade(SQLiteDatabase db, int oldVersion,
callbacks
int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + DBHelper.DB_TABLE); this.onCreate(db);
Within our DBHelper class we first have a series of constants that define important static values relating to the database we want to work with, such as database name,
database version, and table name B . Then we show several of the most important
parts of the database helper class that we have created for the WeatherReporter appli- cation, the inner classes.
The first inner class is a simple Location bean that is used to represent a user’s
selected location to save C . This class intentionally does not have accessors and muta-
tors, because these add overhead and we don’t really need them when we will use this bean only within our application (we won’t expose it). The second inner class is a
SQLiteOpenHelper implementation D .
Our DBOpenHelper inner class extends SQLiteOpenHelper, which is a class that Android provides to help with creating, upgrading, and opening databases. Within this class we are including a String that represents the CREATE query we will use to build our database table; this shows the exact columns and types our table will have
E . The data types we are using are fairly self-explanatory; most of the time you will use INTEGER and TEXT types, as we have (if you need more information about the other types SQLite supports, please see the documentation: http://www.sqlite.org/ datatype3.html ). Also within DBOpenHelper we are implementing several key SQLite- OpenHelper callback methods, notably onCreate and onUpgrade (onOpen is also sup-
ported, but we aren’t using it) F . We will explain how these callbacks come into play
and why this class is so helpful in the second part of our DBHelper (the outer class), which is shown in listing 5.11.
Listing 5.11 Portion of the DBHelper class showing convenience methods
public DBHelper(Context context) { this.dbOpenHelper = new DBOpenHelper(context, "WR_DATA", 1); this.establishDb();
C Provide
Create DBOpenHelper
private void establishDb() {
establishDb
instance B
if (this.db == null) {
C HAPTER 5 Storing and retrieving data
this.db = this.dbOpenHelper.getWritableDatabase();
public void cleanup() {
Provide cleanup
if (this.db != null) {
D method
this.db.close(); this.db = null;
public void insert(Location location) { ContentValues values = new ContentValues(); values.put("zip", location.zip); values.put("city", location.city); values.put("region", location.region); values.put("lastalert", location.lastalert); values.put("alertenabled", location.alertenabled); this.db.insert(DBHelper.DB_TABLE, null, values);
} public void update(Location location) {
ContentValues values = new ContentValues(); values.put("zip", location.zip);
Provide E
values.put("city", location.city);
convenience insert, update,
values.put("region", location.region);
delete, get
values.put("lastalert", location.lastalert); values.put("alertenabled", location.alertenabled); this.db.update(DBHelper.DB_TABLE, values, "_id=" + location.id, null);
} public void delete(long id) {
this.db.delete(DBHelper.DB_TABLE, "_id=" + id, null); }
public void delete(String zip) { this.db.delete(DBHelper.DB_TABLE, "zip='" + zip + "'", null); }
public Location get(String zip) { Cursor c = null; Location location = null; try {
c = this.db.query(true, DBHelper.DB_TABLE, DBHelper.COLS,
"zip = '" + zip + "'", null, null, null, null, null); if (c.getCount() > 0) {
c.moveToFirst(); location = new Location(); location.id = c.getLong(0); location.zip = c.getString(1); location.city = c.getString(2); location.region = c.getString(3); location.lastalert = c.getLong(4); location.alertenabled = c.getInt(5);
} } catch (SQLException e) {
Persisting data to a database
Log.v(Constants.LOGTAG, DBHelper.CLASSNAME, e); } finally { if (c != null && !c.isClosed()) { c.close(); } } return location;
F Provide additional get methods
public List<Location> getAll() { ArrayList<Location> ret = new ArrayList<Location>(); Cursor c = null; try {
c = this.db.query(DBHelper.DB_TABLE, DBHelper.COLS, null, null, null, null, null);
int numRows = c.getCount(); c.moveToFirst(); for (int i = 0; i < numRows; ++i) {
Location location = new Location(); location.id = c.getLong(0); location.zip = c.getString(1); location.city = c.getString(2); location.region = c.getString(3); location.lastalert = c.getLong(4); location.alertenabled = c.getInt(5); if (!location.zip.equals(DBHelper.DEVICE_ALERT_ENABLED_ZIP)) {
ret.add(location); } c.moveToNext();
} } catch (SQLException e) { Log.v(Constants.LOGTAG, DBHelper.CLASSNAME, e); } finally { if (c != null && !c.isClosed()) { c.close(); } } return ret;
} . . . getAllAlertEnabled omitted for brevity
} Our DBHelper class contains a member-level variable reference to a SQLiteDatabase
object, as we saw in listing 5.10 (the first half of this class). This object is the Android database workhorse. It is used to open database connections, to execute SQL state- ments, and more.
Then the DBOpenHelper inner class we also saw in the first part of the DBHelper
class listing is instantiated inside the constructor B . From there the dbOpenHelper is
used, inside the establishDb method if the db reference is null, to call openDatabase
with the current Context, database name, and database version C . This establishes db
as an instance of SQLiteDatabase through DBOpenHelper.
C HAPTER 5 Storing and retrieving data
Although you can also just open a database connection directly on your own, using the open helper in this way invokes the provided callbacks and makes the process easier. With this technique, when we try to open our database connection, it is automatically created or upgraded (or just returned), if necessary, through our DBOpenHelper. While using a DBOpenHelper entails extra steps up front, once you have it in place it is extremely handy when you need to modify your table structure (you can simply incre- ment your version and do what you need to do in the onUpgrade callback—without this you would have to manually alter and/or remove and re-create your existing structure).
Another important thing to provide in a helper class like this is a cleanup
method D . This method is used by callers who can invoke it when they pause, in order to close connections and free up resources. After the cleanup method we then see the raw SQL convenience methods that encapsulate the operations our helper provides. In this class we have methods to
insert, update, delete and get data E . We also have a few additional specialized get and get all methods F . Within these methods you get a feel for how the db object is
used to run queries. The SQLiteDatabase class itself has many convenience methods, such as insert, update, and delete—which we are wrapping—and it provides direct query access that returns a Cursor over a result set.
Databases are package private Unlike the SharedPreferences we saw earlier, you can’t make a database WORLD_READABLE. Each database is accessible only by the package in which it was created—this means accessible only to the process that created it. If you need to pass data across processes, you can use AIDL/ Binder (as in chapter 4) or create a ContentProvider (as we will discuss next), but you can’t use a database directly across the process/package boundary.
Typically you can get a lot of mileage and utility from basic steps relating to the SQLiteDatabase class, as we have here, and by using it you can create a very useful and fast data-storage mechanism for your Android applications. The final thing we need to discuss with regard to databases is the sqlite3 tool, which you can use to manipulate data outside your application.
5.3.2 Using the sqlite3 tool When you create a database for an application in Android, the files for that database
are created on the device in the /data/data/[PACKAGE_NAME]/database/db.name location. These files are SQLite proprietary, but there is a way to manipulate, dump, restore, and otherwise work with your databases through these files in the ADB shell—the sqlite3 tool.
This tool is accessible through the shell; you can get to it by issuing the following commands on the command line (remember to use your own package name; here we are using the package name for the WeatherReporter sample application):
Working with ContentProvider classes 149
cd [ANDROID_HOME]/tools adb shell sqlite3 /data/data/com.msi.manning.chapter4/databases/w_alert.db
Once you are in the shell prompt (you have the #), you can then issue sqlite3 com- mands; .help should get you started (if you need more, see the tool’s documentation: http://www.sqlite.org/sqlite.html ). From the tool you can issue basic commands, such as SELECT or INSERT, or you can go further and CREATE or ALTER tables. This tool comes in handy for basic poking around and troubleshooting and to .dump and .load data. As with many command-line SQL tools, it takes some time to get used to the for- mat, but there is no better way to back up or load your data. (If you need that facil- ity—in most cases with mobile development you really shouldn’t have a huge database. Keep in mind that this tool is available only through the development shell; it’s not something you will be able to use to load a real application with data.)
Now that we have shown how to use the SQLite support provided in Android, from creating and accessing tables to store data, to investigating databases with the pro- vided tools in the shell, the next thing we need to cover is the last aspect of handling data on the platform, and that is building and using a ContentProvider.