seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

C++からSQLite3を使ってみる。

Python2.5でSQLite3を使って以来、C++でも試したいと思っていた。
Windows用のSQLite3のDLLは、msvcrt.dllのみに依存する、という手軽なものであり、DLL自体のサイズも400KB足らずと軽量である。
これを実際にプログラムに組み込んで使ってみた。

必要なもの

SQLiteのページから、以下の2つをダウンロードする。

  • sqlitedll-3_3_8.zip
  • sqlite-source-3_3_8.zip (sqlite3.h が必要なため)

DLLは当然として、この中にはsqlite3.dllと、そのDEFファイルのみが置かれている。
ヘッダファイル等がないので、ソースを取得する必要がある。
実際に使うヘッダは、sqlite3.hだけである。

準備

DLLはあるがlibファイルがないので、

lib /def:sqlite3.def /machine:x86

とやって、libとexpファイルを作成する。

こうやってできあがったsqlite3.libsqlite3.expをプロジェクトのフォルダにもってきて、libをリンカの追加のライブラリに指定すれば、通常のDLL同様に使えるようになる。(LoadLibraryとGetProcAddressが好きな人もいるだろうが、この場合、あまり意味はないだろう。)

つぎに、ソース一式のアーカイブの中からsqlite3.hを取り出して、自分のプロジェクトにもってきて参照できる場所においておく。

とりあえず、sqlite3.dllが実行時にはロードできなければならないので、プロジェクトのカレントディレクトリか実行ファイルと同じ場所においておく。*1

以上で準備完了。

実験コード

#include <locale.h>
#include <tchar.h>

#include "sqlite3.h"

