cocos2d-xでSQLiteを使う
珍しい内容でもないですが、自分なりのやり方を備忘録も兼ねてまとめておきます。
wxSplite3を使って暗号化も行いますが、その手順についてはこちらを参考にさせていただきました。
cocos2d-xでwxSqlite3を使ってみた | SystemTelescope
今回使用したバージョン
- Cocos2d-x 3.9
- wxSplite3 3.1.1
動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxSQLite
(Cocos2d-xのフルプロジェクトなので容量大きいです。)
データベース関連の処理をまとめるクラスを作成
データベースに関連する処理を全て行うDataManagerクラスを作成します。
DataManager.hpp
#include "cocos2d.h" #include "sqlite3.h" #define DB_FILE_NAME_ENC "EncryptedDB.db" // データベースを暗号化する場合のファイル名 #define ENCRYPT_DB_PASSWORD "23da3i3kJLale" // データベースを暗号化する場合のパスワード class DataManager { private: static DataManager* mManager; // データベースファイルを開いた時に記憶しておく時間(処理時間計測用) std::chrono::system_clock::time_point _dbOpenTime; public: DataManager(); static DataManager* getInstance(); };
DataManager.cpp
#include "DataManager.hpp" DataManager* DataManager::mManager = NULL; #pragma mark - 初期化 DataManager::DataManager() { } DataManager* DataManager::getInstance() { if(mManager == NULL) { mManager = new DataManager(); } return mManager; }
また、データベースのテーブル毎に、1件分の情報を格納するエンティティクラスを作成しておくと、
情報の管理がしやすくなります。
たとえばunit_dataというテーブルがあり、idとnameとweightカラムを持つとします。
class UnitData : public Node { public: int _id; std::string _name; float _weight; CREATE_FUNC(UnitData); };
データベースのオープンとクローズ
データベースにアクセスする前にはデータベースをオープンし、アクセスが終わったらクローズする必要があります。
毎回行うので、これらの処理は専用のメソッドとして用意します。
また、更新系処理ではトランザクションを使ったほうがよいので、これも別にします。
DataManager.cpp
// DBをオープン sqlite3* DataManager::openDB() { // DBファイルを開いた時間を記憶しておく(処理時間計測のため) _dbOpenTime = std::chrono::system_clock::now(); // SQLiteから読込 std::string dbPath = FileUtils::getInstance()->getWritablePath() + DB_FILE_NAME_ENC; sqlite3 *db = nullptr; // DBファイルオープン auto status = sqlite3_open(dbPath.c_str(), &db); if(status != SQLITE_OK) { CCLOG("▼sqlite3_open failed."); return nullptr; } // 暗号化する status = sqlite3_key(db, ENCRYPT_DB_PASSWORD, (int)strlen(ENCRYPT_DB_PASSWORD)); if(status != SQLITE_OK) { CCLOG("▼sqlite3_key failed."); return nullptr; } CCLOG("○DB opened successfully. File : %s",dbPath.c_str()); return db; } // DBをオープンしてトランザクションをスタートする sqlite3* DataManager::openDBAndStartTransaction() { sqlite3 *db = this->openDB(); if(db) { // トランザクション開始 auto status = sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr); if(status != SQLITE_OK) { CCLOG("▼Starting transaction failed."); return nullptr; } } return db; } // DBをクローズ bool DataManager::closeDB(sqlite3 *db) { auto status = sqlite3_close(db); if(status != SQLITE_OK) { CCLOG("▼Closing DB failed."); return false; } auto duration = std::chrono::system_clock::now() - _dbOpenTime; CCLOG("○DB Closed. time : %dms.",(int)std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()); return true; } // トランザクションをコミットしてDBをクローズする bool DataManager::commitTransactionAndCloseDB(sqlite3 *db) { // トランザクションコミット auto status = sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr); if(status != SQLITE_OK) { CCLOG("▼Commiting transaction failed."); return false; } return this->closeDB(db); }
テーブル作成
テーブルの作成はSQLで行います。
create table文は外部SQLファイルにしておくと管理が楽です。
createTable.sql
create table if not exists unit_data( id integer primary key, name text, weight real );
DataManager.cpp
// テーブルを作成する void DataManager::createTable() { // エラーメッセージ格納用 char* errorMessage = NULL; sqlite3 *db = this->openDB(); // トランザクションなし接続にする int status; // SQLファイル読み込み std::string createDBSQL = FileUtils::getInstance()->getStringFromFile("res/createTable.sql"); // テーブル作成 status = sqlite3_exec(db,createDBSQL.c_str(),nullptr,nullptr,&errorMessage); if(status != SQLITE_OK) { cocos2d::log("▼Creating table failed. Message : %s",errorMessage); CCASSERT(false, errorMessage); } // DBファイルクローズ this->closeDB(db); }
create table文でif not existsを使うことで、テーブルがなければ作成、あればなにもしないということができます。
データの追加/更新
テーブルに主キーが設定されていれば、insert or replace文が使えます。
SQL文を作成するときはsqlite3_mprintfを使うと、文字列のシングルクォーテーションエスケープなどを行ってくれます。
その場合、%Qを使うといいと思います。
DataManager.cpp
// データを登録もしくは更新する void DataManager::insertOrUpdateUnitData(int id, std::string name, float weight) { sqlite3 *db = this->openDBAndStartTransaction(); int status = 0; // エラーメッセージ格納用 char* errorMessage = NULL; // InsertSQL auto insertSQL = sqlite3_mprintf("insert or replace into unit_data(id,name,weight) values(%d,%Q,%f)", id,name.c_str(),weight ); status = sqlite3_exec(db, insertSQL, nullptr, nullptr, &errorMessage); if(status != SQLITE_OK) { CCLOG("▼Inserting UnitData data failed. Message : %s",errorMessage); CCASSERT(false, errorMessage); } else { CCLOG("○UnitData data successfully updated. no : %d",id); } // 解放(忘れてはいけない) sqlite3_free(insertSQL); this->commitTransactionAndCloseDB(db); }
データを検索
検索した結果は一件ずつ先ほど作成したエンティティに格納し、Vectorに追加して返すようにします。
DataManager.cpp
// データを検索する Vector<UnitData*> DataManager::selectUnitDataList() { Vector<UnitData*> unitDataList; sqlite3 *db = this->openDB(); // Select sqlite3_stmt *stmt = nullptr; auto selectSQL = "select id,name,weight from unit_data order by id desc"; if(sqlite3_prepare_v2(db,selectSQL,-1,&stmt,nullptr) == SQLITE_OK) { while(sqlite3_step(stmt) == SQLITE_ROW) { UnitData *unitData = UnitData::create(); unitData->_id = (int)sqlite3_column_int(stmt, 0); unitData->_name = StringUtils::format("%s",sqlite3_column_text(stmt, 1)); unitData->_weight = (float)sqlite3_column_double(stmt, 2); unitDataList.pushBack(unitData); } } else { CCASSERT(false,"Select UnitDataList error."); } // Statementをクローズ sqlite3_reset(stmt); sqlite3_finalize(stmt); this->closeDB(db); return unitDataList; }