为什么这一行的代码行删不掉???还有为什么下面插入的图片这么大?怎么改小???搞不定了,随性写几个字在这儿,吐槽一下吧!吐槽中.............
一、说明
我将用实例与扩充解释的方式具体说明如何创建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中添加
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.代码解释
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();
static Cbsqlite3 *s_instance = NULL; Cbsqlite3 *Cbsqlite3::shared() { if (s_instance == NULL) { s_instance = new Cbsqlite3(); } return s_instance; }
---------------------------------------------------------------------------------------------
// 封装的函数,打开数据库 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函数指针。
原型: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; }
---------------------------------------------------------------------------------------------
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
如果我的这篇看不懂,或者看得太累,可以看看他的。
如有错误,还请提供宝贵意见!
原文链接:https://www.f2er.com/cocos2dx/344584.html