iOS で SQLite – FMDB の使い方 2017

2017年1月22日 3 開発 , , ,

2011 年に書いた iOS で SQLite – FMDB の使い方という記事へ現在も結構なアクセスがある。

しかし当時は ARC すらなくサンプルとして古すぎる。なにしろ 5 年も前だし。また最近 iOS アプリ開発に戻ってきたこともあり、2017 年度の開発環境を学ぶ題材としてサンプルを再実装してみた。プロジェクトは GitHub に公開してある。

サンプル再実装における考察などは以下にまとめる。

開発方針

再実装にあたり開発方針をまとめる。

  • サンプル プロジェクトの機能と UI は元記事の内容を踏襲
  • Objective-C と Swift の両方を実装
  • FMDB は CocoaPods で管理
  • ユニット テストを実装する

なるべく最新の Objective-C と Storyboard を使用するのは当然として、Swift 版も実装する。Swift は 2014 年に発表されてから破壊的な変更を繰り返してきたが、Swift 3 で一段落ついたと認識している。

Swift 3の開発の振り返りとSwift 4の計画が記されたメールの紹介 – Qiita と冒頭で紹介されているメーリング リストを読むに、Swift 4 では互換性が重視され Swift 1 〜 3 のような構文レベルの大変更は抑止されるだろう。というわけで今こそ Swift 入門のチャンスと判断した。

サンプルは Objective-C、Swift 共に同等の内容とする。馴染みある Objective-C から先に実装してそれを Swift へ移植。なるべく Swift 的に好ましい機能や記法を採用するがオブジェクトやメソッド定義の変更をともなうレベルの差分は控える。

FMDB のインストールは CocoaPods を採用。元記事では FMDB のソースをプロジェクトにコピーしていたが、この方法だとバージョン管理に難がある。パッケージ管理が利用可能ならそちらへ任せるほうがよい。

あわせて Xcode の提供するユニット テスト機能も試す。

FMDB とは?

iOS アプリ開発において、SQLite を扱いやすくするためのライブラリ。Apple 的には Core Data 推しであり Xcode の GUI からテーブル編集可能などの優遇措置がある。

だが Core Data は SQLite を基本的に隠蔽している。そのため他のプラットフォームで培った SQL 知見を活かすには素で操作したくなる。特に iOS/Android 両対応のアプリを開発する場合、DB 設計と SQL 文を共用したい場面もあるだろう。

しかし SQLite は C 言語で実装されているため、Objective-C や Swift から利用しようとすると API や接続状態の管理が実に厄介だ。FMDB はこの辺をわかりやすく面倒みてくれる。

要は JDBC とか Android でいう SQLiteDatabase、.NET の System.Data.SQLite みたいなものだ。クライアント言語によりそった簡易なデータベース接続と操作 API を提供してくれる。

CocoaPods と FMDB

CocoaPods による FMDB インストールの詳細は CocoaPods を試すにまとめたので、そちらを参照のこと。サンプル プロジェクトのリポジトリには Podfile を定義してあるので、これを clone してから

$ pod install

すれば FMDB をインストールできる。その後に *.xcworkspace ファイルを Xcode で開けば FMDB への参照がプロジェクトに設定された状態となっている。

Xcode で iOS アプリを開発する場合、現在は以下のパッケージ管理を利用できる。これらから CocoaPods を選んだ理由について。

CocoaPods は Objective-C と Swift に両対応している。

Carthage は CocoaPods よりも先に Swift 対応したことで注目を集めたシステムで、機能も簡素である。CocoaPods の不満点を解消するために生まれたらしい。

Swift Package Manager は Swift 用である。Swift は OSS 化されているため iOS アプリ以外の開発でも採用される可能性がある。そのため iOS や macOS とは独立したパッケージ管理となっているようだ。

これらのうち今回は Objective-C から利用可能で知見も多い CocoaPods を選んだ。CocoaPods は 2016/5 に v1.0 がリリースされ、Podfile まわりで互換問題もあったようだが現在は落ち着いている。つまり当面は安定するだろう。これも採用理由である。

CocoaPods でインストール可能な FMDB には複数の仕向けがある。ccgus/fmdbe の CocoaPods 欄から引用。

