实例说明在Cocos2d-x 3.x中使用SQLite

发表于2015-11-14
评论0 980浏览

一、说明

我将用实例与扩充解释的方式具体说明如何创建SQLite数据库、建表,以及在Cocos2d-x中使用SQLite读取现有数据库。

因为网上的大多教程只是通过Cocos2d-x创建表,然后操作数据库。如何手动建表,读取现有的表却很少提。把自己的经验写出来,减少新手采坑的机会。

项目有个需求:列表显示祝福语类别,点击一个类别,列表分页显示祝福语。由于有几百条祝福语,直接全部写到内存中太傻了。分几个文件,读文件方式又麻烦。对于这样的要求,用sqlite是最适合不过了。一是轻量级,二是方便简单。

下面是要做到的效果图:

1.列表显示类别:

01.jpg

2.列表分页显示祝福语:

02.jpg

3.给的祝福语文档:

03.jpg

格式很乱。做的时候想扁人。


二、准备:

看到这个需求,首先要设计好数据库表。

1.下载配置sqlite3环境

由于我使用的是Mac OSX,sqlite3已经集成到Mac环境了。直接打开终端,输入sqlite3就进入到sqlite3命令行了。

至于windows系统,可以上SQLite官网下载windows的二进制(也就是.exe)文件。在系统环境变量中配置好路径,就可以在任意路径下的cmd窗口使用sqite3了。

2.创建数据库

SQLite环境配置好了。

打开终端输入:

sqlite3

进入sqlite3命令行。

输入:

.version

查看sqlite3版本,我的当前版本是:

SQLite 3.8.5 2014-08-15

现在官网已经升级到3.8.8了。

注意上面的.version命令,前面有个点。这个是sqlite3的点命令中的一个。

打开终端,输入:

sqlite3 phrases.db

就创建好了一个数据库,名字叫phrases.db。(后缀不一定要是.db,什么都行,写你自己名字都行)并且自动进入sqlite3命令行了(.quit命令可以退出sqlite3命令行)。后面我们就可以在里面建立不同的表了。

3.建表

命令行下操作表很麻烦,要写很长的SQL语句,还容易写错。对于我这种SQL菜鸟,灵机一动,叮~~~ 下载个SQLite可视化编辑工具不就得了。

于是我搜到了一个:

SQLiteManager,链接地址:http://www.sqlabs.com/sqlitemanager.php,下载对应版本。

不过这个软件有限制,如果不购买,你只能用它看到前20行的数据,不过没关系,我们只用它来建表。(偷笑大笑)。土豪表鄙视我。

安装SQLiteManager,然后右键用SQLiteManager打开之前创建的数据库phrases.db,界面很简洁,很好用。

首先,我要建立2张表,一张用来保存祝福短语的类别名称,一张用来保存每个类别的祝福短语。(从这个表述中你也知道,这两张表是有关联的。)

a.先建立类别表:

04.jpg

3个字段:

cid代表类别ID,并且把它作为主键,主键都是非空,自动增长和唯一的。

name为类别名

count记录了对应祝福语类别中祝福语的条数。

