2012年4月25日水曜日

iOSでSQLiteを使う(FMDB)

SQLiteのライブラリはC言語のライブラリとして提供されています。したがって、SQLiteの各関数の呼び出しや引数の指定方法、パラメータの型はC言語の文法・型に従う必要があります。

特にC言語への型変換に関して、DBの場合は数値(整数、浮動小数点数)、文字列、バイナリ、日付等様々な型があり、Cocoa TouchからC言語への型変換をSQL実行時と結果取得時に行う必要があるため、結構面倒だったりします。

そんなこともあってか、iOSのSQLiteラッパーライブラリとしてFMDBというライブラリがあり、このライブラリはこの面倒な型変換を全て受け持ってくれるため、この点だけでもFMDBを使う魅力は大いにあると思います。(注:後述しますが、FMDBは型変換だけでは無いです)
また、FMDBはARC有効/無効どちらにも対応しています。プリプロセッサでARCの有効状態を判定しているみたいですね。

素でいくかFMDBを利用するかの方針決定
 私は素でSQLiteを使う方法とFMDBを使う方法を試しましたが、やはりFMDBを使ったほうがコードが簡単になりますし、なによりもCocoa Touchのクラスを直接使うことが可能である点とマルチスレッド用のクラスやDBプーリングのクラスも提供されていることからFMDBを素直に使用したほうが結局のところ効率が良いと感じました。
ちなみにFMDBのライセンスはMITライセンスです。
以降はFMDBを使用することを前提としています。
FMDBのダウンロード
=========== 2014/3/31 追記 ===================
FMDBのインストールについて追記します。
以下、本記事に沿ってインストールしても問題はありませんが、CocoaPodsでインストールされることをオススメします。 OS Xはデフォルトでrubyがインストールされているので、以下コマンドですぐに利用できます。
・CocoaPodsのインストール
$ sudo gem install cocoapods
・FMDBのインストール
//FMDBをインストールしたいあなた自身が作成したプロジェクト配下へ移動します
$ cd [インストールしたいあなたのプロジェクト]
$ vi Podfile
Podfileを作成する。中身は以下。最新版をインストールするなら以下で良いでしょう。
pod 'FMDB'
・インストール実行
$ pod install
xcodeでワークスペースとして起動する。
$open YourApp.xcworkspace
するか Finderで拡張子がxcworkspaceとなっている方を起動すると 既にFMDBがインストールされた状態となっているはずです。後はヘッダファイルを引き込めばOKですね。 因みに、CocoaPodsでインストールした場合は本記事の「FMDBのダウンロード」と「SQLiteのライブラリ取り込みとFMDBラッパークラスファイルの取り込み」の章はスキップして下さい。
Reference : CocoaPods.
=============== 追記終わり ==================
 FMDBはライブラリと言いましたが、ソースコードとして提供されているので、ダウンロードして必要なファイルをプロジェクトに追加します。
 ソースはgithubで公開されています。https://github.com/ccgus/fmdbブラウザでここを開いて直接ZIPファイルをダウンロードしても良いですし、Xcodeをインストールしているのであれば恐らくgitコマンドがインストールされていると思われるので以下の様にコマンドでgithubのFMDBプロジェクトのcloneをローカルに作成しても良いと思います。

 gitを使う場合コマンドプロンプトを起動して、以下を実行します。実行カレントディレクトリ配下にfmdbのディレクトリが作成されます。
%git clone https://github.com/ccgus/fmdb.git
 Xcode用のプロジェクトファイルもあるのでそのままXcodeで起動してコンパイル確認してみても良いですね。またテストコード用のソースコード(fmdb.m)も付属されており、このソースはFMDBを利用するに大変参考になります。
SQLiteのライブラリ取り込みとFMDBラッパークラスファイルの取り込み

・SQLiteのライブラリを追加

・FMDBのソースファイルをプロジェクトに追加
FMDB関連ファイルを格納するGroupフォルダを作成して格納した方がスッキリする。
DBファイルの事前作成とDBファイルのプロジェクト取り込み
 SQLiteのDBファイルは事前に作成して良いし、アプリケーション上で作成しても良いです。今回は事前に作成しています。
コマンドプロンプトで以下を実行し、初期スキーマを生成しテーブルを一つ作成します。
MacBook-Pro:tmp naoyuki$ sqlite3 sample.db 
SQLite version 3.7.7 2011-06-25 16:35:41Enter ".help" for instructionsEnter SQL statements terminated with a ";"
sqlite>create table example (i integer, n numeric, t text, r real);
sqlite> .schema
CREATE TABLE example (i integer, n numeric, t text, r real);
sqlite> .quit
MacBook-Pro:tmp naoyuki$ 

