iPhone プログラムで SQLite を使用する
SPECIAL
iPhone プログラムでデータベースを利用する
iPhone アプリのプログラミングでは、データベースとして SQLite を利用することが出来るようになっています。
SQLite はアプリケーションに組み込んで使用するタイプの軽量なデータベース (DBMS) で、最終的にはデータベースファイルへの読み書きだけで、データベースの操作を行うことが可能になっています。
ただ、SQLite は日付時刻型がなかったりとか、データ型を "INTEGER NOT NULL PRIMARY KEY" で宣言すれば自動的にオートナンバー型のように振る舞うなど、Microsoft SQL Server や MySQL 等とは雰囲気がずいぶん違いそうなので、そういう面での慣れも必要になるかもしれません。
データベースファイルの準備
iPhone プログラムで SQLite を使うためには、予め SQLite 用のデータベースファイルを作成する必要があります。
作成には Mac OS X のターミナルを利用して、次のようにして、SQLite を実行します。
こうすることで、データベースファイルを "database.sqlite" として、SQLite ツールが起動します。
ここで CREATE 文を使用して、データベースを作成します。
create table history
(
historyId INTEGER NOT NULL PRIMARY KEY,
displayName TEXT,
registDate REAL
);
出来上がったデータベースファイルは、Xcode から、目的のプロジェクトに追加しておきます。
SQLite のライブラリーを追加する
Xcode のプロジェクトに、SQLite のライブラリーを追加します。
プロジェクトの "Framework" のところから、"追加" → "既存のフレームワーク" として、"libsqlite3.0.dynlib" を追加します。
これで利用準備が整いました。
また、コーディングの際には、#import <sqlite.h> として、SQLite に関する関数定義を読み込んであげます。
SQL クエリを実行する
SQLite の利用準備が整ったら、いよいよデータベース操作のプログラミングを行います。
SQLite では、利用にあたって sqlite3_open 関数を使用して、データベースファイルを開くところから始めます。この関数は第一引数にデータベースファイルのパスを、第二引数にデータベース接続ハンドルを受け取るための sqlite3* 型の変数を渡します。
iPhone プログラムの場合、データベースファイルを Resource フォルダーに投げ込んで、初回起動時にバンドルからワークスペースへデータベースをコピーし、それを利用してデータベース操作を行う形が良さそうなので、全体的には次のように初期化コードを作成して行く感じになると思います。
sqlite3* db;
NSString* work_path;
NSString* database_filename;
NSString* database_path;
NSString* template_path;
// データベース名をここでは "database.sqlite" とします。
database_filename = @"database.sqlite";
// データベースファイルを格納するために文書フォルダーを取得します。
work_path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
// データベースファイルのパスを取得します。
database_path = [NSString stringWithFormat:@"%@/%@", work_path, database_filename];
// 文書フォルダーにデータベースファイルが存在しているかを確認します。
NSFileManager* manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:database_path])
{
NSError* error = nil;
// 文書フォルダーに存在しない場合は、データベースの複製元をバンドルから取得します。
template_path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:database_filename];
// バンドルから取得したデータベースファイルを文書フォルダーにコピーします。
if (![manager copyItemAtPath:template_path toPath:database_path error:&error])
{
// データベースファイルのコピーに失敗した場合の処理です。
}
}
// 文書フォルダーに用意されたデータベースファイルを開きます。
if (sqlite3_open([database_path UTF8String], &db) == SQLITE_OK)
{
// データベースファイルを SQLite で開くことに成功しました。
}
少し長くなりますが、このような流れで、バンドルに格納した初期化用のデータベースファイルを使用して文書フォルダー内のデータベースファイルが存在しない場合は初期化した上で、文書フォルダー内のデータベースファイルを SQLite で利用可能な状態にすることができると思います。
SQLite でデータベースファイルを開いたら、あとは sqlite3_prepare_v2 関数で SQL 文をコンパイルして、必要に応じて sqlite3_bind_int 関数などでパラメーターに値をバインドし、最後に sqlite3_step 関数を用いてそれを実行するという流れになります。
この時、パラメーターとして @名前 形式を利用した場合には、sqlite3_bind_parameter_index 関数を利用して、パラメーター名からインデックスを取得できるので、それを利用してパラメーターの設定を行います。
sqlite3_stmt* statement;
NSString* sql;
NSInteger param_index;
sql = @"SELECT displayName, registDate FROM history WHERE historyId = @historyId";
// SQL 文をコンパイルします。
if (sqlite3_prepare_v2(db, [sql UTF8String], -1, &statement, NULL) != SQLITE_OK)
{
// SQL 文のコンパイルに失敗した場合の処理です。
}
// // パラメーター @historyId のインデックス番号を取得します。
param_index = sqlite3_bind_parameter_index(statement, "@historyId");
if (param_index == 0)
{
// 指定したパラメーター名が見つからなかった場合の処理です。
}
//// WHERE 句の id として変数 target_id の値を設定します。
sqlite3_bind_int(statement, param_index, target_id);
// SQL 文を実行し、結果行が得られなくなるまで繰り返します。
while (sqlite3_step(statement) == SQLITE_ROW)
{
// 最初のカラムの値を文字列型として読み取ります。
NSString* text_value;
const unsigned char* buffer;
buffer = sqlite3_column_text(statement, 0);
text_value = [NSString stringWithCString:(const char*)buffer encoding:NSUTF8StringEncoding];
}
このように、コンパイルした SQL 文を sqlite3_stmt で受け、それに対してパラメーターを設定したり、実行して、結果をカラムの 0 から始まるインデックスで指定して取得するという流れになります。
必要に応じて sqlite3_reset 関数を使用して、設定済みのパラメーター情報を初期化することもできる様子です。
なお、SQL 文の実行により出力されたカラムの個数を取得するには、次のようにします。
NSInteger colmun_count = sqlite3_column_count(statement);
また、出力されたカラムの名前を取得するには、sqlite3_column_name 関数を使用します。
たとえば、0 番目のカラム名を NSString* 型で取得したい場合には、次のような感じになります。
NSString* column0_name = [NSString stringWithCString:sqlite3_column_name(statement, 0) encoding:NSUTF8StringEncoding];
こうして、一連のデータベース処理を終え、データベースファイルを使用しなくても良い状態になったら、次のようにしてデータベースを閉じて終了します。
sqlite3_close(db);
以上のように、SQLite を利用するには、コードがずいぶんと長くなってしまうように思います。
やっていることは単純なので、慣れてくれば理解はしやすいと思いますが、コーディングの効率を高めるためには Objective-C で上手にラップするなどの工夫が必要かもしれません。
日付データの取り扱い
SQLite の利用にあたって、日付データについても、少し慣れが必要そうです。
SQLite では、INTEGER ,REAL, TEXT, BLOB といった程度しかデータ型を持っていないため、日付情報は TEXT 型か REAL 型で持たせる必要が出てきます。
それを支援するための date, time, datetime, julianday といった組み込み関数も用意されているので扱いは楽な方かもしれないですけど、慣れるまでは戸惑う点も多そうでした。
日付型を REAL 型で持たせる場合を中心に考えると、データベースへ格納する際には、'2010-07-09 20:18:00' や '2010-07-09' といった日時を表す文字列を用意して、それを julianday 関数を通すことで、ユリウス暦日という REAL 型の値に置き換えることが可能です。
julianday('2010-07-09 20:18:00')
ユリウス暦日は紀元前 4714 年 11 月 24 日の正午を基準とした通算日で表したものらしいので、この値の大小比較で、日付の序列関係を判定することができます。
逆に、そうやって REAL 型として記録していたユリウス暦日を文字列に変換する場合には、日付と時刻の両方なら datetime 関数、日付だけなら date 関数、時刻だけなら time 関数へ REAL 型のカラムを通すことで、日時を表す文字列に変換することが出来ます。
datetime(2455387.34583333)
date(2455387.34583333)
time(2455387.34583333)
これらの変換関数では、第二引数で、指定した日付・時刻から相対的に指定した分だけ動いた日付を取得することが可能になっています。
datetime(2455387.34583333, '1 days')
date(2455387.34583333, '-1 days')
time(2455387.34583333, 'localtime')
例えば、指定した日の一日後であれば、第二引数に '1 days' と指定します。逆に 1 日前であれば '-1 days' となります。他にも '0.1 days' と小数点で指定できたり、'start of day' によって指定した日の当日 00:00 を取得したり、'weekday 0' によって指定した日を起点とした最初の日曜日を取得出来たりなど、日付を動かすことがとてもしやすくなっていました。
ちなみに、ユリウス暦日で保存した値を再び日本時間に戻すのも簡単で、この date, time, datetime 関数の第二引数に 'localtime' を指定するだけでそれを実現できます。
以上のように、iPhone プログラミングで SQLite を扱う場合、NSDate 型を文字列に変換したり、逆に文字列から NSDate へ変換して、それを julianday 関数や datetime 関数に通すという処理が重要になってくると思います。
NSDate 型と文字列との相互変換については 文字列から NSDate に変換する や NSDate を文字列に変換する に記してみていますので、必要に応じてそちらも参考にしてください。