アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

iOS で SQLite - FMDB の使い方

November 20, 2011開発FMDB, iOS, iPhone, Lita, SQLite

iOS で SQLite を簡単に扱うためのライブラリ FMDB についてまとめる。

FMDB とは?

FMDB は SQLite を iOS の Objective-C で扱いやすくするための Wrapper ライブラリ。 GitHub で公開されている。

インターフェースや使用感は JDBC や ADO.NET に近い。よってこれらを利用したことがあればスムーズに理解できるだろう。

FMDB の仕様準備

まず FMDB を利用したいプロジェクトで SQLite 用のライブラリを有効にする。手順は以下。

  1. Xcode 左ペインのナビゲーションからプロジェクトを選択
  2. 右ペインに PROJECT と TARGET が表示されるので後者を選択
  3. 右ペイン上部のタブから Build Phases を選択
  4. ビルドに関する設定項目が表示されるので、その中の Link Binary With Libraries を選択
  5. Link Binary With Libraries 左下に +- ボタンがあるので + をクリック
  6. Choose framework and libraries add: というダイアログが表示される
  7. フレームワークとライブラリ一覧の中から libsqlite3.0.dylib を選択、ダイアログ上部の検索窓で絞り込みも可能
  8. ダイアログ右下の Add ボタンをクリック

文章だと分かりにくいのでスクリーンショットに手順を記載してみた。

SQLite ライブラリの追加

次に FMDB のソースをプロジェクトへ組み込む。

  1. FMDB のプロジェクト ページ から ZIP 形式のプロジェクトを入手
  2. ZIP を展開
  3. 展開されたフォルダ内にある src フォルダを開く
  4. フォルダ内のファイルで fmdb.m 以外を、自身のプロジェクト フォルダへコピー

    • fmdb.m はサンプルとテストを兼ねたソース
    • fmdb.m には main 関数が実装されているので、これをプロジェクトに組み込むとコンパイル エラーになる
    • プロジェクトに組み込む場合、サブ フォルダを用意したほうがよい
    • 私の場合、lib/FMDB に格納している
  5. Xcode から FMDB のソースを参照する
  6. ビルドが通ることを確認する

以上で 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 を実行すると都度 BEGINCOMMIT が実行されるため速度が大幅に低下する。そうした処理をおこなう場合は対象区間を明示的にトランザクション処理して効率化する。

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。それぞれカラム名と位置インデックスで値を取得できる。例えば stringForColumnname を指定したなら name という文字列 TEXT カラムが得られる。intForColumnIndex4 を指定した場合は 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 なら NSStringINTEGER なら 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 という厳密な型があるわけではなく TEXTREALINTEGER で運用する。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 経由でアプリのデータをダウンロードできるので、その中のファイルを参照することになる。

Lita

ファイルを参照できるとデバッグにも役立つ。私は特定条件で起きるバグを確認したり意図的に問題が起きるデータベースをアプリが参照するものと置き換える、などの操作をよく行う。

Comments from WordPress

  • tera tera 2011-12-07T02:43:04Z

    2週間前からiOSプログラミングに取り組んでいるところです。
    とてもわかりやすい解説で、FMDBだけでなく、iOSの勉強にもなりました。
    ありがとうございます。

  • tera tera 2011-12-07T04:40:15Z

    追伸:
    質問です。

    サンプルプログラムを実行して、データを数件登録してみたのですが、Mac上のどこにapp.dbファイルが作成されたのか分かりません。

    解説に書かれてある場所は、フォルダ自体が途中までしかない(/Users/ユーザ名 まではありますが、Libraryがない)のです。

    app.dbで全体を検索したのですが、見つかりません。
    他にありそうな場所が分かりましたら、教えてくださいませ。

  • akabeko 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 tera 2011-12-08T00:43:01Z

    @akabeko さん、ご返信ありがとうございます。
    ご推測のとおり、OSはLionです。

    ちゃんとapp.dbありました。
    ライブラリも登録しました。
    早速Lita勉強してみます。

    ありがとうございました。

  • teruyakusumoto teruyakusumoto 2014-01-27T09:01:19Z

    最近iOSアプリの勉強を始めた大学生です。
    FMDBの使い方はこのページで学ばせていただきました。とても分かりやすかったです。

    質問なのですが、このアプリを拡張するとして、
    テーブルビューのセクションやセルを公開年月日順でソートするとしたら、どのような処理をすればよいのでしょうか。

    回答いただければ嬉しいです。

  • akabeko アカベコ 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 teruyakusumoto 2014-01-28T18:35:29Z

    アカベコ様

    分かりやすくお早い返信ありがとうございます!
    ORDER BY ですぐにできました。
    ソートする対象は配列やDBになるのですね。

    恐縮ですが、ソートに関してもうひとつお聞きしたいです。

    配列の中身を独自の基準でソートするにはどうすればよいのでしょうか。
    例えば「月」「火」「水」はそのままでは、「月」→「水」→「火」のようにソートされてしまいます。

    このような文字列を、独自の基準を作ってソートすることは可能でしょうか。

  • akabeko アカベコ 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 teruyakusumoto 2014-01-30T05:18:17Z

    アカベコ様

    思い通りに動きました!
    快く回答していただき感謝です。
    今後もこのブログで学ばせていただきます。