9.3.2 Android数据库的使用
Android中使用android.database.sqlite.SQLiteDatabase来表示一个数据库对象,它提供了两种模式,来帮助开发者进行增删改查等基本数据库操作。
最原始的方式,就是利用SQL语句描述操作,调用SQLiteDatabase.execSql或SQLiteDatabase.rawQuery来执行操作。示例如下:
//利用sql查询数据
Cursor data=db.rawQuery("select id, name from contacts");
//利用sql插入数据
db.execSql("insert into contacts(id, name)values(2,'jack')");
此外,Android更提倡使用结构化的方式描述数据库的操作,这样开发者不需要学习如何维护SQL语句,而是使用最熟悉的面向对象的方式进行数据库操作。与前例同样的功能实现如下:
//结构化的方式查询数据
Cursor data=db.query("contacts",
new String[]{"id","name"},null, null, null, null, null);
//结构化的方式插入数据
ContentValues values=new ContentValues();
values.put("id",2);
values.put("name","jack");
db.insert("contacts",null, values);
在实践中,开发者如果只需要进行较为简单的数据增删改查操作,那么应该首选结构化的方式进行数据库操作。一方面,这种方式更接近于面向对象的编程方式,对于不熟悉SQL语言的开发者而言,更利于理解;另一方面,这种方式也与对数据源组件的调用兼容,彼此可以更容易地进行转换。
但如果描述的数据库操作过于复杂,比如使用复合的查询语句,增加索引和构建多个数据表,那么就应该直接使用SQL语句来进行描述,这样更为直接,也更易于维护。
在实践中,有的SQL语句需要被反复使用,为了避免反复解析SQL语句产生的开销,可以对需要复用的SQL语句进行预编译,来提高数据库操作的执行效率。示例如下:
//编译复杂的SQL语句
SQLiteStatement compiledSql=db.compileStatement(aSQL);
//执行SQL
compiledSql.execute();
除了基本的数据库操作,Android还提供了丰富的高级数据库功能(得益于Sqlite的支持),比如:支持触发器、支持复合索引以及支持对数据库事务的处理:
try{
db.beginTransaction();
…//执行相关的数据库操作,如有异常,直接进入finally部分
db.setTransactionSuccessful();
}finally{
//不论成功与否,都需要调用endTransaction来结束事务
//如果事务执行成功,则提交,否则,则回滚
db.endTransaction();
}
为了帮助开发者更好地构建和使用SqliteDatabase, Android提供了android.database.sqlite.SQLiteOpenHelper类。在SQLiteOpenHelper中,封装了一个SqliteDatabase对象,使用者可以通过SQLiteOpenHelper.getReadalbeDatabase或者SQLiteOpenHelper.getWriteableDatabase来获取该SqliteDatabase对象。顾名思义,用SQLiteOpenHelper.getReadalbeDatabase获取到的是一个只读的SqliteDatabase对象,而用SQLiteOpenHelper.getWriteableDatabase获取到的是一个可读写的SqliteDatabase对象。如果调用这两个函数时,数据库尚未构建,或者已存在的数据库与目标版本不一致,SQLiteOpenHelper就会尝试对数据库进行构造或者版本变更。子类需要派生SQLiteOpenHelper.onCreate等函数,来实现构造和变更版本的逻辑:
class MyDatabaseHelper extends SQLiteOpenHelper{
//定义当前数据库的版本信息,需要大于0
private static final int DATABASE_VERSION=4;
MyDatabaseHelper(Context context, String name){
super(context, name, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db){
//数据库首次构造时,会调用该函数,可以在这里构建表、索引,等等
createTables(db);
createIndexes(db);
…
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
//如果给定的当前数据库版本高于已有数据库版本,调用该函数
//在实践中,通常会根据两次表结构的差距,进行修改
//如果原有数据库中的数据不那么重要,也可以简单地删除重新创建
if(oldVersion==1){
//针对这两次的不同差距,部署升级逻辑
upgradeTables(…);
}else if(oldVersion==2){
…
}else{
…
}
}
@Override
void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion){
//如果给定的当前数据库版本低于已有数据库版本,调用该函数
//在实践中,如果发生数据库回滚,也需要按照逻辑进行数据回滚
//由于这常发生在老版本已发布的情况下,所以最常见的策略是删除重建
//该接口在Android 3.0以上版本才提供,默认实现为空
DropAllTables();
DropAllIndexes();
…
}
@Override
public void onOpen(SQLiteDatabase db){
//数据库每次打开时,都会调用该函数,通常都无需做什么
}
}
开发者把数据库构建和版本变更的逻辑都统一封装到SqliteOpenHelper中后,SqliteDatabase的使用和获取就更为简单了:
//创建SqliteOpenHelper对象
MyDatabaseHelper helper=new MyDatabaseHelper(context,"my_database");
//使用SqliteOpenHelper对象来进行数据库操作
SQLiteDatabase database=helper.getWritableDatabase();
database.execSql(…);
…
并发问题是使用数据库过程中最容易碰到的问题,如果在开发中碰到了android.database.SQLException异常,并提示"database is locked",那很有可能是出现了数据库的死锁导致无法访问。在Android数据库底层Sqlite的实现中,对文件的读写会进行加锁保护,避免数据在并发读写中被破坏,而在Android框架层SqliteDatabase会对所有数据库对象进行加锁保护,通过同一个SqliteDatabase对象访问数据库是线程安全的。但在实践中,一旦出现了指向同一个数据库的多个SqliteDatabase对象同时在多个线程中被使用,那就跳脱了SqliteDatabase中的锁保护,就会导致数据库出现被锁的异常。
因此在实践中,需要保证同时访问数据库的SqliteDatabase对象仅有一个。比如,在构造数据源对象时,会用成员变量来保存SqliteOpenHelper对象(本质上也就是保持一个SqliteDatabase对象),在整个数据源对象中使用同一个连接。又比如,在开发中也可以将打开的SqliteDatabase对象放到应用环境对象中,在整个进程中保持其唯一性,来控制并发访问数据库的安全性。
通过加锁的方式保证线程安全,在操作非常密集的时候代价是非常大的,会明显降低数据库的读写性能。在实践中,可以将概念上并发的操作转换成为串行的操作序列(数据源组件就是如此),避免对数据的并发访问。对于不存在并发读写的数据库,可以调用SQLiteDatabase.setLockingEnable函数,将数据库设置为线程非安全模式,不通过加锁进行保护,以提升性能:
db.setLockingEnable(false);
小贴士 在Android SDK中提供了工具Sqlite3,在shell模式下,可以使用它来进行各种数据库的增删改查操作,辅助开发人员进行调试。一个示例如下:
//进入shell模式
$adb shell
//加载数据库,进入sql操作模式
sqlite3 data/data/com.smaple.database/databases/test.db
//执行各种sql语句
sqlite>select*from sample
…