2016-03-16

Android数据库支持

本文节选并翻译《Enterprise Android - Programing Android Database Applications for the Enterprise》。
学习目标:

  1. 学习Android对SQL的支持。
  2. 理解在Java中使用SQL——通过SQLiteDatabase类。
  3. 创建数据库——SQLiteOpenHelper类。
  4. 理解loaders、cursors和adapters。

为了在程序中使用本地的,结构化的数据,需要完成以下事情:

  1. 在java代码中嵌套SQL命令,程序运行时执行它们。
  2. 根据需要创建,初始化,并升级数据库。
  3. 选择一种针对当前程序的数据库生命周期管理策略。
  4. 解析查询得到的数据,在程序中使用它们。

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

//待续...

Android中SQLite数据库小计的更多相关文章

  1. Android中SQLite数据库操作(1)——使用SQL语句操作SQLite数据库

    下面是最原始的方法,用SQL语句操作数据库.后面的"Android中SQLite数据库操作(2)--SQLiteOpenHelper类"将介绍一种常用的android封装操作SQL ...

  2. android中sqlite数据库的基本使用和添加多张表

    看了很多关于android使用sqlite数据库的文章,很多都是介绍了数据库的建立和表的建立,而表通常都是只建立一张,而实际情况我们用到的表可能不止一张,那这种情况下我们又该怎么办呢,好了,下面我教大 ...

  3. 我的Android六章:Android中SQLite数据库操作

    今天学习的内容是Android中的SQLite数据库操作,在讲解这个内容之前小编在前面有一篇博客也是讲解了SQLite数据库的操作,而那篇博客的讲解是讲述了 如何在Window中通过DOM来操作数据库 ...

  4. Android中SQLite数据库操作(2)——SQLiteOpenHelper类

    如果开发者对SQL语法不熟悉,我要告诉你一个好消息,Android提供了一个SQLiteOpenHelper类. 在实际项目中很少使用SQLiteDatabase的方法(请看:http://blog. ...

  5. Android 中 SQLite 数据库的查看

    当 SQLite 数据库创建完成后,如何查看数据库的内容呢?如果直接使用 File Explorer 查看,最多只能看到 database 目录下出现了一个 BookStore.db 文件,Book ...

  6. Android中Sqlite数据库进行增删改查

    今天这篇文章写Sqlite数据库,通过一个小案例来完整讲一下数据库常见的CRUD操作. 先对知识点总结: SQLite数据库 轻量级关系型数据库 创建数据库需要使用的api:SQLiteOpenHel ...

  7. Android中Sqlite数据库多线程并发问题

    最近在做一个Android项目, 为了改善用户体验,把原先必须让用户“等待”的过程改成在新线程中异步执行.但是这样做遇到了多个线程同时需要写Sqlite数据库,导致操作数据库失败. 本人对Java并不 ...

  8. Android中SQLite数据库操作(2)——使用SQLiteDatabase提供的方法操作数据库

    如果开发者对SQL语法不熟,甚至以前从未使用过任何数据库,Android的SQLiteDatabase提供了insert.update.delete或query语句来操作数据库. 一.insert方法 ...

  9. Android 开发中 SQLite 数据库的使用

    SQLite 介绍 SQLite 一个非常流行的嵌入式数据库,它支持 SQL 语言,并且只利用很少的内存就有很好的性能.此外它还是开源的,任何人都可以使用它.许多开源项目((Mozilla, PHP, ...

随机推荐

  1. SQL谜题(加减符号替代)

    问题:将以下字符串”.1.2.3.4.5.6.7.8.9 = 1“中的符号点(.)更改为符号加(+)或符号(-),有多少种方法?请用SQL解决此问题 计算过程: CREATE TABLE #(VAL ...

  2. 组合模式/composite模式/对象结构型模式

    组合模式/composite模式/对象结构型 意图 将对象组合成树形结构以表示"整体--部分"的层次结构.Composite使得用户对单个对象和组合对象的使用具有一致性. 动机 C ...

  3. OpenCv遍历图像小结

    参考:http://www.cnblogs.com/ronny/p/opencv_road_2.html http://blog.csdn.net/xiaowei_cqu/article/detail ...

  4. 用opencsv文件读写CSV文件

    首先明白csv文件长啥样儿: 用excel打开就变成表格了,看不到细节 推荐用其它简单粗暴一点儿的编辑器,比如Notepad++, csv文件内容如下: csv文件默认用逗号分隔各列. 有了基础的了解 ...

  5. 了解 Spring Data JPA

    前言 自 JPA 伴随 Java EE 5 发布以来,受到了各大厂商及开源社区的追捧,各种商用的和开源的 JPA 框架如雨后春笋般出现,为开发者提供了丰富的选择.它一改之前 EJB 2.x 中实体 B ...

  6. linux中redis安装

    一.登录redis官网下载redis-3.0.7.tar.gz 二.通过ftp工具上传至自己的服务器中 三.tar -zxvf redis-3.0.7.tar.gz解压 四.cd redis-3.0. ...

  7. 【转】TCP协议

    TCP是什么? TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的.可靠的. 基于IP的传输层协议.TCP在IP报文的协议号是6.TCP是一 ...

  8. 【BZOJ1700】[Usaco2007 Jan]Problem Solving 解题 动态规划

    [BZOJ1700][Usaco2007 Jan]Problem Solving 解题 Description 过去的日子里,农夫John的牛没有任何题目. 可是现在他们有题目,有很多的题目. 精确地 ...

  9. 时代杂志发文:2017 AR/MR将变得比VR更加重要

    每到年末都有很多企业或高管分析科技产业明年趋势.近日,时代杂志网页版刊登了2017年科技行业的五大趋势和热点话题的预测.该本作者TimBajarin,是硅谷市场研究公司CreativeStrategi ...

  10. ECF R9(632E) & DP

    Description: 给你$n$个数可以任取$k$个(可重复取),输出所有可能的和. $n \leq 1000,a_i \leq 1000$ Solution: 好神的DP,我们排序后把每个数都减 ...