其实sqlite表还有个隐含字段,叫rowid。如果没有手动设置主键,则rowid是默认主键。之所以不用 ,是因为我接下来要建立的表中要通过外键连接这张表的cid字段,以便填充count字段的值。更多了解rowid(http://unmi.cc/sqlite-primary-rowid-autoincrement/)

b.建立祝福短语表:

05.jpg

两个字段:

cid为 类别ID,对应对的类别表中的cid,并且把cid作为外键连接到类别表,on Delete和on Update都选择CASCADE.    Options选择带有NOT DEFERRABLE的都可以,表示不等待,也就是一更改被约束的外键就执行on Delete和onUpdate动作,而不是等待Commit再执行。

ON DELETE 和 ON UPDATE行为

外键的ON DELETE和 ON  UPDATE从句, 可以用来配置  当从父表中删除 某些行时发生的行为(ON DELETE).  或者 修改存在的行的父键的值, 发生的行为(ON UPDATE)

单个外键约束可以为ON DELETE和ON UPDATE配置不同的行为.   外键行为在很多时候类似于 触发器(trigger)

ON DELETE和ON UPDATE的行为是 NO ACTION,  RESTRICT, SET NULL,  SET DEFAULT 或者 CASCADE

如果没有明确指定星闻,那么默认就是NO ACTION

NO ACTION: 当父键被修改或者删除时, 没有特别的行为发生

RESTRICT:  存在一个或者多个子键对应于相应的父键时,  应用程序禁止删除(ON DELETE RESTRICT)或者修改(ON UPDATE RESTRICT) 父键

RESTRICT与普通的外键约束的区别是,  当字段(field)更新时, RESTRICT行为立即发生

SET NULL: 父键被删除(ON DELETE SET NULL) 或者修改 (ON UPDATE SET NULL)

SET DEFAULT: 类似于SET NULL

CASCADE: 将实施在父键上的删除或者更新操作,传播给与之关联的子键.

对于 ON DELETE CASCADE, 同被删除的父表中的行 相关联的子表中的每1行,也会被删除.

对于ON UPDATE CASCADE,  存储在子表中的每1行,对应的字段的值会被自动修改成同新的父键匹配

context 为祝福短语字段,记录的是短语内容。

好了,字段和外键约束都做好了。

4.填充数据吧!

tb_category表的数据比较少,直接上图:

06.jpg

写博客前,由于我的tb_context表的数据已经填好了,所以count字段值都填充好了。

下面该填充tb_context表了。

为了让tb_category表中的count字段能统计出tb_context表中不同类别的祝福短语条数,我们还要建立两个触发器(关于触发器的介绍:官方文档W3C文档),用于在tb_context表中插入和删除行时,触发tb_category表中count字段数据的更新。一个是插入触发器count_insert,一个是删除触发器count_delete

第一种写法:每进行一行数据的插入和删除,对应类别的count值增减1。

1
2
CREATE TRIGGER count_insert AFTER INSERT ON tb_context FOR EACH ROW BEGIN UPDATE tb_category SET count = count+1 WHERE new.cid=old.cid; end;  
CREATE TRIGGER count_delete AFTER DELETE ON tb_context FOR EACH ROW BEGIN UPDATE  tb_category SET count = count-1 WHERE cid=old.cid; end;

第二种写法:每进行一行数据的插入和删除,用【COUNT(*)函数】重新统计对应类别的count值。

1
2
CREATE TRIGGER count_insert AFTER INSERT ON tb_context FOR EACH ROW BEGIN UPDATE  tb_category SET count = (SELECT COUNT(*) FROM tb_context where cid=new.cid) WHERE cid = new.cid;end;  
CREATE TRIGGER count_delete AFTER DELETE ON tb_context FOR EACH ROW BEGIN UPDATE  tb_category SET count = (SELECT COUNT(*) FROM tb_context WHERE cid=old.cid) WHERE cid = old.cid;end;

上面两种写法都可以达到要求,不同之处在于:

第一种直接增加count值,要保证tb_context插入和删除操作之前,tb_category中的初始值与tb_context中对应类别的祝福语条数相同,不然统计的count值不对。

第二种每次插入和删除,都重写COUNT一遍对应类别的祝福语条数,不需要保证初始值是对的。但是COUNT函数查询统计需要更多消耗。(其实我的数据少,可以忽略不计哈。)

终于回到正题,填充数据tb_context的数据了。

对于策划那边给我的那个格式凌乱的祝福短语文档,我无力吐槽。

好吧,先建个excl表格,把这祝福短语文档中的内容一股脑拷贝到excl中去。

然后填写好对应的类别id,把格式调对。

效果如图:(中间删丢了一个,害我花了我一个小时)。

07.jpg

注意,没有表头。

sqlite导入数据最喜欢这种格式。

好了,下面导出到CSV格式:

这个很简单,不多说,另存为.csv就可以了。

用文本编辑器打开:

08.jpg

看,哥们儿字段是都是逗号分隔的。

利用sqlite3的.import命令将数据从文件导入到表中。

在执行.import之前需要用.separator命令设置数据的分隔符逗号,否者默认的分割符号是竖线'|'。

打开终端,输入:

1
sqlite3 phrases.db

进入sqlite3命令行后,输入:

1
2
 .separator ','
.import contex.csv tb_context

OK, 大功告成。

用SQLiteManager打开数据库看下,是不是都进去了。

09.jpg

不付费只能看到前20条数据。

顺便看看tb_category中count字段值对不对。(如果你用的是第一种触发器的写法,更要注意下。)

现在你可以试试在tb_context中插入几条数据,删除几条数据,tb_category中count字段的值会不会跟着增减。再修改下tb_context的cid为tb_category表中不存在cid值,或者删除tb_category表中的一个类别,看看外键有没有起作用。(记得别删除有用的数据,免得找不回来)

SQLiteManager的价值我们已经用完了。你可以卸载它了。


三、在Cocos2d-x 3.x中读取现有sqlite数据库

1.配置cocos2d-x3.x sqlite数据库环境。

IOS/Mac : 系统库中自带了sqlite3,只要在xCode Linked Frameworks and Libraries中添加libsqlite3.0.dylib就可以了。

10.jpg

Android: 在 SQLite官网下载sqlite3的源代码,并拷贝到工程目录下(如Class目录),在xCode中添加

11.jpg

修改Android.mk文件

12.jpg

Windows :  Cocos2d-x 3.x中已经集成好了windows相关平台下的动态库了。

在 cocos/../external/sqlite3目录下。使用时在VS中包含头文件和动态库就可以了。

另外发现cocos/../external/sqlite3目录下有头文件,和Android.mk文件,但是没有实现文件,Android.mk文件也是空的,我们可以把下载的sqlite3.c做成一个外部模块。然后再我们自己的Android.mk文件中导入这个外部模块就可以用了。具体怎么做还请百度谷歌。

2.读取数据库

之前上网查找相关文档,大部分都是在cocos2d-x工程中创建数据库,再保存数据。

但当我想读取现有数据库时,还是遇到了一点小坑。至于什么坑,慢慢说。

现在把我创建好的phrases.db数据库拷贝到我的cocos2d-x项目的Resources/db目录下。

由于公司自己封装了sqlite3的操作类,而且强烈要求我必须用公司封装好的,便于维护。好吧,我先用着。接下来我会解释清楚这些代码的。

终于到写代码的时候了!!!!

首先,我们封装了一个静态函数:resetAvailableDbPath()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
std::string CbCCUtility::resetAvailableDbPath(std::string dbName)
{
    // 用于保存数据库的路径
    std::string availableDbPath;
    if (/*CbCCUtility::isAndroid()*/true)
    {
        // 获取系统可读写路径加上数据库名,保存在availableDbPath中
        string fullDbPath = CCFileUtils::getInstance()->fullPathForFilename(dbName.c_str());
        availableDbPath = CCFileUtils::getInstance()->getWritablePath() + dbName;
        // 只读方式打开可读写路径下的phrases,由于第一次启动时,phrases.db数据库并不在改路径下,就会打开失败,fp=nullptr
        FILE* fp =fopen(availableDbPath.c_str(), "r");
        if(fp == nullptr)
        {
            // // 如果不存在,则将Resources/db/phrases.db拷贝到可读写路径下。
            ssize_t dbFileSize = 0;
            unsigned char *dbData = CCFileUtils::getInstance()->getFileData(dbName.c_str(), "rb", &dbFileSize);
            fp = fopen(availableDbPath.c_str(), "wb");
            if (fp != nullptr)
            {
                fwrite(dbData, dbFileSize, 1, fp);
                // 这里要注意释放dbData,dbData指向了getFileData中malloc出来的内存,并没有释放。
                CC_SAFE_FREE(dbData);
                fclose(fp);
            }
        }
        else
        {
            fclose(fp);
        }
    }
    // 无视之
    else
    {
        string fullDbPath = CCFileUtils::getInstance()->fullPathForFilename(dbName.c_str());
        availableDbPath = fullDbPath;
    }
    return availableDbPath;
}
1
// 这里我就踩了两个坑,一个大坑,一个小坑。

大坑:

安卓和ios真机无法读取资源目录下的phrases.db文件,必须拷贝到可读写路径。而xCode模拟器可以直接通过全路径读写resource目录下的.db文件。让我迷惑了好久。

小坑:

调用

1
CCFileUtils::getInstance()->getFileData();

要注意释放返回的指针指向的内存。
在程序启动后,我们先调用这个函数,把数据库文件路径配置好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool AppCommon::appInit()  
{  
    CCLOG("AppCommon::appInit() --- begin");  
       
    // reset search paths.  
    {  
        vector<string> arraySearchPaths = FileUtils::getInstance()->getSearchPaths();  
        arraySearchPaths.push_back("app_android");  
        arraySearchPaths.push_back("app_ios");  
        arraySearchPaths.push_back("app_publish");  
        arraySearchPaths.push_back("db");  
        arraySearchPaths.push_back("image/common");  
        arraySearchPaths.push_back("image/home");  
        arraySearchPaths.push_back("image/voice");  
        arraySearchPaths.push_back("image/greet/list");  
        arraySearchPaths.push_back("image/greet/phrase");  
        arraySearchPaths.push_back("image/wm_sending");  
        arraySearchPaths.push_back("sound/effect");  
        CbCCUtility::resetSearchPaths(arraySearchPaths);  
    }  
       
    // 资源路径设置完后,重置数据库文件路径  
    CbCCUtility::resetAvailableDbPath("phrases.db");  
   
    CCLOG("AppCommon::appInit() --- end");  
    return true;  
}

再写一个静态函数,用于返回可用数据库的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::string CbCCUtility::getAvailableDbPath(std::string dbName)  
{  
    std::string availableDbPath;  
    if (CbCCUtility::isAndroid())  
    {  
        string fullDbPath = CCFileUtils::getInstance()->fullPathForFilename(dbName.c_str());  
        availableDbPath = CCFileUtils::getInstance()->getWritablePath() + dbName;  
    }  
    else  
    {  
        string fullDbPath = CCFileUtils::getInstance()->fullPathForFilename(dbName.c_str());  
        availableDbPath = fullDbPath;  
    }  
    return availableDbPath;  
}

现在,我们要打开数据库,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::string dbpath = CbCCUtility::getAvailableDbPath("phrases.db");  
// 初始化数据库指针  
sqlite3* pDB = nullptr;  
// 调用sqlite_open()函数,打开数据库  
int ret = sqlite3_open(dbpath.c_str(), &_pDB);  
// 打开失败  
if(ret != SQLITE_OK)  
{  
    return;  
}  
// 操作数据库  
//  ...  
   
// 关闭数据库  
sqlite3_close(pDB);  
pDB = nullptr;

我的读取tb_category表是在GreetingWordsScene类中。只保留有用代码。

1
2
3
4
5
6
7
8
9
10
11
12
// GreetingWordsScene.h
class GreetingWordsScene : public Layer  
{  public:  
    virtual bool init() override;  
    CREATE_FUNC(GreetingWordsScene)  
    static Scene* createScene();  private:
    // 从tb_category中读取类别数据  
    bool getCategoryFromTable();  private:  
    // 用于保存类别数据  
    std::vector<std::string> _categoryName;  
};  
// GreetingWordsScene.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Scene* GreetingWordsScene::createScene()  
{  
    Scene* pScene = Scene::create();  
    GreetingWordsScene* pLayer = GreetingWordsScene::create();  
    pScene->addChild(pLayer);  
    return pScene;  
}  
   
bool GreetingWordsScene::init()  
{  
    if(!Layer::init())  
    {  
        return false;  
    }  
       
    // 重点在这里  
    clock_t t1 = clock();  
    // 从数据库表中获取短语类别列表  
    if(!getCategoryFromTable())  
    {  
        return true;  
    }  
    clock_t t2 = clock();  
    log("数据库读取时间为%ld, t1 = %ld, t2 = %ld", t2 - t1, t1, t2);  
       
   
    return true;  
}  
   
bool GreetingWordsScene::getCategoryFromTable()  
{  
    // 公司封装的sqlite3操作库  
    CbSqlite3* cbsqlite = CbSqlite3::shared();  
    std::string dbpath = CbCCUtility::getAvailableDbPath("phrases.db");  
       
    // 封装的函数,打开数据库  
    int ret = cbsqlite->openDB(dbpath);  
    if(ret != SQLITE_OK)  
    {  
        return false;  
    }  
    // 启用外键支持  
//    sqlite3_exec(_pDB,"PRAGMA foreign_keys = ON",0,0,&err_msg);  
   
    // 查询tb_category表中name字段,sql语句后面不要有分号  
    std::string sql = "SELECT NAME FROM TB_CATEGORY";  
    // 公司封装的数据结构,是std::vector<CbSqlite3ResultRow>的子类,用于保存CbSqlite3ResultRow类型的多个键值对。  
    CbSqlite3Results results;  
    // 公司封装的查询函数  
    cbsqlite->queryData(sql, results);  
    // CbSqlite3ResultRow是公司封装的数据结构,是std::map<std::string, std::string>的子类。保存字段和字段值的键值对。  
    // 遍历CbSqlite3Results数组,取出字段值。数组中存的是CbSqlite3ResultRow对象,CbSqlite3ResultRow存的时字段和字段值的键值对。  
    for(CbSqlite3ResultRow& row : results)  
    {  
        // 根据tb_categoryz中的字段名name(row的键)获取name字段对应的值(row的值)。  
        _categoryName.push_back(row.valueString("name"));  
        // 打印得到的字段值  
        log("%s", _categoryName[_categoryName.size()-1].c_str());  
    }  
    // 封装的函数,关闭数据库  
    cbsqlite->closeDB();  
       
    return true;  
}

上面用了很多公司封装好的读取sqlite数据库的函数。我说过,我会一个一个慢慢解释清楚的。

3.代码解释

先看CbSqlite3的头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CbSqlite3  
{  
public:  
    static CbSqlite3 *shared();  
   
    CbSqlite3();  
    ~CbSqlite3();  
       
public:  
       
    int openDB(std::string db); // if not exist, try to create one.  
    int closeDB();  
    int isTableExist(std::string tb, bool &isExist);  
    int createTable(std::string sql);  
    int deleteTable(std::string sql);  
    int insertData(std::string sql);  
    int updateData(std::string sql);  
    int queryDataCount(std::string sql, int &count);  
    int queryData(std::string sql, CbSqlite3Results &results);  
       
private:  
    sqlite3 *_pDB;  
};

解释代码..........

a.

1
2
3
// 公司封装的sqlite3操作库  
    CbSqlite3* cbsqlite = CbSqlite3::shared();  
// 解释:获取单例的CbSqlite3对象。
1
2
3
4
5
6
7
8
9
static CbSqlite3 *s_instance = NULL;  
CbSqlite3 *CbSqlite3::shared()  
{  
    if (s_instance == NULL)  
    {  
        s_instance = new CbSqlite3();  
    }  
    return s_instance;  
}


b.

1
2
3
4
5
6
7
// 封装的函数,打开数据库  
    int ret = cbsqlite->openDB(dbpath);  
    if(ret != SQLITE_OK)  
    {  
        return false;  
    }  
//   解释:很简单,直接调用sqlite3_open()函数,打开数据库
1
2
3
4
5
int CbSqlite3::openDB(std::string db)  
{  
    int ret = sqlite3_open(db.c_str(), &_pDB);  
    return ret;  
}


c.

1
2
3
4
5
// 查询tb_category表中name字段,sql语句后面不要有分号  
    std::string sql = "SELECT NAME FROM TB_CATEGORY";  
    // 公司封装的数据结构,是std::vector<CbSqlite3ResultRow>的子类,用于保存CbSqlite3ResultRow类型的多个键值对。  
    CbSqlite3Results results;  
//  解释:看看CbSqlite3Results的数据结构

继承自std::vector<CbSqlite3ResultRow>,说明CbSqlite3Results是一个保存CbSqlite3ResultRow类型数据的数组(vector)容器

1
2
3
4
5
class CbSqlite3Results: public std::vector<CbSqlite3ResultRow>  
{  
public:  
    std::string _sql;  
};

那CbSqlite3ResultRow又是什么呢?

再看:

说明CbSqlite3ResultRow是一个键值对都是string类型的map容器

1
2
3
4
5
6
7
8
9
10
11
12
class CbSqlite3ResultRow: public std::map<std::string, std::string>  
{  
public:  
    // 获取字段对应的整型值,10进制  
    int valueInteger(std::string field);  
    // 获取字段对应的整型值,16进制  
    int valueXInteger(std::string field);  
    // 获取字段对应的字符串  
    std::string valueString(std::string field);  
    // 获取字段对应的浮点数  
    float valueFloat(std::string field);  
};

该map容器用于存取数据库中读到的每一行的多个 [字段,值] 的键值对。(这里不明白没关系,后面还会解释。)

里面封装的函数只是根据字段名从map中取出对应的字段值

d.

1
2
// 公司封装的查询函数  
    cbsqlite->queryData(sql, results);

解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// results是用户传入的数据的地址  
int CbSqlite3::queryData(std::string sql, CbSqlite3Results &results)  
{  
    int ret = 0;  
    // 数据库操作指针为空,返回sqlite错误码  
    if (_pDB == NULL)  
    {  
        return SQLITE_ERROR;  
    }  
    // 保存sql语句  
    results._sql = sql;  
    // 清空数组  
    results.empty();  
    // 保存查询出错时的出错信息  
    char * err_msg = NULL;  
    // loadQueryData是回调函数,每查询到一行数据,就会调用一次该回调函数。&results对应loadQueryData函数中的para,  
    ret = sqlite3_exec(_pDB, sql.c_str(), loadQueryData, &results, &err_msg);  
    return ret;  
}
1
// 回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/ column_count是查询出的符合条件的一行数据的列数,array_column_names是字段名的数组,该数组存的是字段名,有几列,就有几个字段名元素。array_column_values是字段值的数组,改数组存的是字段值,有几列,就由几个字段值元素。该数组的下标与array_column_names下标一一对应。  
static int loadQueryData(void * para, int column_count, char ** array_column_values, char ** array_column_names)  
{  
    // 将自己的变量指针强制类型转换  
    CbSqlite3Results &results = *(CbSqlite3Results *)para;  
    声明一个std::map<std::string, std::string>类型的变量row,用于保存查询到的结果,字段名作key,字段值作value  
    CbSqlite3ResultRow row;  
    for (int i = 0; i < column_count; i++)  
    {  
        // 以array_column_name作为key, array_column_values作为值,把查询到的数据存入变量名位row的map中。  
        row[array_column_names[i]] = array_column_values[i];  
    }  
    // 每得到一个键值对map,就把改map存到results中,results是自己提供的变量空间。这样,我们就可以获得所有查询结果了。  
    results.push_back(row);  
    return SQLITE_OK;  
}

重点在sqlite_exec()函数和它的回调参数sqlite3_callback这里,也就是上面对应的loadQueryData函数指针。

1)先说sqlite3_exec()函数:

