今日はsqlite c++ラッパーライブラリ「SQLiteCpp」を紹介しようと思います。
ゲームではマスターデータ、ユーザーデータ管理のために、ソースコード埋め込みではなくリソースファイルとしてデータを管理することが多いと思います。
形式が何であれ、XML、JSON、バイナリ、SQL…いろいろ選択肢はあります。
今回はSQLについて取り扱っていこうと思います。
SQLは膨大なデータを扱うのに優れていて、スレッドセーフであり、データの検索がとてもしやすい形式です。
C++カジュアルゲームでSQLを使う場合、ライブラリ単体で動作するSQLiteがオススメです。
ただ、公式のC言語向けSQLiteライブラリは、C言語ゆえの手続きじみた面倒な処理を書かなくてはなりません。
sqlite3* db; int status = sqlite3_open_v2("filename", &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (status != SQLITE_OK){ std::cout << "open failed :" << std::endl; exit(1); } const char* query = "CREATE TABLE IF EXISTS test"; char* err; int status = sqlite3_exec(db, query, NULL, NULL, &err); if (status != SQLITE_OK){ std::cout << "query failed :" << err << std::endl; exit(1); } sqlite3_close(db);
openしてclose。ポインタのポインタを渡したり、どこか懐かしい構文です。
prepareでクエリの最適化やバインディングを行う際は
const char* query = "select * from value where id = ?"; sqlite3_stmt *stmt = NULL; sqlite3_prepare(db, query, sql.size(), &stmt, NULL); sqlite3_reset(stmt); sqlite3_bind_int(stmt, 0, 0); if(SQLITE_ROW == sqlite3_step(stmt)){ std::cout << sqlite3_column_int(stmt, 0) << std::endl;; } sqlite3_finalize(stmt);
うーん面倒くさい。
プログラマは怠惰で忘れっぽい生き物なので、こういうコードを書いてるといつか破綻しそうです。
オブジェクト指向っぽく書きたい。
そんな人のために、SQLiteには多くのC++ラッパーライブラリが存在します。
その中の一つ、「SQLiteCpp」を紹介します。
非常にシンプルに作られており、とても使い易いライブラリです。
cmakeでビルドする方法が記載されていますが、xcodeではもっと簡単にプロジェクトに組み込めます。
SQLiteCppフォルダをプロジェクトに配置し
SQLiteCpp/include をHeader Serch Pathsに追加して
SQLiteCpp/src/*.cpp、SQLiteCpp/sqlite3.cファイルすべてをCompile Sourcesに追加するだけです。
SQLiteCppの使い方
インスタンスの作り方
#include <SQLiteCpp/SQLiteCpp.h> int main() { try { SQLite::Database db("filename"); } catch (std::exception& e) { std::cout << "exception: " << e.what() << std::endl; } return 0; }
コンストラクタやクエリは例外を投げるのでtry-catchした方がいいです。
コンストラクタは第4引数まで持ちます。
第一引数:ファイル名
第二引数:オープンタイプ
OPEN_READONLY : 読み込み専用
OPEN_READWRITE : 読み書き
OPEN_CREATE : なかったら作る
OPEN_URI : URIも許可する
複数のタイプを組み合わせることができます。(例:SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE)
デフォルト値: OPEN_READONLY
第三引数:データベースがロックされている時のタイムアウト時間[ms]
デフォルト値:0
第四引数: VFSの設定
独自のファイルシステムを指定する際に使う。
デフォルト値:空文字列
成功したかどうかさえわかれば良いタイプのクエリを送る(例: CREATE TABLE)
int nb = db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)");
返り値:SQL文で変更された行の数
返り値でSQLITE_OKはかえってきません。
内部でSQLITE_OKかどうかをチェックし、例外を投げる機構が作られてるので、OKチェックをしたい場合はtry-catchしてください。
トランザクションの作成
明示的にトランザクションを使う場合はTransactionクラスを使います。
SQLite::Transaction transaction(db); /* dbに関する操作 */ transaction.commit();
もしcommitせずにtransactionインスタンスが破棄された場合、デストラクタでロールバックが行われます。
データを取ってくるクエリ
Statementクラスを使います。
SQLite::Statement query(db, "SELECT * FROM test WHERE id >= ?"); query.bind(1, 0); while (query.executeStep()){ int id = query.getColumn(0); std::string text = query.getColumn(1); std::cout << "row: " << id << ", " << text << std::endl; }
エラーをとりたい場合は同様にtry-catchします。
バインドはプレースホルダ「?」を使って行います。
プレースホルダインデックスは1から始まります。注意してください(本家sqlite3は0から)
getColumn関数は対応するインデックスのデータを取得しますが、返り値がColumnというデータラッパクラスです。
プリミティブ型になら何でもキャスト出来る型なので、インデックス等をよく確認するか、isIntegerメソッド等を利用するとデバッグに便利かもしれません。
非explicitキャスト演算子がオーバーロードされているため、以下の3文は等価です。
id = query.getColumn(0); id = static_cast(query.getColumn(0)); id = (int)query.getColumn(0);
1行目が暗黙キャスト、2行目がC++風キャスト、3行目がC風キャスト。
すべて内部でgetIntメソッドを呼びます。
他の型も同様です。
また、bindは内部でprepareしてくれます。
内部で扱われてるsqlite_stmtラッパクラスのコンストラクタとデストラクタが、prepare-rest-finalizeをやってくれてるからです。
こんなところでしょうか。
他にもexecAndGetとかisOkとかありますが、結局組み合わせだったり単なる問い合わせやGetterなので解説の必要はないでしょう。
このように、使いづらいC言語ライブラリでも、C++でラップすることによってたちまち使いやすくなります。
オブジェクト指向言語は偉大ですね。
今回はこの辺で。