iOS で SQLite - FMDB の使い方
iOS で SQLite を簡単に扱うためのライブラリ FMDB についてまとめる。
- 2017/1/22 本記事のサンプルを最新の Objective-C と Swift で書き直して記事にまとめました iOS で SQLite - FMDB の使い方 2017
FMDB とは?
FMDB は SQLite を iOS の Objective-C で扱いやすくするための Wrapper ライブラリ。 GitHub で公開されている。
インターフェースや使用感は JDBC や ADO.NET に近い。よってこれらを利用したことがあればスムーズに理解できるだろう。
FMDB の仕様準備
まず FMDB を利用したいプロジェクトで SQLite 用のライブラリを有効にする。手順は以下。
- Xcode 左ペインのナビゲーションからプロジェクトを選択
- 右ペインに PROJECT と TARGET が表示されるので後者を選択
- 右ペイン上部のタブから Build Phases を選択
- ビルドに関する設定項目が表示されるので、その中の Link Binary With Libraries を選択
- Link Binary With Libraries 左下に
+
と-
ボタンがあるので+
をクリック - Choose framework and libraries add: というダイアログが表示される
- フレームワークとライブラリ一覧の中から libsqlite3.0.dylib を選択、ダイアログ上部の検索窓で絞り込みも可能
- ダイアログ右下の Add ボタンをクリック
文章だと分かりにくいのでスクリーンショットに手順を記載してみた。
次に FMDB のソースをプロジェクトへ組み込む。
- FMDB のプロジェクト ページ から ZIP 形式のプロジェクトを入手
- ZIP を展開
- 展開されたフォルダ内にある src フォルダを開く
- フォルダ内のファイルで fmdb.m 以外を、自身のプロジェクト フォルダへコピー
- fmdb.m はサンプルとテストを兼ねたソース
- fmdb.m には main 関数が実装されているので、これをプロジェクトに組み込むとコンパイル エラーになる
- プロジェクトに組み込む場合、サブ フォルダを用意したほうがよい
- 私の場合、lib/FMDB に格納している
- Xcode から FMDB のソースを参照する
- ビルドが通ることを確認する
以上で FMDB を使用するための準備は完了。
データベース作成と open/close
データベースを用意する場合、予め作成したデータベース ファイルをプロジェクトのリソースに組み込んでコピーするか SQLite 自身の機能で生成する。私は後者のほうが好みなのでそちらについて書く。iOS アプリの作業領域へ app.db という名前のデータベース ファイルを生成する場合の処理は、以下のようになる。
NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES );
NSString* dir = [paths objectAtIndex:0];
FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"app.db"]];
[db open];
[db close];
FMDatabase
がデータベースを表す。databaseWithPath
メソッドにファイルのパスを指定することでファイルが既存なら参照、なければ新規作成される。そしてデータベース接続された FMDatabase
インスタンスを返す。
データベース操作を開始する場合、このインスタンスに対して open メソッドを呼び出す。終了は close メソッドとなる。close を呼び出すとデータベースが閉じられ、変更内容がファイルに保存される。FMDatabase のヘッダを見る限りインメモリ モードは用意していないようだ。
CREATE
テーブル作成。
FMDatabase* db = [FMDatabase databaseWithPath:@"データベースのパス"];
NSString* sql = @"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);";
[db open];
[db executeUpdate:sql];
[db close];
FMDatabase - executeUpdate
メソッドに CREATE
文を指定すればテーブル作成される。SQLite は IF NOT EXISTS
に対応しており、これを付けておくとテーブルが存在しない時だけ作成してくれて便利。
INSERT
行の挿入。
FMDatabase* db = [FMDatabase databaseWithPath:@"データベースのパス"];
NSString* sql = @"INSERT INTO users (name) VALUES (?)";
[db open];
[db executeUpdate:sql, @"名前"];
[db close];
executeUpdate
は Prepared Statement に対応している。SQL 文中の ?
で記述したパラメータ部分に可変長引数の 2 番目以降を割り当ててゆく。
DELETE
行の削除。
FMDatabase* db = [FMDatabase databaseWithPath:@"データベースのパス"];
NSString* sql = @"DELETE FROM users WHERE id = ?";
[db open];
[db executeUpdate:sql, [NSNumber numberWithInteger:14]];
[db close];
SELECT
行の選択。
FMDatabase* db = [FMDatabase databaseWithPath:@"データベースのパス"];
NSString* sql = @"SELECT id, name FROM users;";
[db open];
FMResultSet* results = [db executeQuery:sql];
NSMutableArray* users = [[[NSMutableArray alloc] initWithCapacity:0] autorelease];
while( [results next] )
{
User* user = [[User alloc] init];
user.userId = [results intForColumnIndex:0];
user.name = [results stringForColumnIndex:2];
[users addObject:user];
[user release];
}
[db close];
FMDatabase - executeQuery
は結果を FMResultSet
インスタンスとして返す。FMResultSet - next
を呼び出すことで取得された行が順に選択されてゆく。行が存在する場合は YES
、尽きたなら NO
が返されるので本メソッドの呼び出しを while
条件とすれば全行を列挙できる。
トランザクション
SQLite は暗黙的なトランザクション制御をおこなう。普段はこれで問題ないしむしろ安全に倒してくれて助かる。しかし大量の INSERT
を実行すると都度 BEGIN
~ COMMI
T が実行されるため速度が大幅に低下する。そうした処理をおこなう場合は対象区間を明示的にトランザクション処理して効率化する。
FMDB による明示的トランザクションは FMDatabase - beginTransaction
で開始され commit
で終了する。エラーなどで処理前の状態に戻したいならば rollback
メソッドを実行して操作を取り消す。
FMDatabase* db = [FMDatabase databaseWithPath:@"データベースのパス"];
NSString* sql = @"INSERT INTO users (name) VALUES (?)";
[db open];
[db beginTransaction];
BOOL isSucceeded = YES;
for( User* user in users )
{
if( ![db executeUpdate:sql, user.name] )
{
isSucceeded = NO;
break;
}
}
if( isSucceeded )
{
[db commit];
}
else
{
[db rollback];
}
[db close];
明示的なトランザクションを実行している間はデータベース全体がロックされる。そのため区間内で新たにデータベースを open
すると競合により応答なしとなるため注意する。
型
FMDB を利用して取得・設定する型についてまとめる。
値の取得は FMDatabase - executeQuery
の結果として返された FMResultSet
インスタンスのメソッドでおこなう。メソッド名は typeForColumn
または typeForColumnIndex
。それぞれカラム名と位置インデックスで値を取得できる。例えば stringForColumn
に name
を指定したなら name
という文字列 TEXT
カラムが得られる。intForColumnIndex
に 4
を指定した場合は SELECT
文の取得対象で 4 番目の値を整数値 INTEGER
として返す。
代表的なデータ型の対応について表にまとめた。
SQLite | Objective-C | 取得メソッド |
---|---|---|
TEXT | NSString | stringForColumn、stringForColumnIndex |
INTEGER | int | intForColumn、intForColumnIndex |
BOOL | BOOL | boolForColumn、boolForColumnIndex |
REAL | double | doubleForColumn、doubleForColumnIndex |
DATETIME ( INTEGER, REAL, TEXT ) | NSDate | dateForColumn、dateForColumnIndex |
BLOB | NSData | dataForColumn、dataForColumnIndex |
FMDatabase - executeQuery
に指定するパラメータは実行時に型チェックされる。TEXT
なら NSString
、INTEGER
なら NSNumber
に対応づけられ間違った型を指定するとエラーになる。取得なら FMResultSet
が Objective-C の型へ自動変換してくれるが設定時は自前で処理しなければならない。FMDB を利用していると意外にハマリやすいポイントなので、この対応も表にしておく。
SQLite | Objective-C | 変換方法 |
---|---|---|
TEXT | NSString | 変換不要 |
INTEGER | NSInteger | NSNumber - numberWithInt |
BOOL | BOOL | NSNumber - numberWithBool |
REAL | double | NSNumber - numberWithDouble |
DATETIME ( INTEGER, REAL, TEXT ) | NSDate | 変換不要 |
BLOB | NSData | 変換不要 |
日時型について
- 2012/4/8 追記
- 日時型に関する説明を間違えていたため説明を加筆修正
- ご指摘いただいた @makoto_kw さん、ありがとうございました
SQLite における日時型は以下のように定義されている。Datatypes In SQLite Version 3 より引用。
- TEXT as ISO8601 strings ("YYYY-MM-DD HH:MM:SS.SSS").
- REAL as Julian day numbers, the number of days since noon in Greenwich on November 24, 4714 B.C. according to the proleptic Gregorian calendar.
- INTEGER as Unix Time, the number of seconds since 1970-01-01 00:00:00 UTC.
DATETIME
という厳密な型があるわけではなく TEXT
、REAL
、INTEGER
で運用する。SQLite クライアントによっては TEXT
しか受け付けなかったり System.Data.SQLite のように接続文字列で DATETIME
型の解釈方法を選択できたりする。FMDB では内部的に Unix Timestamp で運用しているので型を定義するときは INTEGER
にするとよい。
前にこの記事では日時型を REAL
で定義するように説明していた。確かに REAL
でも動作はするのだがデータ量としては INTEGER
で十分である。SQLite の日時型としても REAL
ならユリウス歴として扱うべきであり好ましくない。
そのためサンプルプログラムの方もテーブル定義の日時型を REAL
から INTEGER
に修正しておいた。
サンプル プログラム
FMDB を利用したサンプルとして Core Data の CoreDataBooks 風なプログラムを作成してみた。基本画面は以下のような感じ。
ナビゲーション バー右の追加ボタンを押すとデータを追加できる。既存の本をタップした場合はデータを編集できる。
- 2012/4/8 修正
- UIDatePicker のモードを日時から年月に変更
ナビゲーション バー左の編集ボタンを押すとリスト項目の編集モードになる。項目をタップすると選択されて表示された削除ボタンを押すとテーブルとデータベースの両方からデータが消える。
以下にサンプル プログラムのプロジェクトを公開する。動作確認は iPhone シミュレータ 5.0 でおこなった。
Lita
FMDB を利用したデータベース操作をおこなった時、その結果がどのように反映されたかをチェックしたい時がある。また、アプリで利用する SQL 文を検討するため実際のデータベースに対して SQL を試したくなる。そのような時は Lita が役に立つ。Mac 用 SQLite クライアントの中ではこれが最も使いやすいと思う。
iPhone シミュレータ ver.5.0 で SQLite データベースを作成するとホストしている Mac 上では以下の場所にファイルが作成される。
/Users/USERNAME/Library/Application Support/iPhone Simulator/5.0/Applications/APPID/Documents
作成されたファイルを Lita で開きいてアプリがデータベース操作をおこなった後に Lita 上の表示を更新すれば格納されるデータの変更が掴みやすい。実機なら Organizer 経由でアプリのデータをダウンロードできるので、その中のファイルを参照することになる。
ファイルを参照できるとデバッグにも役立つ。私は特定条件で起きるバグを確認したり意図的に問題が起きるデータベースをアプリが参照するものと置き換える、などの操作をよく行う。
Comments from WordPress
- tera 2011-12-07T02:43:04Z
2週間前からiOSプログラミングに取り組んでいるところです。
とてもわかりやすい解説で、FMDBだけでなく、iOSの勉強にもなりました。
ありがとうございます。 - tera 2011-12-07T04:40:15Z
追伸:
質問です。サンプルプログラムを実行して、データを数件登録してみたのですが、Mac上のどこにapp.dbファイルが作成されたのか分かりません。
解説に書かれてある場所は、フォルダ自体が途中までしかない(/Users/ユーザ名 まではありますが、Libraryがない)のです。
app.dbで全体を検索したのですが、見つかりません。
他にありそうな場所が分かりましたら、教えてくださいませ。 - akabeko 2011-12-07T13:16:01Z
@tera さん、はじめまして。
推測になりますが、tera さんの利用されている Mac の OS は Lion ではないでしょうか?その場合、標準ではライブラリが Finder に表示されないため、以下の記事に紹介されているような方法で表示する必要があります。
Mac Fan.jp:Lionのライブラリフォルダはどこにいった?
http://macfan.jp/guide/2011/07/26/lion_2.html私はライブラリを Finder のサイドバーに登録しています。Finder でライブラリを開き、タイトルバー上のフォルダ アイコンをサイドバーにドラッグ & ドロップすれば登録完了です。こちらの方法については、以下の記事が分かりやすいと思います。
リンゴが好きでぃす♪: ライブラリフォルダへの経路を作っちゃえ (OS X Lion)
http://kjx130.blog19.fc2.com/blog-entry-2789.html - tera 2011-12-08T00:43:01Z
@akabeko さん、ご返信ありがとうございます。
ご推測のとおり、OSはLionです。ちゃんとapp.dbありました。
ライブラリも登録しました。
早速Lita勉強してみます。ありがとうございました。
- teruyakusumoto 2014-01-27T09:01:19Z
最近iOSアプリの勉強を始めた大学生です。
FMDBの使い方はこのページで学ばせていただきました。とても分かりやすかったです。質問なのですが、このアプリを拡張するとして、
テーブルビューのセクションやセルを公開年月日順でソートするとしたら、どのような処理をすればよいのでしょうか。回答いただければ嬉しいです。
- アカベコ 2014-01-27T23:10:26Z
teruyakusumoto さん、はじめまして。
UITableView は UITableViewDataSource を介して、セクションとその中のセル数を取得し、画面上の位置にあわせてセルを表示します。つまり UITableViewDataSource に返すデータをソートし、UITableView に更新通知することになるでしょう。本記事のサンプルで表すと、
1.BooksViewController の authors と books をソート
2.BooksViewController の self.tableView に対して reloadData を実行ソートについては、NSMutableArray 自体を並び替えるか、SQL の ORDER BY を利用して並び替えたデータを再取得する方法が考えられます。前者は自力でソートする必要があるかわりに、データの再取得を回避できます。後者はソートを SQL へ委ねられるかわりに、データ全件を再取得することになります。
- teruyakusumoto 2014-01-28T18:35:29Z
アカベコ様
分かりやすくお早い返信ありがとうございます!
ORDER BY ですぐにできました。
ソートする対象は配列やDBになるのですね。恐縮ですが、ソートに関してもうひとつお聞きしたいです。
配列の中身を独自の基準でソートするにはどうすればよいのでしょうか。
例えば「月」「火」「水」はそのままでは、「月」→「水」→「火」のようにソートされてしまいます。このような文字列を、独自の基準を作ってソートすることは可能でしょうか。
- アカベコ 2014-01-29T15:16:04Z
teruyakusumoto さん、こんばんは。
独自基準のソートですが、SQL の場合は ORDER BY と CASE を組み合わせる方法が考えられます。テーブル名が test、曜日にあたるカラムが day という TEXT とした場合、以下のような感じでしょうか。
SELECT * FROM test ORDER BY CASE day WHEN '月' THEN 1 ELSE 2 END, CASE day WHEN '火' THEN 1 ELSE 2 END, CASE day WHEN '水' THEN 1 ELSE 2 END, CASE day WHEN '木' THEN 1 ELSE 2 END, CASE day WHEN '金' THEN 1 ELSE 2 END, CASE day WHEN '土' THEN 1 ELSE 2 END, CASE day WHEN '日' THEN 1 ELSE 2 END, day ;
Objective-C 側で処理するならば、NSArray - sortedArrayUsingComparator などを利用します。
- teruyakusumoto 2014-01-30T05:18:17Z
アカベコ様
思い通りに動きました!
快く回答していただき感謝です。
今後もこのブログで学ばせていただきます。