原型:int sqlite3_exec(sqlite3*, const char *sql, sqlite3_callback, void *,  char **errmsg ); 

参数1:sqlite3_open()函数的第二个传出参数,也就是打开数据库得到的数据库指针。

参数2:要执行的sql语句。我的程序中执行的是    "SELECT NAME FROM TB_CATEGORY"语句,从tb_category表中查询name字段。

参数3:sqlite3_callback

函数指针:typedef int (*sqlite3_callback)(void*,int,char**, char**);

如果该函数指针不为NULL,那么每查询到一行符合查询条件的数据,就会调用一次这个函数。进行INSERT、DELETE、UPDATE操作时,一般回调都填NULL。因为我们并不需要返回什么查询结果。等下解释这个函数。

参数4:你自定义的一个变量的地址,你把该地址传进去,sqlite3_exec()函数会把这个自定义变量地址传给回调函数的第一个参数,也是void*接收改地址,你可以用这个指针做任何事,比如,保存回调函数第二个、第三个参数返回的数据信息。如果不想使用这个自定义变量地址,可以传一个NULL。

参数5:传入一个指向字符串的指针地址。也就是指针的指针。当sql语句查询失败时,sqlite3_exec函数把你传入的这个指针指向错误信息所在的首地址,那么你就可以通过errmsg 打印这个错误信息,以便于了解出了什么错。