pod 'FMDB'
# pod 'FMDB/FTS'   # FMDB with FTS
# pod 'FMDB/standalone'   # FMDB with latest SQLite amalgamation source
# pod 'FMDB/standalone/FTS'   # FMDB with latest SQLite amalgamation source and FTS
# pod 'FMDB/SQLCipher'   # FMDB with SQLCipher

今回は FMDB/FTS を採用する。FTS というのは全文検索モジュールのこと。SQLite FTS3 and FTS4 Extensions に詳しい。特別なテーブル内の TEXTMATCH で高速に検索可能となる。

この機能はオプションなので不要ならば使わなくてもよい。おなじみの SQLite へ便利機能が追加された程度であり、普通に使う分には FTS を意識することはないだろう。

DAO と RAII

FMDB はデータベース接続と SQL 文を実行する API を提供するのだが、その知識をカプセル化するため DAO ( Data Access Object ) パターンを採用。FMDB API を利用する DAO クラスと、DAO インスタンスの生成を担当する Factory クラスを用意する。

FMDB におけるデータベースは FMDatabase - databaseWithPath などのメソッドから得られた FMDatabase インスタンスとなる。データベース接続する場合はこのインスタンスに対して open、接続を閉じるなら close メソッドを呼ぶ。

NSString   *path = @"データベース ファイルのパス";
FMDatabase *db = [FMDatabase databaseWithPath:path];
if ([db open]) {

  // ...データベース操作

  [db close];
}

データベース接続を安全に管理するため open/close を対応させる必要がある。簡単なのは対応をメソッドでカプセル化することだ。なにかデータベースを操作したくなったらメソッドを追加し、その中で open/close を完結させる。

しかしこの方法だと複数の操作を連続実行したい場合、その単位でカプセル化するか毎回 open/close することを覚悟してメソッドを複数実行することになる。元記事のサンプルではそうしていた。

今回はこれを避けるため、DAO クラスのインスタンス生成と破棄を open/close に対応させる。Objective-C は init/dealloc、Swift なら init/deinit で open/close を処理。いわゆる RAII ( Resource Acquisition Is Initialization ) 的な管理である。

Objective-C と Swift はインスタンス生成と破棄を明確にハンドリングできるため RAII と相性もよい。ARC ( Automatic Reference Counting ) を利用していても参照カウンター管理を自動化するだけである。インスタンス生成したスコープで参照が完結するなら、そこを抜けたときにカウンターがゼロとなり「インスタンス破棄 = データベース接続を閉じる」ことになる。

例えば以下のように DAO クラスを定義して

- (void)init:(FMDatabase *)db {
    if (!(db)) { return nil; }

    self = [super init];
    if (self) {
        self.db = db;
    }

    return self;
}

- (void)dealloc {
    [self.db close];
}

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
}

- (NSArray *)read {
}

DAO Factory クラスでは

- (BookDAO *)bookDAO {
    return [[BookDAO allo] init:[self connection]];
}

- (FMDatabase *)connection {
    FMDatabase* db = [FMDatabase databaseWithPath:self.dbFilePath];
    return ([db open] ? db : nil);
}

というようにデータベース接続を済ませた FMDatabase を渡してインスタンス生成する。このメソッドから返された DAO インスタンスはスコープを抜けるまで add や read を繰り返しても同じデータベース接続が使いまわされる。

- (void)sample {
    // データベースが open される
    BookDAO *dao = [self.daoFactory bookDAO];

    // DAO を使用したデータベース操作

    // DAO インスタンス破棄 & データベース接続を閉じる
}

イメージとしては上記のような感じ。

Objective-C から FMDB を利用する

FMDB 関連の API を参照する場合は

#import <FMDatabase.h>
#import <FMResultSet.h>

のように import する。CocoaPods でインストールした場合はフレームワーク扱いとなる。面倒なのでフレームワーク名を省略しているが、名前衝突を心配するなら <FMDB/FMDatabase.h> のように記述してもよい。

SQL 文の定義

SQL 文は NSString リテラルとして定義する。FMDB の API 呼び出しで直に書くよりも、定数にしておいたほうが管理しやすいと思われる。この辺の話は NSString 連結を利用して heredoc 風に定数を記述するにまとめた。

定数の位置は DAO クラスの *.m ファイル冒頭にしておく。SQL 文が長く大量にある場合は別ファイルに括りだすのものよいだろう。

CRUD

