Cocos2d-x(1) SQLite
SQLite的应用
SQLite:轻量级的关系数据库,用于高速且安全地在本地存储数据。在对性能要求较高时,可以考虑使用 SQLite存储数据。
SQLite(1)
从性能上说,XML 方式的存储基本可以满足 1 MB 以下的存储要求。但在更复杂的情景中,我们可能需要存储多种不同的类,每个类也需要存储不同的对象,此时XML 存储的速度就将成为瓶颈。即便分文件存储,管理起来也很麻烦,这个时候可以引入数据库来提升存储效率。
关系数据库是一种经典的数据库,其中的数据被组织成表的形式,具有相同形式的数据存放在同一张表中,表内每一行代表一个数据。在表的基础上,数据库为我们提供增、删、改、查等操作,这些操作通常采用SQL (结构化查询语言)表达。这种格式化、集中的存储再加上结构化的操作语言带来一个非常大的好处:可以进行深度的优化,大大提升存储和操作的效率。
SQLite是移动设备上常用的一个嵌入式数据库,具有开源、轻量等特点,其源代码只有两个".c"文件和两个".h" 文件,并且已经包括了充分的注释说明。相比MySQL 或者SQL Server 这样的专业级数据库,甚至是比起同样轻量级的Access,SQLite的部署都可谓非常简单,只要将这4 个文件导入工程中即可,这使得编译之后的 SQLite非常小。
SQLite将数据库的数据存储在磁盘的单一文件中,并通过简单的外部接口提供SQL 支持。由于其设计之初即是针对小规模数据的操作,在查询优化、高并发读写等方面做了极简化的处理,可以保证不占用系统额外的资源,因此,在大多数的嵌入式开发中,会比专业数据库有更快速、高效的执行效率。
SQLite的核心接口函数只有一个,如下所示:
int sqlite3_exec( sqlite3*, //一个已打开的数据库 const char *sql, //将要执行的SQL 语句 int (*callback)(void*, int, char**, char**), //回调函数 void *, //回调函数的第一个参数(用于传递自定义数据 char **errmsg //出错时返回的错误信息 );
这个函数在一个打开的数据库中为我们执行一条SQL 语句,并通过回调函数处理结果,其参数的含义已经由注释给出。为了开发上的便利,我们还可以通过第四个参数指定一个任意类型的对象传递给回调函数。当此函数运行出错时,错误信息会以字符串形式输出在errmsg中。具体的用法我们将在下面详细介绍。
我们依然沿用UserRecord类作为例子,在其中添加 3 个接口函数,具体如下所示:
sqlite3* prepareTableInDB(const char* table, const char* dbFilename); void saveToSQLite(const char* table = "UserRecord", const char* dbFilename = "sql.db" ); void readFromSQLite(const char* table = "UserRecord",const char* dbFilename = "sql.db" );
首先,我们需要为一次读写操作准备数据库,相关代码如下:
sqlite3* UserRecord::prepareTableInDB(const char* table,const char *dbFilename) { sqlite3 *pDB = NULL; char *errorMsg = NULL; if(SQLITE_OK != sqlite3_open(dbFilename, &pDB)) { CCLOG("open sql file failed!"); return NULL; } string sql = "create table if not exists " + string(table) + "(id char(80) primary key,coin integer,experience integer)"; sqlite3_exec(pDB, sql.c_str(), NULL, NULL, &errorMsg); if(errorMsg != NULL) { CCLOG("exec sql %s fail with msg: %s", sql.c_str(), errorMsg); sqlite3_close(pDB); return NULL; } return pDB; }
这里我们完成两部分操作,首先用sqlite3_open打开数据库,如果数据库文件不存在,则会自动创建。打开成功后,如果目标表格不存在,则创建表格。这里我们执行了一句SQL 语句,用了最基本的 sqlite3_exec 的方式,单纯地执行并查看是否成功,不涉及数据库操作后与游戏数据的交互。
准备完数据库之后,我们来尝试将数据从SQLite读取到内存数据中,相关代码如下:
void UserRecord::readFromSQLite(const char* table, const char *dbFilename) { char sql[1024]; sqlite3* pDB = prepareTableInDB(table, dbFilename); if(pDB != NULL) { int count = 0; char *errorMsg; sprintf(sql,"select * from %s where id = %s", table, this->getUserID().c_str()); sqlite3_exec(pDB, sql, loadUserRecord, this, &errorMsg); if(errorMsg!=NULL) { CCLOG("exec sql %s fail with msg: %s", sql, errorMsg); sqlite3_close(pDB); return; } } sqlite3_close(pDB); }
这里同样执行了一条SQL 语句,将目标对象根据 ID从数据库中读出,但不同的是,这里我们用到了下面这个回调函数并在其中将查询结果读取到UserRecord对象中:
int loadUserRecord(void* para,int n_column,char** column_value,char **column_name) { UserRecord* record = (UserRecord*)para; int coin, experience; sscanf(column_value[1],"%d",&coin); sscanf(column_value[2],"%d",&experience); record->setCoin(coin); record->setExp(experience); return 0; }
SQLite(1)
该回调函数用于处理SQL 操作成功后返回的数据。返回的数据可能是一个字符串或整型量,也可能是数据表中的若干行数据,而每组数据都会调用回调函数一次,若查询操作得到了N 行结果,则回调函数会被调用 N 次,每次传输一行待处理的结果。回调函数一共有4 个参数,第一个参数是需要供回调函数使用的某段数据的指针,通常指向一个对象或一个数组,以便根据查询结果修改数据;第二个参数是操作结果返回的记录的列数;第三个参数是返回结果的数组,这些返回结果中的每一列都是一个字符串;第四个参数则是每一列的列名。对于一条特定的SQL 语句来说,第二个和第四个参数通常是固定不变的。
在上面这个回调函数中,我们传入的是一个UserRecord类型的指针,因为我们要把查询结果存入这个 UserRecord对象之中以便后续使用。查询请求已经限制了返回结果最多仅有一个,因此我们不需要额外的判断。只需要从返回的字符串中提取出金币数量和经验值,并把相应的数据填充到UserRecord对象中就可以了。
同样,我们可以编写将UserRecord写入SQLite 数据库的接口函数,相关代码如下:
void UserRecord::saveToSQLite(const cha r* table, const char *dbFilename) { char sql[1024]; sqlite3* pDB = prepareTableInDB(table, dbFilename); if(pDB!=NULL) { int count = 0; char *errorMsg; sprintf(sql, "select count(*) from %s where id = %s", table, this->getUserID().c_str()); sqlite3_exec(pDB, sql, loadRecordCount, &count, &errorMsg); if(errorMsg != NULL) { CCLOG("exec sql %s fail with msg: %s", sql, errorMsg); sqlite3_close(pDB); return; } if(count) { sprintf(sql, "update %s set coin = %d, experience=%d where id = %s", table, this->getCoin(), this->getExp(), this->getUserID().c_str()); } else { sprintf(sql, "insert into %s values( %s,%d,%d)", table, this->getUserID().c_str(), this->getCoin(), this->getExp()); } sqlite3_exec(pDB, sql, NULL, NULL, &errorMsg); if(errorMsg != NULL){ CCLOG("exec sql %s fail with msg: %s", sql, errorMsg); sqlite3_close(pDB); return; } } sqlite3_close(pDB); } int loadRecordCount(void* para, int n_column, char** column_value, char** column_name) { int *pCount=(int*)para; sscanf(column_value[0], "%d", pCount); return 0; }
这个功能同样是由一个调用数据库接口的主调函数和一个处理返回结果的回调函数共同完成的。由于数据库中的更新和插入使用不同的命令,所以我们必须先查询数据库中是否存在同ID的对象,再决定是更新当前对象还是插入数据库中。
注意上面的每一个SQLite操作后,我们都检查了操作是否成功,在失败的情况下及时中止后面的操作。而在一切操作完成之后,不管操作是否成功,都必须关闭数据库,以保证对数据库的改变能够正确保存。 最后,我们可以尝试查看读写的效果。除了直接从数据库中读取特定的数据之外,还可以借助工具查看整个数据库的状态。S QLite Database Browser 就是一个可以方便地查看SQLite数据库的图形化工具,它是开源而且免费的。图 13- 1 显示的就是将一个 UserRecord对象写入数据库后数据库的状态。
尽管数据库中的文件已经被封装为数据库专用格式的文件,无法通过简单的文本工具查看其内容,但是如果通过合适的工具打开,SQLite数据库和XML 同样存在明文存放数据的问题。对于敏感的数据,同样需要通过加密来提高安全性,其做法与XML 类似,在此就不再赘述了。

End, thank you!!
浙公网安备 33010602011771号