sqlite3 C++ラッパーライブラリ「SQLiteCpp」の使い方

今日は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」を紹介します。

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++でラップすることによってたちまち使いやすくなります。
オブジェクト指向言語は偉大ですね。

今回はこの辺で。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です