ForestEXOR Games Blog

引退プログラマーは最新技術に着いて行けない

SQLite使ってみる その1

SQLiteの使い方を一通り、基本的にゲームクリエイター視点です。
使い方はggって色々な所を参考にしたのでここを見るよりそちらを見た方が詳しくわかると思う…
・環境
VisualStudio Community 2015
sqlite-amalgamation-3240000.zip(sqlite3.hとsqlite3.cを使用)
sqlite-dll-win32-x86-3240000.zip(sqlite3.dllをsqlite3-x86.dllに名前変更)

・とりあえず使用準備

使用するDBファイルには特に拡張子は必要無いが一応.dbと付けておく。

#include <stdio.h>
#include "sqlite3.h"
#pragma comment( lib, "sqlite3-x86.dll" )

int main(){
	char* strDBName = "test.db";
	char* strErrMsg = nullptr;
	sqlite3* pDB = nullptr;

	::getchar();
	return 0;
}
・データベースを開く

sqlite3_open_v2()を使う。
第3引数のflagsはゲームのデータ等なら「SQLITE_OPEN_READONLY」
アプリ等で書き込みが必要な時や管理者が変更すると言った場合は「SQLITE_OPEN_READWRITE」
この時既存の.dbファイルが無い場合はエラーコード「SQLITE_CANTOPEN」が返ってくる。
※追記
オープンに失敗した場合でも「sqlite3*」はインスタンスが作成されるのでクローズしないとメモリリークする事が判明、申し訳ありませんでした。

// 読み取り書き込みモードで開く 読み取り専用の場合は「SQLITE_OPEN_READONLY」
int iErr = ::sqlite3_open_v2( strDBName, &pDB, SQLITE_OPEN_READWRITE, nullptr );
if( SQLITE_OK != iErr  ){
	// オープン失敗
	::printf_s( "DB open failed.\n%s\n", ::sqlite3_errmsg(pDB) );

	// 追記部分↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

	::getchar();
	return -1;
}
・新規作成でデータベースを開く

SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE」を使用する場合はsqlite3_open()でもいいと思う。

iErr = ::sqlite3_open_v2( strDBName, &pDB, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
if( SQLITE_OK != iErr ){
	::printf_s( "DB open failed.\n%s\n", ::sqlite3_errmsg(pDB) );
	// 追記部分↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
	::getchar();
	return -1;
}else{
	::printf_s( ".db file created. DB open success.\n" );
}
・テーブル作成

hogeという名前のテーブルを作成。
列はinteger型の「id」と更新時の変化がわかりやすいdate型の「time」の2つにしてみた。
浮動小数にはデータ型でREALを指定するらしい、
勉強不足なだけかもしれないけど実際のSQLでREALというデータ型の記憶がない…
また検証してないので本当にREALでうまくいくかは未確定。

iErr = ::sqlite3_exec( pDB, "create table hoge(id integer, time date)", nullptr, nullptr, &strErrMsg );
if( SQLITE_OK != iErr ){
	// テーブル作成失敗
	::printf_s( "create table failed.\n" );
	::printf_s( "%s\n", strErrMsg );
	::sqlite3_free( strErrMsg );

	// DBを閉じる
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );
	::getchar();
	return -1;
}
// テーブル作成成功
::printf_s( "create table success.\n" );
・レコード挿入

timeに現在時刻を指定することで更新時に変化をわかりやすくする。

iErr = ::sqlite3_exec( pDB, "insert into hoge(id, time) values(1, CURRENT_TIME)", nullptr, nullptr, &strErrMsg );
if( SQLITE_OK != iErr ){
	// 挿入失敗
	::printf_s( "%s\n", strErrMsg );
	sqlite3_free( strErrMsg );

	// DBを閉じる
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );
	::getchar();
	return -1;
}
// 挿入成功
::printf_s( "insert success.\n" );
・レコードの更新

注意としては挿入と更新を同時にすると処理速度が速過ぎるのでCURRENT_TIMEに変化がない。
後述でソースコードを1つにまとめて2度起動した時のスクショを貼ります。

iErr = sqlite3_exec( pDB, "update hoge set time = CURRENT_TIME where id = 1", nullptr, nullptr, &strErrMsg );
if( SQLITE_OK != iErr ){
	// 更新失敗
	::printf_s( "%s\n", strErrMsg );
	sqlite3_free( strErrMsg );

	// DBを閉じる
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );
	::getchar();
	return -1;
}
// 更新成功
::printf_s( "update success. 1\n" );
・レコードの抽出と表示

BLOB型の扱いがわからなかったのとNULL型はまず使わない気がする(し扱いに困る)ので割愛。

