2016-03-16
Android数据库支持
本文节选并翻译《Enterprise Android - Programing Android Database Applications for the Enterprise》。
学习目标:
- 学习Android对SQL的支持。
- 理解在Java中使用SQL——通过SQLiteDatabase类。
- 创建数据库——SQLiteOpenHelper类。
- 理解loaders、cursors和adapters。
为了在程序中使用本地的,结构化的数据,需要完成以下事情:
- 在java代码中嵌套SQL命令,程序运行时执行它们。
- 根据需要创建,初始化,并升级数据库。
- 选择一种针对当前程序的数据库生命周期管理策略。
- 解析查询得到的数据,在程序中使用它们。
Java中执行SQL:SQLiteDatabase类
为了在java代码中针对SQLite数据库执行一些SQL查询等操作,Android 框架提供了SQLiteDatabase类。通过获得一个SQLiteDatabase对象实例,可以执行一些基本的,底层的数据库操作。
以下使用db表示一个SQLiteDatabase对象
db.execSQL(String sql)
execSQL是一个 同步方法
,它接收原始的SQL语句——那些可以在SQLite CmdLine中执行的SQL命令。方法执行完毕,SQL语句操作即执行完毕。
- 那些以“.”开头的命令只能在命令行执行,它们是sqlite3 命令行工具,不是execSQL可执行的SQL语句。
- execSQL每次只能执行一个有效的SQL语句。
- execSQL的执行不能返回任何数据,如果在这里传递一个query作为sql语句,那么会引起SQLiteException异常。
db.rawQuery(String sql)
rawQuery可以执行sql并返回Cursor作为结果:
Cursor c = db.rawQuery("pragma table_info(" + tableName + ")", null);
execSQL和rawSQL不应该作为代码中执行SQL的一般选择,应该尽量使用其它等价的SQL API来代替它们。execSQL的方便之处在于创建数据库结构
,通常来说rawSQL是完全应该避免使用的。
execSQL和rawSQL方法都接收bindArgs
参数,方便在SQL中传递变量。避免SQL注入这样的问题。
SQL语句的等价API
为了避免使用SQL字符串作为代码中执行SQL的途径——这需要良好的SQL知识,而且,很难像普通代码那样被调试和排查。Android SQLite API还提供了一系列的API来对应不同的SQL语法。包括insert、update、delete和query等,相应地,还有一些等价的简化方法和数据库管理方法。
delete
db.delete("pets", "age > 10 AND age < 20", null);
db.delete("pets", "age = ? OR name = ?", new String[] {"15", "linus"});
此方法是删除数据的SQL的一个简单拆分,比rawSQL略参数化些。
update
ContentValues newAges = new ContentValues();
newAges.put("age", Integer.valueOf(99));
db.update(
"pets",
newAges,
"name = ? OR name = ?",
new String[] {"linus", "fellini"});
类ContentValues提供了一组列名和值的绑定。
insert
ContentValues newPet = new ContentValues();
newPet.put("name", "luna");
newPet.put("age", 99);
db.insert("pets", null, newPet);
insert方法不抛出异常,返回-1表示失败。而update和delete在违反关系数据库的一些约束时会抛出SQLiteException表达执行错误。
可以使用insertOrThrow来主动抛出执行错误的异常。
replace
如果记录不存在就insert,否则对已存在记录执行update。
query
查询方法是最复杂的一类数据库操作,对应了一组API。一个完整的查询SQL看起来如下:
SELECT table1.name, sum(table2.price)
FROM table1, table2
WHERE table1.supplier = table2.id AND table1.type = "spigot"
GROUP BY table1.name
HAVING sum(table2.price) < 50
ORDER BY table1.name ASC
LIMIT 100
查询条件为空可以使用null来代替。query方法接收selection和selectionArgs两个参数。前者可包含一些参数标记,后者是对应标记的实际值。
对应示例如下:
Cursor c = db.query(
"pets",
new String[] { "name", "age" },
"age > ?",// selection
new String[] { "50" },//selectionArgs
null, // group by
null, // having
"name ASC");
要对超过一个表进行联合查询,需要借助SQLiteQueryBuilder来构建对应的SQL,之后使用query方法执行此SQL即可。SQLiteQueryBuilder负责检查对应的SQL语法错误,避免SQL注入。
外键约束和事务
SQLite默认不开启外键约束,可以使用setForeignKeyConstraintsEnabled来开启外键约束。但是不同API版本的行为和设置方式会有差异。同样的,触发器这样的特性也不要过于依赖。最基本的,主键和列的唯一约束,自增等都是支持的。应该保持SQLite的轻量级和高效,可以在代码中自行组合方法来完成约束的实现。
最后,SQLite对事务有完整的支持:
db.beginTransaction();
try {
// sql...
db.setTransactionSuccessful();
}
finally {
db.endTransaction();
}
SQLiteDatabase类提供的其它一些方法涉及到数据库的删除和创建,但是,使用SQLiteOpenHelper来完成对数据库的管理是最好的选择。
创建数据库:使用SQLiteOpenHelper
在典型的网站后台这样的应用中,数据库的设计和创建是一项独立且完整的任务,这些过程更像是软件部署的一个环节,而不是程序执行的一部分。
Android应用则是外全不同的情况,用户下载并运行apk来安装一个程序,其apk中包含所有相关的数据,安装过程程序自身完成各种引导和设置。如果需要数据库,程序自身负责创建它。而SQLiteOpenHelper类就是用来提供数据库结构创建和升级的功能。
SQLiteOpenHelper是一个抽象类,它提供了一个创建数据库需要的模板,对应每一个数据库,都需要一个SQLiteOpenHelper的子类来完成对其的创建和升级。
当程序运行时,执行的代码请求一个数据库实例时,帮助类会检查数据库文件是否存在,不存在就创建对应名称的数据库文件,之后执行onCreate方法完成对数据库结构(主要就是各种表)的初始化。
我们应该一直通过帮助类来获得
数据库对应的SQLiteDatabase对象,因为它保证返回给我们的是完整、初始化好的、可使用的数据库(这里指数据库连接已打开)。最好不要自己的类中去使用字段持有一个SQLiteDatabase对象,Helper类提供了数据库对象的创建,打开和关闭方法,自己维护的SQLiteDatabase对象对象很容易陷入一个废弃、无法使用的状态。
不要在onCreate中调用会直接或间接执行getReadableDatabase或getWriteableDatabase的方法or代码。可以想象,这会陷入方法的循环执行。
数据库版本
数据库的onCreate方法接收一个大于0的int参数version作为对应数据库的版本标识,作为数据库的元数据。
帮助类在检查数据库的存在性时,同时会检查数据库的版本,如果当前的version参数和现有数据库的版本号不一致,则根据大小关系执行onUpgrade和onDowngrade方法。
这两个方法中可以对表结构进行调整,更重要的是,在数据库表结构的变化过程中,自己的代码需要尽可能根据需要保持用户数据,避免丢失。这两个方法的执行都是事务性
的。
一个好的建议:使用alter table修改原表名,之后创建同名的新表(结构会有变化,但某些列是不变的),然后将数据拷贝到新表。
onConfigure和onOpen
一些情况下,数据库是开启了外键约束的,这会影响数据库升级和降级的代码逻辑。
可以使用以下两个方法来达到暂时性的开启和关闭外键约束这样的目的:
- onConfigure 方法在数据库连接成功后立即执行——在onCreate、onUpgrade和onDowngrade方法的前面。此处执行setForeignKeyConstraintsEnabled会强制约束生效——对于数据库的整个操作过程。
- onOpen 方法在onCreate、onUpgrade和onDowngrade之后执行,使得这三个和数据库结构创建和修改的方法的执行可以更自由和快速。例如像简单的改表名这样的操作,应该暂时无视外键约束。onOpen方法在数据库结构完全初始化之后执行,那么此处执行setForeignKeyConstraintsEnabled方法,可以让外键约束在数据库结构初始化完成后才生效。
实际获得一个数据库对象的操作可能会很耗时,因为第一次的数据库创建或升级会涉及到表的创建甚至数据的拷贝,所以需要注意这些操作的异步执行。相反的,SQLiteOpenHelper对象本身的创建是非常快速的。对应getReadableDatabase 和 getWriteableDatabase的执行会引起对实际数据库对象的创建和获取,使用loader可以完成对数据库的异步访问。
数据库对象的管理
安卓应用程序在使用数据库时,需要考虑对SQLiteDatabase对象的生命周期的管理。一个打开的数据库对象大约占1KB内存。
数据库对象的管理有以下2种策略:
- 获得并一直持有db对象(Get it and keep it)。
- 仅在需要的时候获取并使用db对象(Get it when you need it)。
一直持有db对象
这是一个很理想且简单的db对象管理方式——除非有进程内存的限制考虑。当然,若对数据库的访问操作仅仅是整个程序中多个Activity中的个别在使用,那么显然没有必要一直保持着db对象。
当程序在作为后台程序很长时间后,安卓系统会选择杀死进程。那么,程序拥有的db对象、任何数据库连接、以及任何程序进程相关的内存资源都会被释放掉。(As
long as you’ve left the database in a consistent state — no uncommitted transactions and no open file
connections to large objects (BLOBs) — tweaking soon-to-be-deallocated memory is a waste of effort.)一旦你让数据库保持在这样一个不变的状态时——没有任何未提交的事务,没有任何对大对象文件的打开的连接时——去纠缠那些很快就会被释放的内存显然是没必要的。
这个策略虽然简单,还是需要注意:
如果代码忘了显式关闭db实例,那么GC仅仅是回收此对象,这样会产生一个错误信息:
09-02 15:27:10.286: E/SQLiteDatabase(16433): close() was never explicitly called on
database '/data/data/net.callmeike.android.sqlitetest/databases/test.db'
09-02 15:27:10.286: E/SQLiteDatabase(16433): android.database.sqlite.
DatabaseObjectNotClosedException: Application did not close the cursor or database
object that was opened here
09-02 15:27:10.286: E/SQLiteDatabase(16433): at android.database.sqlite.SQLiteD
atabase.<init>(SQLiteDatabase.java:1943)
09-02 15:27:10.286: E/SQLiteDatabase(16433): at android.database.sqlite.
SQLiteDatabase.openDatabase(SQLiteDatabase.java:1007)
...
09-02 15:27:10.286: E/System(16433): Uncaught exception thrown by finalizer
09-02 15:27:10.297: E/System(16433): java.lang.IllegalStateException: Don't have
database lock!
上面问题的一个典型场景就是:在一个Activity中定义了字段来保持一个db对象的引用,当程序不可见——转为后台程序时,一旦Activity对象被GC,那么此db对象失去引用,也会被回收,我们无法再访问它——也就无法去关闭db对象的连接了。
为了获得并保持一个db对象,应该使用一个强引用来指向它。可以通过一个静态变量或者是Application对象的变量来引用db对象。在Application对象中定义引用db对象的字段是很好的做法——这样可以很方便实现在多个Activity之间共享此db对象。当然,直接将Application对象设计为单例模式来全局访问也是可以的。
public class KeyValApplication extends Application {
private KeyValHelper dbHelper;
private Thread uiThread;
@Override
public void onCreate() {
super.onCreate();
// ...
uiThread = Thread.currentThread();
dbHelper = new KeyValHelper(this);
}
public SQLiteDatabase getDb() {
if (Thread.currentThread().equals(uiThread)) {
throw new RuntimeException("Database opened on main thread");
}
return dbHelper.getWriteableDatabase();
}
}
注意,不要在UI线程中执行实际打开数据库连接的操作——它(很可能)是耗时操作。
dbHelper对象会创建并缓存准备好的db对象,正常情况下多次调用getWriteableDatabase和getReadableDatabase都返回的是同一个db对象,所以,我们没必要自己“缓存”一个db对象,关闭db对象也应该通过dbHelper.close()方法来关闭。
在文件系统被占满这样的极端情况下,dbHelper只能返回给我们一个只读的db,但当文件系统又有空闲的时候,dbHelper又会返回一个新的db对象——它是可读写的,之前的db对象被close并释放掉。
所以,dbHelper完全负责我们要用到的db对象的创建、关闭和引用的释放,我们自己的代码中——也就是使用db对象执行操作的方法中,使用局部变量暂时持有db对象引用,或直接使用getDb()这样的访问器代替变量来获得db对象——不要在自己的类中使用字段(成员变量)来引用获得的db对象——你几乎无法正确的维护它!
最好的做法就是一直使用getWriteableDatabase(它比getReadableDatabase更灵活,而且getReadableDatabase通常返回的就是同一个db对象)获得db对象并直接使用,不要自己去维护它。
Cursor & Loader & Adapter
//待续...