FMDB API の代表的なものとして FMDatabase はデータベース接続と SQL 文の実行を担当して FMResultSet が処理結果となる。基本、これらだけ覚えれば利用できる。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase - executeUpdate メソッドを使用。これは第一引数に SQL 文となる NSString、それ以降は可変長引数になっていて SQL 文の Placeholder へ対応する。INSERT だとこんな感じ。

static NSString * const kSQLInsert = @""
"INSERT INTO "
  "books (author, title, release_date) "
"VALUES "
  "(?, ?, ?);";

- (Book *)add:(NSString *)author title:(NSString *)title releaseDate:(NSDate *)releaseDate {
    Book *book = nil;
    if ([self.db executeUpdate:kSQLInsert, author, title, releaseDate]) {
        NSInteger bookId = [self.db lastInsertRowId];
        book = [Book bookWithId:bookId author:author title:title releaseDate:releaseDate];
    }

    return book;
}

SELECT では FMDatabase - executeQuery メソッドを利用する。引数の仕様は executeUpdate と一緒。WHERE 句で条件指定する場合は第二引数以降も使用することになるだろう。以下は単純な全行取得の例。

static NSString * const kSQLSelect = @""
"SELECT "
  "id, author, title, release_date "
"FROM "
  "books;"
"ORDER BY "
  "author, title;";

- (NSArray *)read {
    NSMutableArray *books = [NSMutableArray arrayWithCapacity:0];
    FMResultSet    *results = [self.db executeQuery:kSQLSelect];

    while ([results next]) {
        [books addObject:[Book bookWithId:[results intForColumnIndex:0]
                                   author:[results stringForColumnIndex:1]
                                    title:[results stringForColumnIndex:2]
                              releaseDate:[results dateForColumnIndex:3]]];
    }

    return books;
}

戻り値は FMResultSet になる。これはカーソル型のオブジェクトで next メソッドを呼ぶことで取得された行カーソルが進む。next は成否を BOOL で返すため行の列挙は while で処理するとよい。

行カーソルが示す先のデータを取得するのも FMResultSet のメソッドになる。XXXXForColumnIndex 系が SELECT 文に指定された列のインデックス、XXXXForColumn 系は列名を指定して値を取得する。XXXX には返される値の型を示す。

インデックスと名前のどちらで取得するかはお好みで。インデックスは単純だが順番変更に弱い。名前の場合は管理が面倒だが、名前さえ維持できれば順番を意識せずに取得できる。

Swift から FMDB を利用する

Swift でフレームワーク内のクラスを利用する場合は名前空間を import するだけでよい。

import FMDB

非常に楽ちんだ。

SQL 文の定義

Swift でも Objective-C のようにクラスの外周に定数を宣言できるのだが、SQL 文と DAO の関係は密なのでクラス単位の static 定数としておく。

class BookDAO: NSObject {
    private static let SQLCreate = "" +
    "CREATE TABLE IF NOT EXISTS books (" +
      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
      "author TEXT, " +
      "title TEXT, " +
      "release_date INTEGER" +
    ");"

    func create() {
        self.db.executeUpdate(BookDAO.SQLCreate, withArgumentsIn: nil)
    }
}

Swift の String は + で連結されるため、これを利用してインデントをつけている。

CRUD

FMDB API は Objective-C とほぼ共通。

CREATE、INSERT ( UPDATE )、DELETE には FMDatabase.executeUpdate(sql:, withArgumentsIn:) メソッドなどを使用する。第一引数に SQL 文となる String、第二引数の Array が SQL 文の Placeholder に対応づけられる。INSERT だとこんな感じ。

class BookDAO: NSObject {
    private static let SQLInsert = "" +
    "INSERT INTO " +
      "books (author, title, release_date) " +
    "VALUES " +
      "(?, ?, ?);"

    func add(author: String, title: String, releaseDate: Date) -> Book? {
        var book: Book? = nil
        if self.db.executeUpdate(BookDAO.SQLInsert, withArgumentsIn: [author, title, releaseDate]) {
            let bookId = db.lastInsertRowId()
            book = Book(bookId: Int(bookId), author: author, title: title, releaseDate: releaseDate)
        }

        return book
    }
}

SELECT は FMDatabase.executeQuery(sql:, withArgumentsIn:) メソッドとなる。引数の仕様は executeUpdate と一緒。以下は単純な全取得の例。