sqlite3_stmt* stmt = nullptr;	// ステートメント
iErr = ::sqlite3_prepare_v2( pDB, "select * from hoge", -1, &stmt, nullptr );
if( SQLITE_OK != iErr ){
	// 抽出失敗
	::printf_s( "%s\n", ::sqlite3_errmsg(pDB) );
	::sqlite3_reset( stmt );
	::sqlite3_finalize( stmt );

	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );
	::getchar();
	return -1;

}else{
	// 抽出成功
	::printf_s( "select success.\n" );
	for( ; SQLITE_ROW == ::sqlite3_step(stmt) ; ){	// step1回で1レコード取得
		::printf_s("+--------+-----------------+\n| ");
		// レコードの列数取得
		int iColMax = ::sqlite3_column_count(stmt);
		for( int i(0) ; i < iColMax ; i++ ){
			// 列名取得
			const char* name = sqlite3_column_name( stmt, i );
			// 列のデータ型取得
			int iColType = sqlite3_column_type( stmt, i );

			switch( iColType ){
			// INTEGER(int)
			case SQLITE_INTEGER:
				::printf_s( "%s = %d", name, sqlite3_column_int( stmt, i ) );
				break;
			// REAL(double)
			case SQLITE_FLOAT:
				::printf_s( "%s = %f", name, (float)sqlite3_column_double( stmt, i ) );
				break;
			// TEXT(string)
			case SQLITE_TEXT:
				::printf_s( "%s = %s", name, sqlite3_column_text( stmt, i ) );
				break;
			// NULL(nullptr)
			case SQLITE_NULL:
				break;
			// BLOB(不明)
			case SQLITE_BLOB:
				break;
			}

			::printf_s(" | ");
		}// for( int i(0) ; i < iColMax ; i++ )
		::printf_s("\n");
	}// for( ; SQLITE_ROW == ::sqlite3_step(stmt) ; )
	::printf_s("+--------+-----------------+\n");
}
// stmtの後始末
::sqlite3_reset( stmt );
::sqlite3_finalize( stmt );
・レコードの削除
iErr = sqlite3_exec( pDB, "delete from hoge where id = 3", nullptr, nullptr, &strErrMsg );
if( SQLITE_OK != iErr ){
	// 削除失敗
	::printf_s( "%s\n", strErrMsg );
	sqlite3_free( strErrMsg );
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );
	::getchar();
	return -1;
}else{
	// 削除成功
	::printf_s( "delete success.\n" );
}
・おまけ1 テーブル数の取得

既存の.dbファイルがあったとしても正しいデータが入っているとは限らない。
そんな時にテーブル数やテーブル名で.dbファイルが正しいか確認するといいかも。

iErr = sqlite3_prepare_v2( pDB, "select count(*) as TableNum from sqlite_master where type='table'", -1, &stmt, nullptr );
if( SQLITE_OK != iErr ){
	::printf_s( "%s\n", ::sqlite3_errmsg(pDB) );
	::sqlite3_reset( stmt );
	::sqlite3_finalize( stmt );
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );
	::getchar();
	return -1;
}else{
	// 表示
	::printf_s( "select success\n" );
	while( SQLITE_ROW == sqlite3_step(stmt) ){
		int iMax = sqlite3_column_count(stmt);
		for( int i(0) ; i < iMax ; i++ ){
			const char* name = sqlite3_column_name( stmt, i );
			int iColType = sqlite3_column_type( stmt, i );
			if( SQLITE_TEXT == iColType ){
				::printf_s( "%s = %s\n", name, sqlite3_column_text( stmt, i ) );
			}else if( SQLITE_INTEGER == iColType ){
				::printf_s( "%s = %d\n", name, sqlite3_column_int( stmt, i ) );
			}
		}
	}
}
::sqlite3_reset( stmt );
::sqlite3_finalize( stmt );
・おまけ2 テーブル名の取得

tbl_nameを*に変更すると色々なデータが抽出できたりする。

iErr = sqlite3_prepare_v2( pDB, "select tbl_name from sqlite_master where type='table'", -1, &stmt, nullptr );
if( SQLITE_OK != iErr ){
	::printf_s( "%s\n", ::sqlite3_errmsg(pDB) );
	::sqlite3_reset( stmt );
	::sqlite3_finalize( stmt );
	iErr = ::sqlite3_close( pDB );
	if( SQLITE_OK != iErr ){
		::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
		::getchar();
		return -1;
	}
	::printf_s( "DB close success.\n" );
	::getchar();
	return -1;
}else{
	// 表示
	::printf_s( "select success\n" );
	while( SQLITE_ROW == sqlite3_step(stmt) ){
		int iMax = sqlite3_column_count(stmt);
		for( int i(0) ; i < iMax ; i++ ){
			const char* name = sqlite3_column_name( stmt, i );
			int iColType = sqlite3_column_type( stmt, i );
			if( SQLITE_TEXT == iColType ){
				::printf_s( "%s = %s\n", name, sqlite3_column_text( stmt, i ) );
			}else if( SQLITE_INTEGER == iColType ){
				::printf_s( "%s = %d\n", name, sqlite3_column_int( stmt, i ) );
			}
		}
	}
}
::sqlite3_reset( stmt );
::sqlite3_finalize( stmt );
・後始末

散々エラー時の処理で書いたけど最後に後始末する事を忘れずに。

iErr = ::sqlite3_close( pDB );
if( SQLITE_OK != iErr ){
	::printf_s( "DB close failed.\n%s\n", ::sqlite3_errmsg(pDB) );
	::getchar();
	return -1;
}
::printf_s( "DB close success.\n" );
・終わり

本当はC++DirectXの事を書こうと思って始めたブログなのに初記事がSQLiteになるとは…
そして久しぶりに文章書いたので説明文なのか話し口調なのかよくわからない汚い文体になってしまった。
年に何回更新するかも不明なブログですけど書いてるうちにまともになってくるかな?

長くなったので上記のコードを一つのプログラムにまとめたものと実行結果のスクショは次の記事に書きます。
SQLite使ってみる その2 - forestexor’s blog(仮)