Android中SQLite数据库小计
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
//待续...
Android中SQLite数据库小计的更多相关文章
- Android中SQLite数据库操作(1)——使用SQL语句操作SQLite数据库
下面是最原始的方法,用SQL语句操作数据库.后面的"Android中SQLite数据库操作(2)--SQLiteOpenHelper类"将介绍一种常用的android封装操作SQL ...
- android中sqlite数据库的基本使用和添加多张表
看了很多关于android使用sqlite数据库的文章,很多都是介绍了数据库的建立和表的建立,而表通常都是只建立一张,而实际情况我们用到的表可能不止一张,那这种情况下我们又该怎么办呢,好了,下面我教大 ...
- 我的Android六章:Android中SQLite数据库操作
今天学习的内容是Android中的SQLite数据库操作,在讲解这个内容之前小编在前面有一篇博客也是讲解了SQLite数据库的操作,而那篇博客的讲解是讲述了 如何在Window中通过DOM来操作数据库 ...
- Android中SQLite数据库操作(2)——SQLiteOpenHelper类
如果开发者对SQL语法不熟悉,我要告诉你一个好消息,Android提供了一个SQLiteOpenHelper类. 在实际项目中很少使用SQLiteDatabase的方法(请看:http://blog. ...
- Android 中 SQLite 数据库的查看
当 SQLite 数据库创建完成后,如何查看数据库的内容呢?如果直接使用 File Explorer 查看,最多只能看到 database 目录下出现了一个 BookStore.db 文件,Book ...
- Android中Sqlite数据库进行增删改查
今天这篇文章写Sqlite数据库,通过一个小案例来完整讲一下数据库常见的CRUD操作. 先对知识点总结: SQLite数据库 轻量级关系型数据库 创建数据库需要使用的api:SQLiteOpenHel ...
- Android中Sqlite数据库多线程并发问题
最近在做一个Android项目, 为了改善用户体验,把原先必须让用户“等待”的过程改成在新线程中异步执行.但是这样做遇到了多个线程同时需要写Sqlite数据库,导致操作数据库失败. 本人对Java并不 ...
- Android中SQLite数据库操作(2)——使用SQLiteDatabase提供的方法操作数据库
如果开发者对SQL语法不熟,甚至以前从未使用过任何数据库,Android的SQLiteDatabase提供了insert.update.delete或query语句来操作数据库. 一.insert方法 ...
- Android 开发中 SQLite 数据库的使用
SQLite 介绍 SQLite 一个非常流行的嵌入式数据库,它支持 SQL 语言,并且只利用很少的内存就有很好的性能.此外它还是开源的,任何人都可以使用它.许多开源项目((Mozilla, PHP, ...
随机推荐
- 用winform程序来了解委托和事件
一.浅谈委托 如果有个过winform 和webform 程序开发的小伙伴一定有个这样的感觉吧,点击Button直接就执行了那个方法,到此他是怎么实现了的呢,大家有考虑过没有? 回到正题,什么是委托呢 ...
- curl的用法以及个人理解(php)
php curl的个人理解 1.首先curl的官方解释为:curl是利用URL语法在命令行方式下工作的开源文件传输工具.{它只是一种传输工具!} 2.curl就是抓取网页的升级版本,支持POST.GE ...
- js立即调用的函数表达式
1.多种实现 // 下面2个括弧()都会立即执行 (function () { /* code */ } ()); // 推荐使用这个 (function () { /* code */ })(); ...
- 【Hibernate框架】对象的三种持久化状态
一.综述 hibernate中的对象有三种状态,分别是TransientObjects(瞬时对象).PersistentObjects(持久化对象)和DetachedObjects(托管对象也叫做离线 ...
- 安装sqoop
安装sqoop 1.默认已经安装好java+hadoop 2.下载对应hadoop版本的sqoop版本 3.解压安装包 tar zxvf sqoop-1.4.6.bin__hadoop-2.0.4-a ...
- 动画效果interpolator
Interpolator 被用来修饰动画效果,定义动画的变化率 AccelerateDecelerateInterpolator 在动画开始与结束的地方速率改变比较慢,在中间的时候加速 Accel ...
- MIT 6.828 JOS学习笔记15. Lab 2.1
Lab 2: Memory Management lab2中多出来的几个文件: inc/memlayout.h kern/pmap.c kern/pmap.h kern/kclock.h kern/k ...
- JDBC入门之一--连接Mysql实验
工具:mysql-connector-java-5.1.40.eclipse 1)首先要将mysql-connector-java包整合到eclipse中,右击项目,然后选择build path,出现 ...
- D3数据绑定
这里转载一个非常经典的关于D3数据绑定的帖子,由D3作者自己写的,非常棒,以至于我忍不住全文copy到此. 原文地址 Thinking with Joins Say you’re making a b ...
- python 爬虫(四)
爬遍整个网络 1 当我们访问整个网络的时候,我们不可避免的会访问不同的网站,但是不同的网站会有完全不同的结构和内容... 现在一步一步的构建访问整个网络的脚本 I 从一个网站开始,每一次都爬向不同的网 ...