iOS で SQLite – FMDB の使い方

2011年11月20日 14 開発 , , , ,

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 ライブラリの追加

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 を実行すると、その都度、BEGIN ~ COMMIT が実行されるため、速度が大幅に低下する。そのような処理をおこなう場合は、対象となる区間を明示的にトランザクション処理する方がよい。

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 のモードを日時から年月に変更。
新規作成・編集画面

新規作成・編集画面

ナビゲーションバー左の編集ボタンを押すと、リスト項目の編集モードになる。項目をタップすると選択され、表示された削除ボタンを押すと、テーブルとデータベースの両方からデータが消える。

データの削除

データの削除

以下にサンプル プログラムのプロジェクトを公開する。

サンプル プロジェクト
TestFMDB2.zip 32.6KB

iPhone シミュレータ 5.0 で動作確認をおこなった。

Lita

FMDB を利用したデータベース操作をおこなった時、その結果がどのように反映されたかをチェックしたい時がある。また、アプリで利用する SQL 文を検討するために、実際のデータベースに対して SQL を実行したいケースはかなり多い。

そのような時は、Lita が役に立つ。Mac 用 SQLite クライアントの中では、これが最も使いやすいと思う。

iPhone シミュレータ ver.5.0 で SQLite データベースを作成した場合、Mac 上の以下の場所にファイルが作成される。

/Users/ユーザー名/Library/Application Support/iPhone Simulator/5.0/Applications/アプリの ID/Documents

作成されたファイルを Lita で開き、アプリがデータベース操作をおこなった後に Lita 上の表示を更新すれば、格納されるデータの変更が掴みやすい。実機の場合は Organizer 経由でアプリのデータをダウンロードできるので、その中のファイルを参照することになる。

Lita

Lita

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


TRACKBACKS

COMMENTS

  • tera
    2011年12月7日 11:43 AM 返信

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

  • tera
    2011年12月7日 1:40 PM 返信

    追伸:
    質問です。
    サンプルプログラムを実行して、データを数件登録してみたのですが、Mac上のどこにapp.dbファイルが作成されたのか分かりません。
    解説に書かれてある場所は、フォルダ自体が途中までしかない(/Users/ユーザ名 まではありますが、Libraryがない)のです。
    app.dbで全体を検索したのですが、見つかりません。
    他にありそうな場所が分かりましたら、教えてくださいませ。

  • 2011年12月7日 10:16 PM 返信

    @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月8日 9:43 AM 返信

    @akabeko さん、ご返信ありがとうございます。
    ご推測のとおり、OSはLionです。
    ちゃんとapp.dbありました。
    ライブラリも登録しました。
    早速Lita勉強してみます。
    ありがとうございました。

  • 2014年1月27日 6:01 PM 返信

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

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

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

    • 2014年1月28日 8:10 AM 返信

      teruyakusumoto さん、はじめまして。

      UITableView は UITableViewDataSource を介して、セクションとその中のセル数を取得し、画面上の位置にあわせてセルを表示します。つまり UITableViewDataSource に返すデータをソートし、UITableView に更新通知することになるでしょう。本記事のサンプルで表すと、

      1.BooksViewController の authors と books をソート
      2.BooksViewController の self.tableView に対して reloadData を実行

      ソートについては、NSMutableArray 自体を並び替えるか、SQL の ORDER BY を利用して並び替えたデータを再取得する方法が考えられます。前者は自力でソートする必要があるかわりに、データの再取得を回避できます。後者はソートを SQL へ委ねられるかわりに、データ全件を再取得することになります。

      • 2014年1月29日 3:35 AM 返信

        アカベコ様

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

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

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

        • 2014年1月30日 12:16 AM 返信

          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 などを利用します。

          • 2014年1月30日 2:18 PM

            アカベコ様

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

REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です