class BookDAO: NSObject {
    private static let SQLSelect = "" +
    "SELECT " +
      "id, author, title, release_date " +
    "FROM " +
      "books;" +
    "ORDER BY " +
      "author, title;"

    func read() -> Array<Book> {
        var books = Array<Book>()
        if let results = self.db.executeQuery(BookDAO.SQLSelect, withArgumentsIn: nil) {
            while results.next() {
                let book = Book(bookId: results.long(forColumnIndex: 0),
                                author: results.string(forColumnIndex: 1),
                                title: results.string(forColumnIndex: 2),
                                releaseDate: results.date(forColumnIndex: 3))
                books.append(book)
            }
        }

        return books
    }
}

Objective-C に対する特徴的な違いとして Optional 型の扱いがある。

Objective-C の場合、nil なオブジェクトに対し [obj message] 形式でメソッドやプロパティを呼び出すと処理が空振りする。これを利用して nil チェックを避けるテクニックがあるのだが、Swift なら if let で Optional な変数を受ければ配下のスコープで nil 済みの安全なオブジェクトを使用できる。

Objective-C の処理をそのまま Swift へ書き換えただけだと大量のエラーに見舞われて面食らうだろう。しかし慣れてくるとそれらが nil という曖昧な状態を避けるための設計ギプスとして有効であることを理解できる。unwrap まわりの面倒くささは確実に nil への抑止力となるはず。

こうした null/nil 安全については以下に詳しい。

Swift に触れ、私も null/nil 安全について以前より強く意識するようになった。

ユニット テスト

Xcode 標準のユニット テストを試す。

まず FMDB 関連は直に使用しない。DAO クラスのインスタンスは DAO Factory から得られるため、これらに関するものだけに依存する。

Xcode プロジェクトを作成する際に Include Unit Tests と UI Tests をチェックするとテスト用プロジェクトも追加される。前者がユニット テスト、後者は UI の動作をテストするものである。今回はユニット テストだけ使用した。

SQLite のデータベースはファイル単位として管理される。そのためテストを実行する都度、ファイルの存在をチェックして前回実行の結果に影響されぬよう注意が必要。もっと厳密にやるならテスト メソッド単位でファイルを消去するほうが安全だけど、そこまではしない。

Xcode のテストは Objective-C、Swift 共に XCTestCase を継承したテスト クラスを実装する。テスト全体の開始に setUp、終了時は tearDown が呼び出されるため、データベースの生成と破棄や DAO Factory 生成などはこれらに絡めておこなう。

以下は Swift の例。

import XCTest

class BookDAOTests: XCTestCase {
    private let filePath = BookDAOTests.databaseFilePath()
    private var daoFactory: DAOFactory!

    override func setUp() {
        super.setUp()

        self.clean()

        self.daoFactory = DAOFactory(filePath: self.filePath)
        if let dao = self.daoFactory.bookDAO() {
            dao.create()
        }
    }

    override func tearDown() {
        self.clean()
        super.tearDown()
    }

    func clean() {
        let manager = FileManager.default
        if manager.fileExists(atPath: self.filePath) {
            do {
                try manager.removeItem(atPath: self.filePath)
            } catch {
                print("Error: faild to remove database file.")
            }
        }
    }

    private static func databaseFilePath() -> String {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let dir   = paths[0] as NSString
        return dir.appendingPathComponent("test.db")
    }
}

テスト対象にしたいクラスは Xcode 右にあるファイルのプロパティから Target Membership で Tests 系プロジェクトもチェックに含める必要あり。Swift は *.swift、Objective-C なら *.m ファイルをチェックすればよい。

Objective-C の場合、更に XCTestCase 派生クラス側のコードでテスト対象クラスのヘッダーを import しなければならない。ヘッダー管理の必要な言語って面倒だ。

これらの設定をせずにテストを実行しようとすると Tests プロジェクトのビルドでリンク エラーになる。Target Membership はよく忘れるので注意すること。

XCTestCase 派生クラスに testXXXX というメソッドを実装するとそれがテスト対象となる。テストを実行した時の処理順は以下。

  1. setUp
  2. testXXXX
  3. tearDown

実際に DAO クラスのデータ更新処理をテストしてみる。

