Sqlite in Android
在Android上保存本地数据有三种方式,SharedPreferences、Files和Sqlite。SharedPreferences主要是用来保存键值对形式的程序配置信息,与ini、properties操作类似。Files主要是用来保存图片、音视频等二进制数据。Sqlite是一种轻量级的关系型数据库,运行在android系统中为应用程序提供按照表结构存储数据的功能。Sqlite并不想与传统的大型的关系型数据库竞争,它的目标很明确,在硬件资源有限的情况下,为应用程序提供基本的数据库操作功能。因此,Sqlite在嵌入式系统、智能移动设备上广泛应用。同样,因为轻量的原因,Sqlite在处理并发性的能力上比起大型的数据库就要欠缺很多。这个问题不是Sqlite的缺陷而是它的特点,作为开发人员需要掌握它。Sqlite提供的是文件锁,即在同一时间内,只能有一个程序对它进行写操作,可以有多个程序对它进行读操作。这对于在嵌入式系统中运行的程序来说,应该足够了,并且程序的结构也可以很清晰,实现一个全局的写操作连接由整个程序共享,并且通过代码控制其并发操作,同时允许多个读操作连接并发执行。但是这种想法在Android上完全是错误的。
在Android的官网上有一个操作Sqlite的例子,正因为是官方发布的例子,那么可以想象有多少开发人员会在实际的项目中用到它。
public class FeedReaderDbHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. public static final int DATABASE_VERSION = 1; public static final String DATABASE_NAME = "FeedReader.db"; public FeedReaderDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } public void onCreate(SQLiteDatabase db) { db.execSQL(SQL_CREATE_ENTRIES); } public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // This database is only a cache for online data, so its upgrade policy is // to simply to discard the data and start over db.execSQL(SQL_DELETE_ENTRIES); onCreate(db); } public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { onUpgrade(db, oldVersion, newVersion); } }
Google为Android开发出了一个SQLiteOpenHelper来管理Sqlite,包括创建数据库文件、创建表和数据库更新。在使用Sqlite时,程序实例化SQLiteOpenHelper的子类,然后再进行相关操作。
FeedReaderDbHelper mDbHelper = new FeedReaderDbHelper(getContext()); SQLiteDatabase db = mDbHelper.getWritableDatabase(); // Create a new map of values, where column names are the keys ContentValues values = new ContentValues(); values.put(FeedEntry.COLUMN_NAME_ENTRY_ID, id); values.put(FeedEntry.COLUMN_NAME_TITLE, title); values.put(FeedEntry.COLUMN_NAME_CONTENT, content); // Insert the new row, returning the primary key value of the new row long newRowId; newRowId = db.insert( FeedEntry.TABLE_NAME, FeedEntry.COLUMN_NAME_NULLABLE, values);
以上代码的标题赫然写着:Put Information into a Database。紧接着,下面代码的标题是:Read Information from a Database。
SQLiteDatabase db = mDbHelper.getReadableDatabase(); // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { FeedEntry._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_UPDATED, ... }; // How you want the results sorted in the resulting Cursor String sortOrder = FeedEntry.COLUMN_NAME_UPDATED + " DESC"; Cursor c = db.query( FeedEntry.TABLE_NAME, // The table to query projection, // The columns to return selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order );
看到这里,自信的开发人员可能已经感到在Android上操作Sqlite已经没有问题了,一个getWritableDatabase()和一个getReadableDatabase()。再加上Sqlite文件锁这个特点,那么程序理所当然是将所有涉及写操作的函数用synchronized保护起来,而读操作可以任意执行,强大的Google已经分离了WritableDatabase和ReadableDatabase。但事实并非如此,官网上的这个例子以及这些API的命名产生了很大的混淆。Kevin Galligan在他的博客《Android Sqlite Locking》中讨论过这个问题,并专门写了一个测试程序证明多线程操作Sqlite时会导致数据库操作失败。为了突出问题便于测试,在该测试程序的基础上做了一些修改以表明:在正常情况下,同一个SQLiteOpenHelper的getWritableDatabase()和一个getReadableDatabase()返回的是同一个SQLiteDatabase并且是线程安全的。因此,数据库操作会串行执行,而并不能实现一个写、多个读的结构。下面代码故意使用getReadableDatabase()实现写操作,getWritableDatabase()实现读操作,同时在LogCat中打印出不同线程中SQLiteDatabase对象的Hash Code。
public void insert(String desc) { SQLiteDatabase readableDatabase = getReadableDatabase(); Log.i(Thread.currentThread().toString(), "insert: " + readableDatabase.hashCode()); ContentValues contentValues = new ContentValues(); contentValues.put("description", desc); readableDatabase.insertOrThrow("session", null, contentValues); } public int count() { SQLiteDatabase writableDatabase = getWritableDatabase(); Log.i(Thread.currentThread().toString(), "count: " + writableDatabase.hashCode()); Cursor cursor = writableDatabase.rawQuery( "select count(*) from session", null); cursor.moveToFirst(); return cursor.getInt(0); }
通过上面的测试可以看到,一个SQLiteOpenHelper实际代表着一个数据库连接,对于一个数据库(在Sqlite中相当于一个.db文件)同时只能建立一个连接执行数据库操作,所以在程序中操作同一个数据库时SQLiteOpenHelper应该是唯一的,SQLiteDatabase对象可以有多个,并且能在多线程中执行操作。在Android自带的Samples里有一个Notepad例子,它采用了ContentProvider作为操作Sqlite的接口。这种方式就是让系统初始化一个SQLiteOpenHelper对象,供其它程序使用。它有一个明显的缺陷,针对每个表都需要实现一个专门的ContentProvider从而使得表间操作相当繁琐。
OrmLite这个持久层的框架解决了上述问题。在OrmLite里是使用OpenHelperManager来管理SQLiteOpenHelper,通过查看源代码可以看到OpenHelperManager使用了单例模式来确保SQLiteOpenHelper的唯一性。在下面的工厂方法中有两个小技巧:第一,Context实际上是获取的整个应用程序的上下文;第二,设置了一个引用计数器,instanceCount,确保程序通过releaseHelper()能够正确关闭SQLiteOpenHelper。
private static <T extends OrmLiteSqliteOpenHelper> T loadHelper(Context context, Class<T> openHelperClass) { if (helper == null) { if (wasClosed) { logger.info("helper was already closed and is being re-opened"); } if (context == null) { throw new IllegalArgumentException("context argument is null"); } Context appContext = context.getApplicationContext(); helper = constructHelper(appContext, helperClass); logger.trace("zero instances, created helper {}", helper); DaoManager.clearDaoCache(); instanceCount = 0; } instanceCount++; logger.trace("returning helper {}, instance count = {} ", helper, instanceCount); @SuppressWarnings("unchecked") T castHelper = (T) helper; return castHelper; }