2)重点解释回调函数:

1
typedef int (*sqlite3_callback)(void*,int,char**, char**);

参数1:用户自定义变量的地址,一般用传出第二、三个参数的信息。使用的时候,强制类型转换为你需要的类型。

参数2:查询出的符合条件的行数据的字段值的数组。

参数3:查询出的符合条件的行条数据的字段名的数组。

注意,我这里说的“查询出的符合条件的一行数据”并不一定是表中的一整行数据,也由可能是新的结果集中的一行数据。

举例来说,对于下面一条查询语句:

1
SELECT name ,cid FORM TB_CATEGORY;

这条语句的结果集是下面的表结构:

1
2
3
4
5
|    name   |  cid  |
|  励志短语  |   1   |
|  生日祝福  |   2   |
        ..........
|  开业祝福  |   8   |

并不包含count字段,因为我在SELECT语句中并没有查询count字段,查询出符合条件的第一条数据时,调用一次回调函数,第二个参数的数组结构便是:{"励志短语", “1”};

,第三个参数的数组结构是{"name", "cid"};

查询到符合条件的第二条数据是,又调用一次回调函数,此时,第二个参数的数组结构是{“生日祝福”, "2"};第三个参数的数组结构是{"name", "cid"};

以此类推,直到查询到8条,回调8次。

