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

为什么这一行的代码行删不掉???还有为什么下面插入的图片这么大?怎么改小???搞不定了,随性写几个字在这儿,吐槽一下吧!吐槽中.............


一、说明

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

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


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

下面是要做到的效果图:

1.列表显示类别:



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



3.给的祝福语文档:



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


二、准备:

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

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.先建立类别表:



3个字段:

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

name为类别名

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

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


b.建立祝福短语表:



两个字段:

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表的数据比较少,直接上图:



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


下面该填充tb_context表了。

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

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

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值。

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,把格式调对。

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



注意,没有表头。

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

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

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

用文本编辑器打开:



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

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

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

打开终端,输入:


sqlite3 phrases.db

进入sqlite3命令行后,输入:

.separator ','

.import contex.csv tb_context


OK,大功告成。

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



不付费只能看到前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就可以了。



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



修改Android.mk文件



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()

<p class="p1"></p><p class="p1"><span class="s1">std</span><span class="s2">::</span><span class="s1">string</span><span class="s2"> </span><span class="s3">CbCCUtility</span><span class="s2">::resetAvailableDbPath(</span><span class="s1">std</span><span class="s2">::</span><span class="s1">string</span><span class="s2"> dbname)</span></p><p class="p1"><span class="s2">{</span></p><p class="p2"><span class="s4">  </span><span class="s5">// </span><span class="s2">用于保存数据库的路径</span></p><p class="p1"><span class="s2">  </span><span class="s1">std</span><span class="s2">::</span><span class="s1">string</span><span class="s2"> availableDbPath;</span></p><p class="p3"><span class="s6">  </span><span class="s7">if</span><span class="s6"> (</span><span class="s2">/*CbCCUtility::isAndroid()*/</span><span class="s7">true</span><span class="s6">)</span></p><p class="p1"><span class="s2">  {</span></p><p class="p3"><span class="s6">    </span><span class="s2">// </span><span class="s8">获取系统可读写路径加上数据库名,保存在</span><span class="s2">availableDbPath</span><span class="s8">中</span></p><p class="p1"><span class="s2">    </span><span class="s1">string</span><span class="s2"> fullDbPath = </span><span class="s3">CCFileUtils</span><span class="s2">::</span><span class="s9">getInstance</span><span class="s2">()-></span><span class="s9">fullPathForFilename</span><span class="s2">(dbname.</span><span class="s10">c_str</span><span class="s2">());</span></p><p class="p1"><span class="s2">    availableDbPath = </span><span class="s3">CCFileUtils</span><span class="s2">::</span><span class="s9">getInstance</span><span class="s2">()-></span><span class="s9">getWritablePath</span><span class="s2">() + dbname;</span></p><p class="p2"><span class="s4">    </span><span class="s5">// </span><span class="s2">只读方式打开可读写路径下的</span><span class="s5">phrases</span><span class="s2">,由于第一次启动时,</span><span class="s5">phrases.db</span><span class="s2">数据库并不在改路径下,就会打开失败,</span><span class="s5">fp=nullptr</span></p><p class="p1"><span class="s2">    </span><span class="s1">FILE</span><span class="s2">* fp =</span><span class="s10">fopen</span><span class="s2">(availableDbPath.</span><span class="s10">c_str</span><span class="s2">(),</span><span class="s11">"r"</span><span class="s2">);</span></p><p class="p1"><span class="s2">    </span><span class="s7">if</span><span class="s2">(fp == </span><span class="s7">nullptr</span><span class="s2">)</span></p><p class="p1"><span class="s2">    {</span></p><p class="p3"><span class="s6">      </span><span class="s2">// // </span><span class="s8">如果不存在,则将</span><span class="s2">Resources/db/phrases.db</span><span class="s8">拷贝到可读写路径下。</span></p><p class="p1"><span class="s2">      </span><span class="s1">ssize_t</span><span class="s2"> dbFileSize = </span><span class="s12">0</span><span class="s2">;</span></p><p class="p1"><span class="s2">      </span><span class="s7">unsigned</span><span class="s2"> </span><span class="s7">char</span><span class="s2"> *dbData = </span><span class="s3">CCFileUtils</span><span class="s2">::</span><span class="s9">getInstance</span><span class="s2">()-></span><span class="s9">getFileData</span><span class="s2">(dbname.</span><span class="s10">c_str</span><span class="s2">(),</span><span class="s11">"rb"</span><span class="s2">,&dbFileSize);</span></p><p class="p1"><span class="s2">      fp = </span><span class="s10">fopen</span><span class="s2">(availableDbPath.</span><span class="s10">c_str</span><span class="s2">(),</span><span class="s11">"wb"</span><span class="s2">);</span></p><p class="p1"><span class="s2">      </span><span class="s7">if</span><span class="s2"> (fp != </span><span class="s7">nullptr</span><span class="s2">)</span></p><p class="p1"><span class="s2">      {</span></p><p class="p1"><span class="s2">        </span><span class="s10">fwrite</span><span class="s2">(dbData,dbFileSize,</span><span class="s12">1</span><span class="s2">,fp);</span></p><p class="p3"><span class="s6">        </span><span class="s2">// </span><span class="s8">这里要注意释放</span><span class="s2">dbData</span><span class="s8">,</span><span class="s2">dbData</span><span class="s8">指向了</span><span class="s2">getFileData</span><span class="s8">中</span><span class="s2">malloc</span><span class="s8">出来的内存,并没有释放。</span></p><p class="p1"><span class="s2">        </span><span class="s13">CC_SAFE_FREE</span><span class="s2">(dbData);</span></p><p class="p1"><span class="s2">        </span><span class="s10">fclose</span><span class="s2">(fp);</span></p><p class="p1"><span class="s2">      }</span></p><p class="p1"><span class="s2">    }</span></p><p class="p1"><span class="s2">    </span><span class="s7">else</span></p><p class="p1"><span class="s2">    {</span></p><p class="p1"><span class="s2">      </span><span class="s10">fclose</span><span class="s2">(fp);</span></p><p class="p1"><span class="s2">    }</span></p><p class="p1"><span class="s2">  }</span></p><p class="p3"><span class="s6">  </span><span class="s2">// </span><span class="s8">无视之</span></p><p class="p1"><span class="s2">  </span><span class="s7">else</span></p><p class="p1"><span class="s2">  {</span></p><p class="p1"><span class="s2">    </span><span class="s1">string</span><span class="s2"> fullDbPath = </span><span class="s3">CCFileUtils</span><span class="s2">::</span><span class="s9">getInstance</span><span class="s2">()-></span><span class="s9">fullPathForFilename</span><span class="s2">(dbname.</span><span class="s10">c_str</span><span class="s2">());</span></p><p class="p1"><span class="s2">    availableDbPath = fullDbPath;</span></p><p class="p1"><span class="s2">  }</span></p><p class="p1"><span class="s2">  </span><span class="s7">return</span><span class="s2"> availableDbPath;</span></p><p class="p1"><span class="s2">}</span></p>
// 这里我就踩了两个坑,一个大坑,一个小坑。

大坑:

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

小坑:

调用

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

要注意释放返回的指针指向的内存。


在程序启动后,我们先调用这个函数,把数据库文件路径配置好。

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;
}


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

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;
}


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

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类中。只保留有用代码