int _tmain(int argc, _TCHAR* argv[])
{
    setlocale(LC_ALL, "");

    int ret;

    // データベースのオープン
    // オープンに失敗しても、Closeは行う必要がある。
    sqlite3* dp = NULL;
    ret = sqlite3_open16(L"testdb.dat", &dp);
    __try {
        if (ret != SQLITE_OK) {
            _tprintf(_TEXT("エラーメッセージ: %s\n"), sqlite3_errmsg16(dp));
            __leave;
        }

        // テーブルの作成
        ret = sqlite3_exec(dp, "CREATE TABLE TESTTBL("
            "IDX INTEGER PRIMARY KEY, VAL1 VARCHAR, VAL2 NUMBER);",
            NULL, NULL, NULL);
        if (ret != SQLITE_OK) {
            _tprintf(_TEXT("CREATEの失敗: %s\n"), sqlite3_errmsg16(dp));
        }

        // 既存テーブル上のデータの削除
        ret = sqlite3_exec(dp, "DELETE FROM TESTTBL;", NULL, NULL, NULL);
        if (ret != SQLITE_OK) {
            _tprintf(_TEXT("DELETEの失敗: %s\n"), sqlite3_errmsg16(dp));
            __leave;
        }

        // トランザクションの開始
        // トランザクションがかかっていないと、とてもとても遅くなる。
        ret = sqlite3_exec(dp, "BEGIN;", NULL, NULL, NULL);
        if (ret != SQLITE_OK) {
            _tprintf(_TEXT("BEGINの失敗: %s\n"), sqlite3_errmsg16(dp));
            __leave;
        }

        // 行の作成ループ
        sqlite3_stmt *stm = NULL;

        ret = sqlite3_prepare16(dp, L"INSERT INTO TESTTBL(VAL1, VAL2)"
            L"VALUES(?, ?)", -1, &stm, NULL);
        if (ret != SQLITE_OK || !stm) {
            // コメント等、有効なSQLステートメントでないと、戻り値はOKだがstmはNULLになる。
            _tprintf(_TEXT("INSERT用PREPAREDの失敗: %s\n"), sqlite3_errmsg16(dp));
            __leave;
        }
        __try {
            for (int idx = 0; idx < 100; idx++) {
                // パラメータのバインド
                sqlite3_bind_int(stm, 1, idx + 100); // インデックスは1ベース
                if (idx % 2 == 0) {
                    // セットされてないバインド変数はNULL扱いとなる。
                    // ただし、CLEAR_BINDINGSを呼び出していないと前回の
                    // バインド値となる。
                    sqlite3_bind_int(stm, 2, idx + 1000);
                }

                // 実行
                ret = sqlite3_step(stm);
                if (ret != SQLITE_DONE) {
                    _tprintf(_TEXT("INSERTの失敗: %s\n"), sqlite3_errmsg16(dp));
                    __leave;
                }

                // 最後に挿入したROWIDの取得
                const long long rowid = sqlite3_last_insert_rowid(dp);
                _tprintf(_TEXT("ROWID: %I64d\n"), rowid);

                // バインドのリセット
                ret = sqlite3_reset(stm);
                if (ret != SQLITE_OK) {
                    _tprintf(_TEXT("RESETの失敗: %s\n"), sqlite3_errmsg16(dp));
                    __leave;
                }
                // リセットしてもバインド変数はリセットされないため
                // 明示的にリセットする必要あり。
                ret = sqlite3_clear_bindings(stm);
                if (ret != SQLITE_OK) {
                    _tprintf(_TEXT("CLEAR BINDINGSの失敗: %s\n"),
                        sqlite3_errmsg16(dp));
                    __leave;
                }
            }
        }
        __finally {
            sqlite3_finalize(stm);
        }

        // コミット
        ret = sqlite3_exec(dp, "COMMIT;", NULL, NULL, NULL);
        if (ret != SQLITE_OK) {
            _tprintf(_TEXT("COMMITの失敗: %s\n"), sqlite3_errmsg16(dp));
            __leave;
        }

        // 作成した行の取得
        stm = NULL;
        ret = sqlite3_prepare16(dp, L"SELECT IDX, VAL1, VAL2 FROM TESTTBL ORDER BY IDX",
            -1, &stm, NULL);
        if (ret == SQLITE_OK && stm) {
            // コメント等、有効なSQLステートメントでないと、戻り値はOKだがstmはNULLになる。
            __try {
                // カラム数の取得
                int colCount = sqlite3_column_count(stm);
                _tprintf(_TEXT("カラム数=%d\n"), colCount);
                for (int col = 0; col < colCount; ++col) {
                    _tprintf(_TEXT("%d : %s %s\n"), col, 
                        sqlite3_column_name16(stm, col),
                        sqlite3_column_decltype16(stm, col));
                }
                for (;;) {
                    ret = sqlite3_step(stm);
                    if (ret == SQLITE_ROW) {
                        const int idx = sqlite3_column_int(stm, 0); // インデックスは0ベース
                        const void* val1 = sqlite3_column_text16(stm, 1);
                        const void* val2 = sqlite3_column_text16(stm, 2); // 実数 -> 文字列に
                        _tprintf(_TEXT("%d:%s:%s\n"), idx,
                            val1 == NULL ? _TEXT("NULL") : val1,
                            val2 == NULL ? _TEXT("NULL") : val2);
                        continue;
                    }
                    break;
                }
                if (ret == SQLITE_ERROR) {
                    _tprintf(_TEXT("LOOP中の失敗: %s\n"), sqlite3_errmsg16(dp));
                }
            }
            __finally {
                sqlite3_finalize(stm);
            }
        }
        else {
            _tprintf(_TEXT("PREPAREDの失敗: %s\n"), sqlite3_errmsg16(dp));
            __leave;
        }
    }
    __finally {
        sqlite3_close(dp);
        _tprintf(_TEXT("closed\n"));
    }
    return 0;
}

感想

試してみた感じでは、意外と簡単。
まだ始めたばかりで、よく分からないAPIとか沢山あるし、ドキュメントも英語なので見落とし・勘違い・間違いも多々ありそうな気がするが、とりあえず簡単にできた気がする。

やっぱり、よく出来ていると思う。

あとはC++で丁寧にラップしてあげれば、もっと使いやすくなる(あるいは使いにくくなる)気がするが、このぐらい有名のものは、当然、すでにライブラリがあるものと思う。
が、調べるのもめんどくさいし本家でないもののライブラリを取得したり追跡したりするのが、なんとなく、どんよりと気重なので、それについては、そのうち探してみることにしたいと思う。

以上、終わり。

*1:実際の配備においても、sqlite3自身は配布ポリシーをもっていないので、システムフォルダやPATHの通った共有フォルダに意味も無くぶち込むのは危険かと思われる。