再比如:

这一条sql语句

1
"SELECT COUNT(*) FROM tb_category"

得到的结果表表结构是

1
2
| COUNT(*)|
|  8    |

只返回一条数据,并且字段名也只有一个(COUNT(*)),字段值为8,那么回调函数的第二个参数的数组结构便是{"8"};第三个参数的数组结构是{"COUNT(*)"};

 而对于下面的sql语句:

1
"SELECT COUNT(*) FROM AS cout1 tb_category"

结果表结构变成了

1
2
|  cout1   |
|   8    |

也就是字段COUT(*)名变成了AS后面的别名。那么回调函数的第二个参数的数组结构便是{"8"};第三个参数的数组结构是{"count1"};

那么,我在函数中用一个std::map<std::string,std::string>保存一条结果的键值对,存到std::vector<CbSqlite3ResultRow>类型的数组中,每次回调都会存入数条数据到resluts中。

这样results就保存了所有的查询出的键值对了。

e.

解释:这个循环就是从results数组中取出查询到的值,遍历results,每个元素都是CbSqlite3ResultRow对象。通过key(字段名),取出值(字段值)。

1
2
3
4
5
6
7
for(CbSqlite3ResultRow& row : results)  
    {  
        // 根据tb_categoryz中的字段名name(row的键)获取name字段对应的值(row的值)。  
        _categoryName.push_back(row.valueString("name"));  
        // 打印得到的字段值  
        log("%s", _categoryName[_categoryName.size()-1].c_str());  
    }