class BookDAOTests: XCTestCase {
    func testUpdate() {
        if let dao = self.daoFactory.bookDAO() {
            let book = dao.add(author: "author", title: "title", releaseDate: Date())
            XCTAssertNotNil(book)

            // Before
            var books = dao.read()
            XCTAssertEqual(books[0].title, "title")

            // After
            let book2 = Book(bookId: (book?.bookId)!, author: (book?.author)!, title: "title2", releaseDate: (book?.releaseDate)!)
            XCTAssert(dao.update(book: book2))
            books = dao.read()
            XCTAssertEqual(books[0].title, "title2")

            XCTAssert(dao.remove(bookId: (book?.bookId)!))

        } else {
            XCTAssert(false)
        }
    }
}

テストにおける値の妥当性チェックは XCTAssert 系の関数で実施する。値の性質ごとに関数が提供されているので、適切なものを選ぼう。よく使うものとしては以下がある。

関数 機能
XCTAssert 一つの値に対して真であることを判定。偽ならば失敗する。
XCTAssertEqual 二つの値を比較。不一致ならば失敗し、それぞれの値の内容を表示する。Objective-C の場合、NSObject 系の比較には利用できないので注意する。Swift は問題なし。
XCTAssertEqualObjects 二つの値を比較。不一致なら失敗し、それぞれの値の内容を表示する。Objective-C で NSObject 系を比較する場合はこの関数を使用する。Swift は不要なので提供されていないようだ。
XCTAssertNil 一つの値に対して nil であることを判定。nil でなければ失敗する。
Not 系 各種 XCTAssert 関数名に Not のついたもの。例えば XCTAssertNotEqual など。元と判定が逆転する。

NSString を判定する場合、厳密さを意識する必要があるかもしれない。詳しくは以下を参照のこと。

Xcode 8.1 でもこの挙動はそのままである。例にある文字列をテストすると XCTAssertEqualObjects でも不一致となった。ここまで判定したいなら NSString - compare を使用する。

これが問題になるとしたら macOS の HFS+ みたいに Unicode 正規化で「が」を「か」と濁点にわけて管理しているものと、そうでない環境で得られたファイル名やパスの同一性などが考えられる。

データを扱う環境がひとつの系で完結しているとか文字エンコーディングと正規化の系が統一されていればよく、iOS アプリの場合はあまり意識しなくてよさそう。そのため今回のサンプルにおけるテストでは XCTAssertEqualObjects を採用した。

この問題が気になるなら以下の記事も参照のこと。

まとめ

Objective-C と Swift について。

Objective-C については 2 年前まで使用していたのでそれほど変化を感じない。元記事のサンプルと比較したら差分は大きいのだけど、想定どおりである。

一方、Swift は印象的だった。Swift 1 〜 3 までの変遷と混乱をチラ見して大変そうな印象しかなかったのだけど、実際にプログラミングしてみると nil 安全まわりが実によい。

基本的に nil を抑止する方針であること、使わざるを得ないときは Optional 型として明示される点が気に入っている。これを経験したことにより、null/nil 安全のない言語で書くときの設計も影響を受けるだろう。

ユニット テストについて。

他のプラットフォームにおける Power Assert 系のように XCTAssert も普通の比較でも失敗時に値の詳細を表示してほしい。用途ごとに関数を使い分けるのは面倒だ。いちおう Swift には keygx/PAssert があるのだけど、Power Assert 的なものは標準にしてもよいのではないか。

あと Cocoa Touch を使用するから仕方ないことだけど、テストに iOS シミュレーターや実機を要するため実行が遅い。スクリプト言語の気軽なテストに慣れていると重く感じる。

FMDB について。

細かな改善はたくさん反映されているのだろうけど、使用感は 5 年前と変わらず。これはよいことだ。普通に設計したらこうなるよね、という期待に答える直感的な API である。

以前よりサンプルの古さが気になっていたので、書き直せてスッキリした。

以下、余談。

この記事とサンプルは 2016 年末に書き終えていたのだけど Swift コードを構文強調する手段に WP Code Highlight.js を使うためブログ全体の Markdown 移行をしているうちに 2017 年へずれ込んでしまった。

結局 WP Code Highlight.js は行間へ余計な br タグを自動挿入するのを防げなくて導入は見送った。仕方ないので SyntaxHighlighter Evolved: Swift Brush を導入して構文強調している。

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

次に 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 のモードを日時から年月に変更

新規作成・編集画面

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

データの削除

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

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

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