Android Database Programming: Chapter No. 2 "Using A Sqlite Database"
Android Database Programming: Chapter No. 2 "Using A Sqlite Database"
Jason Wei
Chapter 5, Querying the Contacts Table, is devoted to exploring the most widely used content provider provided by the Android OSthe Contacts content provider. It explores the structure of the Contacts tables, and provides examples of common queries. Chapter 6, Binding to the UI, talks about ways the user can bind their data to the user interface. Because of how data is typically displayed as lists, this chapter walks through the implementations of two types of list adapters. Chapter 7, Android Databases in Practice, tries to step away from the programming and focus on higher-level design concepts. It talks about ways in which all the local storage methods discussed up to this point can be used, and also highlights the downfalls of such local methodsopening the door for the next couple of chapters, where we focus on external data stores. Chapter 8, Exploring External Databases, introduces the notion of using an external database and lists some common external data stores that are available to the reader. The chapter finishes with an example of how to set up a Google App Engine data store. Chapter 9, Collecting and Storing Data, extends the development of the previous chapter by talking about ways in which your application can go and collect data, which can then be inserted into your new external database. The methods for collecting data include using available APIs, as well as writing custom web scrapers. Chapter 10, Bringing it Together, finishes the application we started in the previous two chapters by showing the reader how to first create HTTP servlets, and second make HTTP requests from the mobile application to these HTTP servlets. This chapter serves as the culmination of the book, and shows the reader how to connect their mobile application with their external database, and ultimately parse and display the HTTP response as a list.
On that note, let's go straight into the code. We begin by dening the schema with a couple of classes:
public class StudentTable { // EACH STUDENT HAS UNIQUE ID public static final String ID = "_id"; // NAME OF THE STUDENT public static final String NAME = "student_name"; // STATE OF STUDENT'S RESIDENCE public static final String STATE = "state"; // GRADE IN SCHOOL OF STUDENT public static final String GRADE = "grade"; // NAME OF THE TABLE public static final String TABLE_NAME = "students"; } public class CourseTable { // UNIQUE ID OF THE COURSE public static final String ID = "_id"; // NAME OF THE COURSE public static final String NAME = "course_name"; // NAME OF THE TABLE public static final String TABLE_NAME = "courses"; } // THIS ESSENTIALLY REPRESENTS A MAPPING FROM STUDENTS TO COURSES public class ClassTable { // UNIQUE ID OF EACH ROW - NO REAL MEANING HERE public static final String ID = "_id"; // THE ID OF THE STUDENT public static final String STUDENT_ID = "student_id"; // THE ID OF ASSOCIATED COURSE public static final String COURSE_ID = "course_id"; // THE NAME OF THE TABLE public static final String TABLE_NAME = "classes"; }
[ 28 ]
Chapter 2
And here's the code for creating the database schema (this should look very similar to what we saw earlier):
public class SchemaHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "adv_data.db"; // TOGGLE THIS NUMBER FOR UPDATING TABLES AND DATABASE private static final int DATABASE_VERSION = 1; SchemaHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { // CREATE STUDENTS TABLE db.execSQL("CREATE TABLE " + StudentTable.TABLE_NAME + " (" + StudentTable.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + StudentTable.NAME + " TEXT," + StudentTable.STATE + " TEXT," + StudentTable.GRADE + " INTEGER);"); // CREATE COURSES TABLE db.execSQL("CREATE TABLE " + CourseTable.TABLE_NAME + " (" + CourseTable.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + CourseTable.NAME + " TEXT);"); // CREATE CLASSES MAPPING TABLE db.execSQL("CREATE TABLE " + ClassTable.TABLE_NAME + " (" + ClassTable.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + ClassTable.STUDENT_ID + " INTEGER," + ClassTable.COURSE_ID + " INTEGER);"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w("LOG_TAG", "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); // KILL PREVIOUS db.execSQL("DROP db.execSQL("DROP db.execSQL("DROP TABLES IF UPGRADED TABLE IF EXISTS " + StudentTable.TABLE_NAME); TABLE IF EXISTS " + CourseTable.TABLE_NAME); TABLE IF EXISTS " + ClassTable.TABLE_NAME);
So here we see that in our onCreate() method we execute SQL commands to create all three tables, and furthermore, in the onUpgrade() method we execute SQL commands that drop all three tables and subsequently recreate all three tables. Of course, since we are overriding the SQLiteOpenHelper class, in theory we can customize the behavior of these methods in any way we want (for instance, some developer's might not want to drop the entire schema in the onUpgrade() method), but for now let's keep the functionality simple. At this point, for those who are well versed in SQL programming and database schemas, you might be wondering if it's possible to add triggers and key constraints to your SQLite database schemas. The answer is, "yes, you can use triggers but no, you cannot use foreign key constraints." In any case, to spend any time on writing and implementing triggers would be deviating too much from the core content of this book, and so I chose to omit that discussion (though these could certainly be helpful even in our simple example). Now that we have our schema created, before moving on to designing all kinds of complex queries for pulling different groups of data (this we'll see in the next chapter), it's time to write some wrapper methods. This will help us to address some of the questions mentioned previously, which will ultimately help us in creating a robust database.
Chapter 2 // TOGGLE THIS NUMBER FOR UPDATING TABLES AND DATABASE private static final int DATABASE_VERSION = 1; SchemaHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { ... } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { ... } // WRAPPER METHOD FOR ADDING A STUDENT public long addStudent(String name, String state, int grade) { // CREATE A CONTENTVALUE OBJECT ContentValues cv = new ContentValues(); cv.put(StudentTable.NAME, name); cv.put(StudentTable.STATE, state); cv.put(StudentTable.GRADE, grade); // RETRIEVE WRITEABLE DATABASE AND INSERT SQLiteDatabase sd = getWritableDatabase(); long result = sd.insert(StudentTable.TABLE_NAME, StudentTable.NAME, cv); return result; } // WRAPPER METHOD FOR ADDING A COURSE public long addCourse(String name) { ContentValues cv = new ContentValues(); cv.put(CourseTable.NAME, name); SQLiteDatabase sd = getWritableDatabase(); long result = sd.insert(CourseTable.TABLE_NAME, CourseTable.NAME, cv); return result; } // WRAPPER METHOD FOR ENROLLING A STUDENT INTO A COURSE public boolean enrollStudentClass(int studentId, int courseId) { ContentValues cv = new ContentValues();
[ 31 ]
Using a SQLite Database cv.put(ClassTable.STUDENT_ID, studentId); cv.put(ClassTable.COURSE_ID, courseId); SQLiteDatabase sd = getWritableDatabase(); long result = sd.insert(ClassTable.TABLE_NAME, ClassTable.STUDENT_ID, cv); return (result >= 0); } }
Now we have three simple wrapper methods for adding data into our schema. The rst two involve adding new students and new courses into the database and the last one involves adding a new mapping between a student (represented by his/her ID) and a course (essentially, we are enrolling a student into a course through this mapping). Notice that in each wrapper method, we're simply adding the values into a ContentValue object, retrieving the writeable SQLite database, and then inserting this ContentValue as a new row into the specied table. Next, let's write some wrapper methods for retrieving data:
public class SchemaHelper extends SQLiteOpenHelper { public long addStudent(String name, String state, int grade) { } public long addCourse(String name) { } public boolean enrollStudentClass(int studentId, int courseId) { } // GET ALL STUDENTS IN A COURSE public Cursor getStudentsForCourse(int courseId) { SQLiteDatabase sd = getWritableDatabase(); // WE ONLY NEED TO RETURN STUDENT IDS String[] cols = new String[] { ClassTable.STUDENT_ID }; String[] selectionArgs = new String[] { String.valueOf(courseId) }; // QUERY CLASS MAP FOR STUDENTS IN COURSE Cursor c = sd.query(ClassTable.TABLE_NAME, cols, ClassTable.COURSE_ID + "= ?", selectionArgs, null, null, null); return c; }
[ 32 ]
Chapter 2 // GET ALL COURSES FOR A GIVEN STUDENT public Cursor getCoursesForStudent(int studentId) { SQLiteDatabase sd = getWritableDatabase(); // WE ONLY NEED TO RETURN COURSE IDS String[] cols = new String[] { ClassTable.COURSE_ID }; String[] selectionArgs = new String[] { String.valueOf(studentId) }; Cursor c = sd.query(ClassTable.TABLE_NAME, cols, ClassTable.STUDENT_ID + "= ?", selectionArgs, null, null, null); return c; } public Set<Integer> getStudentsByGradeForCourse(int courseId, int grade) { SQLiteDatabase sd = getWritableDatabase(); // WE ONLY NEED TO RETURN COURSE IDS String[] cols = new String[] { ClassTable.STUDENT_ID }; String[] selectionArgs = new String[] { String.valueOf(courseId) }; // QUERY CLASS MAP FOR STUDENTS IN COURSE Cursor c = sd.query(ClassTable.TABLE_NAME, cols, ClassTable.COURSE_ID + "= ?", selectionArgs, null, null, null); Set<Integer> returnIds = new HashSet<Integer>(); while (c.moveToNext()) { int id = c.getInt(c.getColumnIndex (ClassTable.STUDENT_ID)); returnIds.add(id); } // MAKE SECOND QUERY cols = new String[] { StudentTable.ID }; selectionArgs = new String[] { String.valueOf(grade) }; c = sd.query(StudentTable.TABLE_NAME, columns, StudentTable.GRADE + "= ?", selectionArgs, null, null, null); Set<Integer> gradeIds = new HashSet<Integer>(); while (c.moveToNext()) { int id = c.getInt(c.getColumnIndex(StudentTable.ID)); gradeIds.add(id); } [ 33 ]
Here we have three fairly similar methods which allow us to get very practical datasets from our schema: Being able to grab a list of students in a given course Being able to grab a list of courses for a given student Lastly (just to add some complexity), being able to grab a list of students of a certain grade for a given course
Note that in all three methods we begin to play with some of the parameters in the SQLiteDatabase object's query() method, and so now seems like a great time to take a closer look at what those parameters are and what exactly we did previously:
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)
And alternatively:
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) public Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
And just for simplicity, here's how we're calling the previous method:
Cursor c = sd.query(ClassTable.TABLE_NAME, cols, ClassTable.COURSE_ID + "= ?", selectionArgs, null, null, null);
So a quick explanation of the three methods. The rst query() method is the standard one, where you specify the table in the rst argument and then which columns you want to return in the second argument. This is equivalent to performing a SELECT statement in standard SQL. Then, in the third argument we begin to lter our query and the syntax for these lters is equivalent to including a WHERE clause at the end of our SELECT query. In our example, we see that we only ask to return the column containing student IDs, as this is the only column we care about (since we're ltering on the course ID column, it would be unnecessarily redundant to return this column as well). Then, in the lter parameter, we ask to lter by the course ID and the syntax is equivalent to passing in the following String:
WHERE course_id = ? [ 34 ]
Chapter 2
Here, the question mark acts as a place card for whatever value we will pass into the lter. In other words, the format of the WHERE statement is there, but we just need to substitute into the question mark the actual value we want to lter by. In this case, we pass into the fourth parameter the given course ID. The last three arguments (groupBy, having, and orderBy) should make a lot of sense for those familiar with SQL, but for those who aren't, here's a quick explanation of each:
groupBy adding this will allow you to group the results by a specied column(s). This would come in handy if you needed to obtain, say, a table with course IDs and the number of students enrolled in that course: simply grouping by course ID in the Class table would accomplish this. having used in conjunction with a groupBy clause, this clause allows you to lter the aggregated results. Say you grouped by course ID in the Class table and wanted to lter out all classes with having less than 10 students enrolled, you could accomplish this with the having clause. orderBy a fairly straightforward clause to use, the orderBy clause allows
us to sort our query's resulting sub table by a specied column(s) and by ascending or descending order. For instance, say you wanted to sort the Students table by grade and then by name specifying an orderBy clause would allow you to do this.
Lastly, in the two query() variants, you'll see the added parameters limit and distinct: the limit parameter allows you to limit how many rows you want back, and the distinct boolean allows you to specify whether you only want to return distinct rows. If this still doesn't make too much sense to you, no fears we'll focus on building complex queries in the next chapter. Now that we understand how the query() method works, let's revisit our earlier example and ush out the getStudentsByGradeForCourse() method. Though there are many ways to execute this method, conceptually they are all very similar: rst, we query for all the students in the given course, and then of these students we want to lter and only keep those in the specied grade. The way I implemented it was by rst obtaining a set of all student IDs from the given course, then obtaining a set of all the students for the given grade, and simply returning the intersection of those two sets. As for whether or not this is the optimal implementation simply depends on the size of your database.
[ 35 ]
And now, last but not least, let's enforce those removal rules mentioned earlier with some special remove wrapper methods:
public class SchemaHelper extends SQLiteOpenHelper { public Cursor getStudentsForCourse(int courseId) { ... } public Cursor getCoursesForStudent(int studentId) { ... } public Set<Integer> getStudentsAndGradeForCourse(int courseId, int grade) { ... } // METHOD FOR SAFELY REMOVING A STUDENT public boolean removeStudent(int studentId) { SQLiteDatabase sd = getWritableDatabase(); String[] whereArgs = new String[] { String.valueOf(studentId) }; // DELETE ALL CLASS MAPPINGS STUDENT IS SIGNED UP FOR sd.delete(ClassTable.TABLE_NAME, ClassTable.STUDENT_ID + "= ? ", whereArgs); // THEN DELETE STUDENT int result = sd.delete(StudentTable.TABLE_NAME, StudentTable.ID + "= ? ", whereArgs); return (result > 0); } // METHOD FOR SAFELY REMOVING A STUDENT public boolean removeCourse(int courseId) { SQLiteDatabase sd = getWritableDatabase(); String[] whereArgs = new String[] { String.valueOf(courseId) }; // MAKE SURE YOU REMOVE COURSE FROM ALL STUDENTS ENROLLED sd.delete(ClassTable.TABLE_NAME, ClassTable.COURSE_ID + "= ? ", whereArgs); // THEN DELETE COURSE int result = sd.delete(CourseTable.TABLE_NAME, CourseTable.ID + "= ? ", whereArgs); return (result > 0); } } [ 36 ]
Chapter 2
So here we have two remove methods, and in each one we manually enforce some schema rules by preventing someone from dropping a course without rst removing those courses from the Class mapping table and vice versa. We call the SQLiteDatabase class's delete() method which, much like the query() method, allows you to pass in the table name, specify a lter argument (that is, a WHERE clause), and then allows you to pass in those lters' values (note that in both the delete() and query() methods, you can specify multiple lters, but more on this later). Finally, let's put these methods in action and implement an Activity class:
public class SchemaActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); SchemaHelper sh = new SchemaHelper(this); // ADD STUDENTS AND RETURN THEIR IDS long sid1 = sh.addStudent("Jason Wei", "IL", 12); long sid2 = sh.addStudent("Du Chung", "AR", 12); long sid3 = sh.addStudent("George Tang", "CA", 11); long sid4 = sh.addStudent("Mark Bocanegra", "CA", 11); long sid5 = sh.addStudent("Bobby Wei", "IL", 12); // ADD COURSES AND RETURN THEIR IDS long cid1 = sh.addCourse("Math51"); long cid2 = sh.addCourse("CS106A"); long cid3 = sh.addCourse("Econ1A"); // ENROLL STUDENTS IN CLASSES sh.enrollStudentClass((int) sid1, sh.enrollStudentClass((int) sid1, sh.enrollStudentClass((int) sid2, sh.enrollStudentClass((int) sid3, sh.enrollStudentClass((int) sid3, sh.enrollStudentClass((int) sid4, sh.enrollStudentClass((int) sid5,
// GET STUDENTS FOR COURSE Cursor c = sh.getStudentsForCourse((int) cid2); while (c.moveToNext()) { int colid = c.getColumnIndex(ClassTable.STUDENT_ID); int sid = c.getInt(colid);
[ 37 ]
Using a SQLite Database System.out.println("STUDENT " + sid + " IS ENROLLED IN COURSE " + cid2); } // GET STUDENTS FOR COURSE AND FILTER BY GRADE Set<Integer> sids = sh.getStudentsByGradeForCourse ((int) cid2, 11); for (Integer sid : sids) { System.out.println("STUDENT " + sid + " OF GRADE 11 IS ENROLLED IN COURSE " + cid2); } } }
So rst we add some dummy data into our schema; in my case, I will add ve students and three courses, and then enroll those students into some classes. Once the schema has some data in it, I will try out some methods and rst request all the students signed up for CS106A. Afterwards, I will test another wrapper method we wrote and request all the students signed up for CS106A, but this time only those students in grade 11. And so the output from running this Activity is as follows:
And voila! We quickly see that Students 1, 2, 3, and 5 were all enrolled in CS106A. However, after ltering by grade 11, we see that Student 3 is the only one signed up for CS106A in grade 11 poor George. Now let's test out the remove methods:
public class SchemaActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); SchemaHelper sh = new SchemaHelper(this); long sid1 = sh.addStudent("Jason Wei", "IL", 12); [ 38 ]
Chapter 2 // GET CLASSES I'M TAKING c = sh.getCoursesForStudent((int) sid1); while (c.moveToNext()) { int colid = c.getColumnIndex(ClassTable.COURSE_ID); int cid = c.getInt(colid); System.out.println("STUDENT " + sid1 + " IS ENROLLED IN COURSE " + cid); } // TRY REMOVING A COURSE sh.removeCourse((int) cid1); System.out.println("------------------------------"); // SEE IF REMOVAL KEPT SCHEMA CONSISTENT c = sh.getCoursesForStudent((int) sid1); while (c.moveToNext()) { int colid = c.getColumnIndex(ClassTable.COURSE_ID); int cid = c.getInt(colid); System.out.println("STUDENT " + sid1 + " IS ENROLLED IN COURSE " + cid); } } }
This time around, we rst ask for all the classes that Student 1 (myself) is enrolled in. But oh no! Something happened to Math51 this quarter and so it was cancelled! We remove the course and make another request to look at all the classes that Student 1 is enrolled in expecting to see that Math51 has been removed from the list. The output is as follows:
[ 39 ]
Indeed, we see that at rst I was enrolled in both Math51 and CS106A, but after the course was removed, I'm not only enrolled in CS106A! By putting wrappers around some of these common insert, get, and remove functions, we can both simplify our development lives going forward while also enforcing certain schema rules. Finally, let's conclude this chapter with how you can hook into a SQLite terminal to look at your data in table form and also issue SQLite queries something extremely useful when debugging your application and making sure that your data is being added/updated/removed correctly.
or type the following command if you want to target a specic emulator to connect to:
adb s emulator-xxxx shell
At this point, you should have started the adb tool, at which point you need to tell it to connect to the emulator's sqlite3 database. This can be done by issuing the command sqlite3 and then passing the path to your application's database le as follows:
# sqlite3 /data/data/<your-package-path>/databases/<your-database>.db
[ 40 ]
Chapter 2
And at this point, we should be able to issue all kinds of SQL queries that allow us to do everything from looking at our database schema to updating and removing individual rows of data in any of our tables. Some sample commands that you'll probably nd most useful are as follows: .tables shows you a list of all the tables in your database
.output FILENAME allows you to output the results of a query into a le (say, for further analysis) .mode MODE allows you to specify the output le format (that is, as a CSV,
SELECT * FROM table_name standard query for selecting all columns of a given table (this is equivalent to a get() command for rows of a table) SELECT * FROM table_name WHERE col = 'value' standard query for
And here's an example of us putting some of these commands to use with our previous schema:
[ 41 ]
Hopefully this should get you going, but for a full list of sqlite3 commands just check out http://www.sqlite.org/sqlite.html, and for a more extensive list of complex queries just stay put it's coming up next.
Summary
In this chapter, we went from a super bare-bones database schema that just contained one table to an entire schema containing multiple interdependent tables. We rst saw how to create and upgrade multiple tables through overriding the SQLiteOpenHelper class, and then thought about some of the challenges surrounding a database schema with interdependencies. We decided to tackle these challenges by surrounding our database schema and its tables with an army of wrapper methods, designed for both ease of future development, as well as robustness in future data. These wrapper methods included everything from simple add methods, helpful as we were able to conceal the need to constantly request a writeable SQLiteDatabase, to more complex remove methods which concealed all of the functionality needed for enforcing various schema rules. Then, we actually implemented an Activity class to show off our new database schema and ran through some sample database commands to test their functionality. Though we were able to validate and output the results of all our commands, we realized that this was a pretty verbose and suboptimal way for debugging our sqlite3 database, and so we looked into the Android Debug Bridge (adb) tool. With the adb tool, we were able to open a command-line terminal that then hooked into a running instance of an emulator or Android device, and subsequently, connect to that emulator/device's sqlite3 database. Here we were able to interact with the sqlite3 database in a very natural way by issuing various SQL commands and queries. Now, the queries that we've seen so far have been pretty elementary, but if necessary, will do the trick for the majority of your application development needs. However, we'll see in the next chapter that by mastering more advanced SQL query concepts, we can enjoy both a substantial performance boost as well as a substantial memory boost in our application!
[ 42 ]
Alternatively, you can buy the book from Amazon, BN.com, Computer Manuals and most internet book retailers.
www.PacktPub.com