- 7.3 SQLite创建数据库分析
- include<unistd.h>
- include<sqlite3.h>//包含SQLite API头文件,这里的3是SQLite的版本号
- include<stdlib.h>
- define LOG_TAG"SQLITE_TEST"//定义该进程logcat输出的标签
- include<utils/Log.h>
- ifndef NULL
- define NULL(0)
- endif
- define DB_PATH"/mnt/sdcard/sqlite3test.db"
- define CHECK_DB_ERR(value, expectValue)\
- define TABLE_PERSONAL_INFO\
- define INSERT_PERSONAL_INFO\
7.3 SQLite创建数据库分析
作为Android多媒体系统中媒体信息的仓库,MediaProvider使用了SQLite数据库来管理系统中多媒体相关的数据信息。作为第二条分析路线,本节的目标是分析MediaProvider如何利用SQLite创建数据库,同时还将介绍和SQLite相关的一些知识点。
下面先来看大名鼎鼎的SQLite及Java层的SQLiteDatabase家族。
7.3.1 SQLite及SQLiteDatabase家族
1.SQLite轻装上阵
SQLite是一个轻量级的数据库,它和笔者之前接触的SQLServer或Oracle DB比起来,犹如蚂蚁和大象的区别。它“轻”到什么程度呢?笔者总结了SQLite具有的两个特点:
从代码上看,SQLite所有的功能都实现在Sqlite3.c中,而头文件Sqlite3.h定义了它所支持的API。其中,Sqlite3.c文件包含12万行左右的代码,相当于一个中等偏小规模的程序。
从使用者角度的来说,SQLite编译完成后将生成一个libsqlite.so,大小仅为300KB左右。SQLite确实够轻,但这个“轻”还不是本节标题“轻装上阵”一词中的“轻”。为什么?此处先向读者们介绍一个直接使用SQLite API开发的Android native程序示例,该示例最终编译成的二进制可执行程序名为SqliteTest。
注意 本书后文所说的SQLite API特指libsqlite.so提供的Native层的API。
使用SQLite API开发的Android native程序示例的代码如下:
[—>SqliteTest.cpp:libsqlite示例]
include<unistd.h>
include<sqlite3.h>//包含SQLite API头文件,这里的3是SQLite的版本号
include<stdlib.h>
define LOG_TAG"SQLITE_TEST"//定义该进程logcat输出的标签
include<utils/Log.h>
ifndef NULL
define NULL(0)
endif
//声明数据库文件的路径
define DB_PATH"/mnt/sdcard/sqlite3test.db"
/*
声明一个全局的SQLite句柄,开发者无须了解该数据结构的具体内容,只要知道它代表了使用者
和数据库的一种连接关系即可。以后凡是针对特定数据库的操作,都需要传入对应的SQLite句柄
*/
static sqlite3*g_pDBHandle=NULL;
/*
定义一个宏,用于检测SQLite API调用的返回值,如果value不等于expectValue,则打印警告,
并退出程序。注意,进程退出后,系统会自动回收分配的内存资源。对如此简单的例子来说,读者大可
不必苛责。其中,sqlite3_errmsg函数用于打印错误信息
*/
define CHECK_DB_ERR(value, expectValue)\
do\
{\
if(value!=expectValue)\
{\
LOGE("Sqlite db fail:%s",g_pDBHandle==NULL?"db not\
connected":sqlite3_errmsg(g_pDBHandle));\
exit(0);\
}\
}while(0)
int main(int argc, char*argv[])
{
LOGD("Delete old DB file");
unlink(DB_PATH);//先删除旧的数据库文件
LOGD("Create new DB file");
/*
调用sqlite3_open创建一个数据库,并将和该数据库的连接环境保存在全局的SQLite句柄
g_pDBHandle中,以后操作g_pDBHandle就是操作DB_PATH对应的数据库
*/
int ret=sqlite3_open(DB_PATH,&g_pDBHandle);
CHECK_DB_ERR(ret, SQLITE_OK);
LOGD("Create Table personal_info:");/*
定义宏TABLE_PERSONAL_INFO,用于描述为本例数据库建立一个表所用的SQL语句,
不熟悉SQL语句的读者可先学习相关知识。从该宏的定义可知,将建立一个名为
personal_info的表,该表有4列,第一列是主键,类型是整型(SQLite中为INTEGER),
每加入一行数据该值会自动递增;第二列名为name,数据类型是字符串(SQLite中为TEXT);
第三列名为age,数据类型是整型;第四列名为sex,数据类型是字符串
*/
define TABLE_PERSONAL_INFO\
"CREATE TABLE personal_info"\
"(ID INTEGER primary key autoincrement,"\
"name TEXT,"\
"age INTEGER,"\
"sex TEXT"\
")"
//打印TABLE_PERSONAL_INFO所对应的SQL语句
LOGD("\t%s\n",TABLE_PERSONAL_INFO);
//调用sqlite3_exec执行前面那条SQL语句
ret=sqlite3_exec(g_pDBHandle, TABLE_PERSONAL_INFO, NULL, NULL, NULL);
CHECK_DB_ERR(ret, SQLITE_OK);
/*
定义插入一行数据所使用的SQL语句,注意最后一行中的问号,它表示需要在插入数据
前和具体的值绑定。插入数据库对应的SQL语句是标准的INSERT命令
*/
LOGD("Insert one personal info into personal_info table");
define INSERT_PERSONAL_INFO\
"INSERT INTO personal_info"\
"(name, age, sex)"\
"VALUES"\
"(?,?,?)"//注意这一行语句中的问号
LOGD("\t%s\n",INSERT_PERSONAL_INFO);
//sqlite3_stmt是SQLite中很重要的一个结构体,它代表了一条SQL语句
sqlite3_stmt*pstmt=NULL;
/*
调用sqlite3_prepare初始化pstmt,并将其和INSERT_PERSONAL_INFO绑定。
也就是说,如果执行pstmt,就会执行INSERT_PERSONAL_INFO语句
*/
ret=sqlite3_prepare(g_pDBHandle, INSERT_PERSONAL_INFO,-1,&pstmt, NULL);
CHECK_DB_ERR(ret, SQLITE_OK);
/*
调用sqlite3_bind_xxx为该pstmt中对应的问号绑定具体的值,该函数的第二个参数用于
指定第几个问号
*/
ret=sqlite3_bind_text(pstmt,1,"dengfanping",-1,SQLITE_STATIC);
ret=sqlite3_bind_int(pstmt,2,30);
ret=sqlite3_bind_text(pstmt,3,"male",-1,SQLITE_STATIC);
//调用sqlite3_step执行对应的SQL语句,该函数如果执行成功,我们的personal_info
//表中将添加一条新记录,对应值为(1,dengfanping,30,male)
ret=sqlite3_step(pstmt);
CHECK_DB_ERR(ret, SQLITE_DONE);
//调用sqlite3_finalize销毁该SQL语句
ret=sqlite3_finalize(pstmt);
//下面将从表中查询name为dengfanping的person的age值
LOGD("select dengfanping's age from personal_info table");
pstmt=NULL;
/*
重新初始化该pstmt,并将其和SQL语句“SELECT age FROM personal_info WHERE name
=?”绑定
*/
ret=sqlite3_prepare(g_pDBHandle,"SELECT age FROM personal_info WHERE name
=?",-1,&pstmt, NULL);
CHECK_DB_ERR(ret, SQLITE_OK);
/*
绑定pstmt中第一个问号为字符串“dengfanping”,最终该SQL语句为
SELECT age FROM personal_info WHERE name='dengfanping'
*/
ret=sqlite3_bind_text(pstmt,1,"dengfanping",-1,SQLITE_STATIC);
CHECK_DB_ERR(ret, SQLITE_OK);
//执行这条查询语句
while(true)//在一个循环中遍历结果集
{
ret=sqlite3_step(pstmt);
if(ret==SQLITE_ROW)
{
//从结果集中取出第一列(由于执行SELECT时只选择了age,故最终结果只有一列),
//调用sqlite3_column_int返回结果集的第一列(从0开始)第一行的值
int myage=sqlite3_column_int(pstmt,0);
LOGD("Got dengfanping's age:%d\n",myage);
}
else//如果ret为其他值,则退出循环
break;
}
else LOGD("Find nothing\n");//SELECT执行失败
ret=sqlite3_finalize(pstmt);//销毁pstmt
if(g_pDBHandle)
{
LOGD("Close DB");
//调用sqlite3_close关闭数据库连接并释放g_pDBHandle
sqlite3_close(g_pDBHandle);
g_pDBHandle=NULL;
}
return 0;
}
通过上述示例代码可发现,SQLite API的使用主要集中在以下几点上:创建代表指定数据库的sqlite3实例。
创建代表一条SQL语句的sqlite3_stmt实例,在使用过程中首先要调用sqlite3_prepare将其和一条代表SQL语句的字符串绑定。如该字符串含有通配符“?”,后续就需要通过sqlite3_bind_xxx函数为通配符绑定特定的值以生成一条完整的SQL语句。最终调用sqlite3_step执行这条语句。
如果是查询(即SELECT命令)命令,则需调用sqlite3_step函数遍历结果集,并通过sqlite3_column_xx等函数取出结果集中某一行指定列的值。
最后需调用sqlite3_finalize和sqlite3_close来释放sqlite3_stmt实例及sqlite3实例所占据的资源。
SQLite API的使用非常简单明了。不过很可惜,这份简单明了所带来的快捷,只供那些Native层程序开发者独享。对于Java程序员,他们只能使用Android在SQLite API之上所封装的SQLiteDatabase家族提供的类和相关API。综合考虑到架构及系统的稳定性和可扩展性等各种情况,Android在SQLite API之上进行了面向对象的封装和解耦等设计,最终呈现在大家面前的是一个庞大而复杂的SQLiteDatabase家族,其成员有61个之多(参阅frameworks/base/core/java/android/database目录中的文件)。
现在读者应该理解本节标题“SQLite轻装上阵”中“轻”的真正含义了。在后续的分析过程中,我们主要和SQLiteDatabase家族打交道。随着分析的深入,读者能逐渐见认识SQLiteDatabase的“厚重”。
2.SQLiteDatabase家族介绍
图7-4展示了SQLiteDatabase家族中的几位重要成员。
图 7-4 SQLitDatabase家族部分成员
图7-4中相关类的说明如下:
SQLiteOpenHelper是一个帮助(Utility)类,用于方便开发者创建和管理数据库。
SQLiteQueryBuilder是一个帮助类,用于帮助开发者创建SQL语句。
SQLiteDatabase代表SQLite数据库,它内部封装了一个Native层的sqlite3实例。
Android提供了3个类,即SQLiteProgram、SQLiteQuery和SQLiteStatement用于描述和SQL语句相关的信息。从图7-4可知,SQLiteProgram是基类,它提供了一些API用于参数绑定。SQLiteQuery主要用于query查询操作,而SQLiteStatement用于query之外的一些操作(根据SDK的说明,如果SQLiteStatement用于query查询,其返回的结果集只能是1行×1列的)。注意,在这3个类中,基类SQLiteProgram将保存一个指向Native层的sqlite3_stmt实例的变量,但是这个成员变量的赋值却和另外一个对开发者隐藏的类SQLiteCompiledSql有关。从这个角度看,可以认为Native层sqlite3_stmt实例的封装是由SQLiteCompiledSql完成的。这方面的知识在后文进行分析时即能见到。
SQLiteClosable用于控制SQLiteDatabase家族中一些类的实例的生命周期,例如SQLiteDatabase实例和SQLiteQuery实例。每次使用这些实例对象前都需要调用acquireReference以增加引用计数,使用完毕后都需要调用releaseReferenece以减少引用计数。
提示 读者见识了SQLiteDatabase家族中这几位成员后有何感想?是否觉得要真正搞清楚它们还需要花费一番工夫呢?
下面来看MediaProvider是如何使用SQLiteDatabase的,重点关注SQLite数据库是如何创建的。