f.

解释:最后关闭数据库

1
2
// 封装的函数,关闭数据库  
    cbsqlite->closeDB();

实现:

1
2
3
4
5
6
7
8
9
int CbSqlite3::closeDB()  
{  
    if (_pDB != NULL)  
    {  
        sqlite3_close(_pDB);  
        _pDB = NULL;  
    }  
    return SQLITE_OK;  
}

调用了sqlite_close函数。

1
sqlite3_exec(_pDB,"PRAGMA foreign_keys = ON",0,0,&err_msg);

这一句被我注释了,如果要插入删除数据,又要利用外键约束方式插入错误数据,那么用这句打开外键约束。sqlite3外键约束默认是关闭的。

那么上面的代码就解释完了,感觉自己好啰嗦,但是你看懂了吗?估计看完也要很大的耐心吧!吐~~~

哦,还没完。


四、后话:

顺便提一下:

sqllite3除了sqlte3_exec这个方法查询外,还提供了sqlite3_get_table函数来进行select操作。这个方法不用回调。

函数原型:

1
int sqlite3_get_table(sqlite3*, const char *sql, char ***resultp, int *nrow, int *ncolumn, char **errmsg );

看到第三个参数没有,三星级。

不过不要怕,其实它只是char**的地址罢了。而这个char**是你定义的字符串数组指针。