// 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


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",&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的头文件

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.-----------------------------------------------------------------------------------------

// 公司封装的sqlite3操作库
    Cbsqlite3* cbsqlite = Cbsqlite3::shared();

// 解释:获取单例的Cbsqlite3对象。

static Cbsqlite3 *s_instance = NULL;
Cbsqlite3 *Cbsqlite3::shared()
{
    if (s_instance == NULL)
    {
        s_instance = new Cbsqlite3();
    }
    return s_instance;
}

---------------------------------------------------------------------------------------------



b.------------------------------------------------------------------------------------------
// 封装的函数,打开数据库
    int ret = cbsqlite->openDB(dbpath);
    if(ret != sqlITE_OK)
    {
        return false;
    }

// 解释:很简单,直接调用sqlite3_open()函数,打开数据库

int Cbsqlite3::openDB(std::string db)
{
    int ret = sqlite3_open(db.c_str(),&_pDB);
    return ret;
}
----------------------------------------------------------------------------------------------



c.---------------------------------------------------------------------------------------------

// 查询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)容器

class Cbsqlite3Results: public std::vector<Cbsqlite3ResultRow>
{
public:
    std::string _sql;
};

Cbsqlite3ResultRow又是什么呢?

再看:

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

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.---------------------------------------------------------------------------------------------

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

解释:

// 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;
}


// 回调函数

/ 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)重点解释回调函数

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

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

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

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


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

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

SELECT name,cid FORM TB_CATEGORY;
这条语句的结果集是下面的表结构:

| name | cid |

| 励志短语| 1 |

| 生日祝福 | 2 |

..........

| 开业祝福 | 8 |

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

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

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

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


再比如:
这一条sql语句

"SELECT COUNT(*) FROM tb_category"

得到的结果表表结构是


| COUNT(*) |

| 8 |

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

而对于下面的sql语句:

"SELECT COUNT(*) FROM AS cout1 tb_category"

结果表结构变成了

| cout1 |

| 8 |

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

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

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

---------------------------------------------------------------------------------------------



e.--------------------------------------------------------------------------------------------

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

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.---------------------------------------------------------------------------------------------

解释:最后关闭数据库

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

实现:

int Cbsqlite3::closeDB()
{
    if (_pDB != NULL)
    {
        sqlite3_close(_pDB);
        _pDB = NULL;
    }
    return sqlITE_OK;
}

调用sqlite_close函数

---------------------------------------------------------------------------------------------


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


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


哦,还没完。


四、后话:

顺便提一下:


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

函数原型:

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

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

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

解释:

参数1:数据库指针。

参数2:sql语句

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

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

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

参数6:错误信息地址。

// 下面是一个例子。

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

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

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 * 。

不过,我写不动了,董淳光总结得很好。还介绍了给sqlite数据库加密以及性能优化的问题。

这个是别人的转帖地址:http://blog.csdn.net/skywalker256/article/details/4556939


如果我的这篇看不懂,或者看得太累,可以看看他的。



如有错误,还请提供宝贵意见!

相关文章

    本文实践自 RayWenderlich、Ali Hafizji 的文章《...
Cocos-code-ide使用入门学习地点:杭州滨江邮箱:appdevzw@1...
第一次開始用手游引擎挺激动!!!进入正题。下载资源1:从C...
    Cocos2d-x是一款强大的基于OpenGLES的跨平台游戏开发...
1.  来源 QuickV3sample项目中的2048样例游戏,以及最近《...
   Cocos2d-x3.x已经支持使用CMake来进行构建了,这里尝试...