スキーマはこんな感じです。
CREATE TABLE example (
        i integer,
        n numeric,
        t text,
        r real
);
 前述で作成したDBファイル(sample.db)をプロジェクトに取り込みます。プロジェクトへの取り込みはメニューの「File」-「Add Files to XXX」で表示されるダイアログから該当ファイルを選択してプロジェクトへ取り込みを行います。
初回起動時はDBファイルをコピーしてからDBオープン
 ここまで長くなりましたがやっとここからコードを書きます。
まず初回時はDBスキーマが格納されているDBファイルを/Documents/配下へコピーする必要があります。このへんの詳細は、 Resource Bundle/リソースバンドルiOS 標準ディレクトリパスの取得あたりを参照してもらえると理解出来るとおもいます。
 以下、サンプルコードです。Document配下にDBファイルが無い場合はアプリケーションディレクトリからDBファイルをコピーしてからDBをオープンしています。
//DBファイル名
static NSString* const DB_FILE = @"sample.db";

@implementation SqliteExecute {
    FMDatabase*     _db;
}

- (BOOL)openDatabase {
    //DBファイルへのパスを取得
    //パスは~/Documents/配下に格納される。
    NSString *dbPath = nil;
    NSArray *documentsPath = NSSearchPathForDirectoriesInDomains
                                (NSDocumentDirectory, NSUserDomainMask, YES);
    //取得データ数を確認
    if ([documentsPath count] >= 1) {
        //固定で0番目を取得でOK
        dbPath = [documentsPath objectAtIndex:0];
        //パスの最後にファイル名をアペンドし、DBファイルへのフルパスを生成。
        dbPath = [dbPath stringByAppendingPathComponent:DB_FILE];
        NSLog(@"db path : %@", dbPath);
    } else {
        //error
        NSLog(@"search Document path error. database file open error.");
        return false;
    }
    
    //DBファイルがDocument配下に存在するか判定
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:dbPath]) {
        //存在しない
        //デフォルトのDBファイルをコピー(初回のみ)
        //ファイルはアプリケーションディレクトリ配下に格納されている。
        NSBundle *bundle = [NSBundle mainBundle];
        NSString *orgPath = [bundle bundlePath];
        //初期ファイルのパス。(~/XXX.app/sample.db)
        orgPath = [orgPath stringByAppendingPathComponent:DB_FILE];
        
        //デフォルトのDBファイルをDocument配下へコピー
        if (![fileManager copyItemAtPath:orgPath toPath:dbPath error:nil]) {
            //error
            NSLog(@"db file copy error. : %@ to %@.", orgPath, dbPath);
            return false;
        }
    }
    
    //open database with FMDB.
    _db = [FMDatabase databaseWithPath:dbPath];
    return [_db open];
}
更新処理
FMDBのメソッドは大きく分けて更新系のメソッドと参照系のメソッドに分かれています。

  • 更新系:- (BOOL)executeUpdate:(NSString*)sql, ...;
  • 参照系:- (FMResultSet *)executeQuery:(NSString*)sql, ...;

  •  上記メソッドは代表的なもので、この他にも引数が異なる形でいくつか提供されています。
    以下、更新系のサンプルです。トランザクションを開始している点とステートメント再利用フラグを有効にしている点に注意して下さい。
    - (BOOL)executeUpdate1 {
        BOOL result = TRUE;
        //トランザクション開始(exclusive)
        [_db beginTransaction];
        
        //ステートメントの再利用フラグ
        //おそらくループ内で同一クエリの更新処理を行う場合バインドクエリの準備を何回
        //も実行してしまうのためこのフラグを設定する。
      //このフラグが設定されているとステートメントが再利用される。
        [_db setShouldCacheStatements:YES];
        
        
        //insertクエリ実行(プリミティブ型は使えない)
    //    [_db executeUpdate:@"insert into example values (?, ?, ?, ?)",
    //                                1, 2, @"test", 4.1];
    // executeUpdateWithFormatメソッドで可能。
    
        for (int i=1; i<=30; i++) {
            [_db executeUpdate:@"insert into example values (?, ?, ?, ?)",
                                [NSNumber numberWithInt:i],
                                [NSDate date],
                                [NSString stringWithFormat:@"string : %d.", i],
                                [NSNumber numberWithFloat:(float)i/3]];
            //check
            if ([_db hadError]) {
                result = FALSE;
                NSLog(@"Err %d: %@", [_db lastErrorCode], [_db lastErrorMessage]);
            }
        }
        
        //commit
        [_db commit];
        
        return result;
    }
    
    
    トランザクションは2種類のモードが提供されており、以下メソッドが提供されています。
    - (BOOL)beginTransaction;
    - (BOOL)beginDeferredTransaction;

    ・beginTransaction
    begin exclusive transaction を指定してトランザクションを開始。
    これは読み取り、書き込み共に排他されるロックモードです。
    通常1スレッドで処理を行うのであれば、このモードで問題ありません。

    ・beginDeferredTransaction
    begin deferred transaction を指定してトランザクションを開始。
    これは必要に応じて最初のアクセス時にロックが働き、読み取りの場合は共有ロックで書き込みの場合は予約ロックがかけられるようです。因みにSQLiteのデフォルトロックモードは「Deferred」です。
    参照処理
    前述で登録したレコードを参照してみます。

    ・結果の取得をインデックスで行う場合。
    - (NSArray*)executeQuery1 {
        //結果格納用配列
        NSMutableArray *result = [[NSMutableArray alloc] init];
        
        //クエリの実行と結果(ResultSet)の取得
        FMResultSet *rs = [_db executeQuery:@"select * from example"];
        
        //結果の取得(indexで取得)
        while ([rs next]) {
            //結果格納用のオブジェクト
            ExampleRecord *record = [[ExampleRecord alloc]init];
            record.columnOfInteger = [rs intForColumnIndex:0];
            record.columnOfDate = [rs dateForColumnIndex:1];
            record.columnOfText = [rs stringForColumnIndex:2];
            record.columnOfReal = [rs doubleForColumnIndex:3];
            
            //配列に格納
            [result addObject:record];
        }
        //close ResultSet.
        [rs close];
        
        return result;
    }
    
    

    ・結果の取得をカラム名で行う場合
    - (NSArray*)executeQuery2 {
        //結果格納用配列
        NSMutableArray *result = [[NSMutableArray alloc]init];
        
        
        //クエリ実行
        FMResultSet *rs = [_db executeQuery:@"select * from example where i = ?", 
                           [NSNumber numberWithInt:2]];
    //    FMResultSet *rs = [_db executeQuery:@"select * from example where t like ?", @"%2."];
    
        if ([_db hadError]) {
            NSLog(@"Err %d: %@", [_db lastErrorCode], [_db lastErrorMessage]);
        }
        
        //結果の取得(カラム名指定)
        while ([rs next]) {
            //結果格納用オブジェクト
            ExampleRecord *record = [[ExampleRecord alloc]init];
            record.columnOfInteger = [rs intForColumn:@"i"];
            record.columnOfDate = [rs dateForColumn:@"n"];
            record.columnOfText = [rs stringForColumn:@"t"];
            record.columnOfReal = [rs doubleForColumn:@"r"];
            
            [result addObject:record];
        }
        //close ResultSet.
        [rs close];
        
        return result;
    }
    
     因みに、上のコードで登場しているExampleRecordというクラスは自前のクラスでレコードデータを格納する クラスです。レコードクラスは無理に作成する必要はありませんが、参考までに載せておきます。

    ・ExampleRecord.h
    @interface ExampleRecord : NSObject {
        int         i_;
        NSDate      *d_;
        NSString    *t_;
        double      r_;
    }
    
    // I'm using ARC.
    @property (nonatomic) int columnOfInteger;
    @property (nonatomic) NSDate* columnOfDate; 
    @property (nonatomic, copy) NSString* columnOfText;
    @property (nonatomic) double columnOfReal;
    
    @end
    

    ・ExampleRecord.m
    #import "ExampleRecord.h"
    
    @implementation ExampleRecord
    @synthesize columnOfInteger = i_;
    @synthesize columnOfDate = d_;
    @synthesize columnOfText = t_;
    @synthesize columnOfReal = r_;
    
    @end
    
    DBの解放
    - (void)closeDatabase {
        if (_db) {
            [_db close];
        }
    }
    

    ちょっと長くなってしまったので、続きは次回ということで。スレッド絡みは気になる所なので、そのあたりができたらと考えています。
    マルチスレッド上での使用を考慮したFMDatabaseQueueについては iOSでSQLiteを使う2(FMDBのFMDatabaseQueueクラスを使ってみる)の方に書いています。

    =========== 2014/4/22 追記 ===================
    FMDBを利用したサンプルプロジェクトをGitHubに上げていますので、参考にしてください。別件で作成したプロジェクトなので、若干本記事と異なる部分がありますが、初期化、参照、更新、バックグラウンド更新の処理があります。簡単なTableViewにAlertViewから入力した文字を表示するだけのアプリです。
    GitHub : https://github.com/takuran/FMDBExample
    $ git clone https://github.com/takuran/FMDBExample.git
    $ cd FMDBExample
    $ git submodule update --init --recursive
    して下さい。
    スクリーンショット:
    削除はスワイプでね。
    =============== 追記終わり ==================

    5 件のコメント :

    1. こんにちは。objective-c初心者です。こちらとても参考にさせていただいています。
      質問なのですが、結果をカラム名で取得した後、その結果(配列:NSMutablearray)をNSLogに表示させたい場合はどうすべきかおしえていただくことはできますでしょうか。
      いくら調べてもできませんでした。

      返信削除
      返信
      1. こんにちわ。ブログ見て頂いて有難うございます。

        サンプルコードでDBから取得した結果(result)はNSMutableArray型の配列ですので、
        NSLogで出力するには通常以下のようにします。

        NSLog(@"%@", result);

        残念なことに、表示は以下のようになるはずです。
        ---
        2014-01-15 22:03:00.589 FMDBTest[13387:70b] (
        "",
        "",
        ""
        )
        ---

        これは、独自クラスであるExampleRecordを配列に格納しているからです。
        独自クラスの場合はNSObjectプロトコルの以下メソッドをオーバーライド
        することでNSLogでも必要な情報を出力するようにすることが出来ます。

        - (NSString *)description

        詳細はクラスリファレンスを参照していただきたいですが、まあクラスの
        文字列表現を返却するメソッドと考えてもらってい良いです。

        ExampleRecord.m に以下コードを追加します。
        ---
        - (NSString*) description {
        return [NSString stringWithFormat:@"%@, %d, %@, %@, %f", [self class], i_, d_, t_, r_];
        }
        ---

        するとNSLogの結果は以下の様になります。
        ---
        2014-01-15 22:15:57.367 FMDBTest[13450:70b] (
        "ExampleRecord, 2, 2014-01-15 12:55:19 +0000, string : 2., 0.666667",
        "ExampleRecord, 12, 2014-01-15 12:55:19 +0000, string : 12., 4.000000",
        "ExampleRecord, 22, 2014-01-15 12:55:19 +0000, string : 22., 7.333333"
        )
        ---

        質問の回答になってますかね。

        削除
    2. 詳しく説明いただきありがとうございます。
      その通りに実行しましたらできました。ほぼ理解できました。ありがとうございます。

      すみません、疑問点がありまして、初歩的な質問なのですが教えてください。

      今回のNSLogへの表示方法ですと、すべての文字列を取り出しました。
      (例えば[record objectAtIndex:0]だと、1行目のすべてのデータが出てしまう。)

      その時、たとえば列名「i」「d」をそれぞれ表示させたい場合はどのような表記方法になるのでしょうか?(参考URLでももちろん構いません)

      イメージとしては、NSLogでいうと、
      NSLog(@"%@",record[i][0]);
      NSLog(@"%@",record[d][0]);

      のような感じで個別に取り出したいと考えています。

      または、例えばipadのような実機に表示させる場合など、別に方法があるのでしょうか。

      根本的なことでいうと、自作クラスを配列に格納する意味というのが、まだよく理解できていません。
      何度もすみません。お時間のある時に教えていただけましたら幸いです。

      返信削除
      返信
      1. こんにちは。理解されたようで良かったです。

        検索結果のレコードの特定のカラムの値を取得したい場合はプロパティで値を取得してから出力すれば良いです。
        例えば以下コードは全配列の各カラムの値を個別に出力します。
        ---
        //結果確認
        for (ExampleRecord* record in result) {
        NSLog(@"Integer column : %d", record.columnOfInteger);
        NSLog(@"Date column : %@", record.columnOfDate);
        NSLog(@"Text column : %@", record.columnOfText);
        NSLog(@"Real column : %f", record.columnOfReal);
        }
        ----

        添え字を使用して配列にアクセスしたい場合は、以下のように。
        ---
        ExampleRecord *record = result[0];
        NSLog(@"%@", record.columnOfText);
        ---

        配列の内容を出力する形式は他にもありますので、ご自身で調べてみてください。
        #「Objective-C 配列」で検索すれば参考になる情報がたくさんあります。

        >または、例えばipadのような実機に表示させる場合など、別に方法があるのでしょうか。

        NSLogはあくまでログ出力ですので、画面上にデータを表示させたい場合は、アプローチが異なってきます。
        以下のサイトではFMDBで取得した結果を画面へ表示されていますので、参考にしてみては如何でしょうか。
        iOS で SQLite – FMDB の使い方

        >根本的なことでいうと、自作クラスを配列に格納する意味というのが、まだよく理解できていません。

        本サンプルでは結果を独自クラスに格納していますが、一つの実装方法であり必ず独自クラスを作成する必要はありません。独自クラスでなくても辞書クラス(NSDictionary)を使用してカラム名をキーに値を格納しても良いと思いますよ。

        削除
    3. 詳しく説明いただきありがとうございます。NSLogと実機では仕組みが異なるのですね。
      そこが分かっていませんでした。
      NSDictionaryでコードを書き途中ですが、実現できそうでした。

      また、画面上のデータ表示について、ご提示いただいた参考サイトのサンプルコードを現在解析中です。仕事場をいったん離れることになってしまったため、取り急ぎご報告させていただきました。
      この度は、何度も質問に答えてくださりありがとうございました。今後も参考にさせていただきます。

      返信削除