解释:

参数1:数据库指针。

参数2:sql语句

参数3:查询结果,用字符串数组保存。

参数4:查询到的结果集的行数。

参数5:结果集的字段数。

参数6:错误信息地址。

1
// 下面是一个例子。

感谢董淳光提供。他的博客原地址我找不到了,从别人转载的帖子看到的。(转帖地址)

这个例子看完应该就清楚了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int main( int , char ** )  
{  
   sqlite3 * db;  
   int result;  
   char * errmsg = NULL;  
   char **dbResult; //是 char ** 类型,两个*号  
   int nRow, nColumn;  
   int i , j;  
   int index;  
    
   result = sqlite3_open( “c://Dcg_database.db”, &db );  
   if( result != SQLITE_OK )  
   {  
        //数据库打开失败  
        return -1;  
   }  
    
   //数据库操作代码  
   //假设前面已经创建了 MyTable_1 表  
   //开始查询,传入的 dbResult 已经是 char **,这里又加了一个 & 取地址符,传递进去的就成了 char ***  
   result = sqlite3_get_table( db, “select * from MyTable_1”, &dbResult, &nRow, &nColumn, &errmsg );  
   if( SQLITE_OK == result )  
   {  
        //查询成功  
        index = nColumn; //前面说过 dbResult 前面第一行数据是字段名称,从 nColumn 索引开始才是真正的数据  
        printf( “查到%d条记录/n”, nRow );  
    
        for(  i = 0; i < nRow ; i++ )  
        {  
             printf( “第 %d 条记录/n”, i+1 );  
             for( j = 0 ; j < nColumn; j++ )  
             {  
                  printf( “字段名:%s  ?> 字段值:%s/n”,  dbResult[j], dbResult [index] );  
                  ++index; // dbResult 的字段值是连续的,从第0索引到第 nColumn - 1索引都是字段名称,从第 nColumn 索引开始,后面都是字段值,它把一个二维的表(传统的行列表示法)用一个扁平的形式来表示  
             }  
             printf( “-------/n” );  
        }  
   }  
    
   //到这里,不论数据库查询是否成功,都释放 char** 查询结果,使用 sqlite 提供的功能来释放  
   sqlite3_free_table( dbResult );  
    
   //关闭数据库  
   sqlite3_close( db );  
   return 0;  
}

sqlite_exec和sqlite_get_table函数都无法操作二进制数据。

想要操作二进制数据,sqlite3提供了一个辅助的数据类型:sqlite3_stmt * 。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引