[objective-c] FMDatabaseの使い方メモ

ここ2、3週間久々にiphoneを弄っているのですが、GCが入ってない環境でのプログラミングはやらないと衰えるんですね。LLに慣れると辛いです。細かい事にまで気を使う必要があるので疲れますね。ブログ書きながらリハビリしようと思います。
さてiPhoneではバックエンドにSQLiteを使えるんですが、そのラッパーのFMDatabaseの使い方をまとめておきます。

インストールとプロジェクト設定はこちらのページを参考にさせて頂きました。ソースをコピーして、プロジェクトにライブラリを追加しましょう。

今回このインターフェースを使って行う処理は、概ね以下のSQLの通りです。テーブル作成後、CRUD操作を一通り行っています。

-- CREATE TABLES
CREATE TABLE authors(
    id   INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT
);

CREATE TABLE books(
    id        INTEGER PRIMARY KEY AUTOINCREMENT,
    title     TEXT,
    author_id INTEGER,
    score     INTEGER
);

-- INSERT 
INSERT INTO authors(id, name) VALUES(1, '村上春樹');
INSERT INTO authors(id, name) VALUES(2, 'ポールグレアム');

INSERT INTO books(author_id, title) VALUES(1, '1Q84');
INSERT INTO books(author_id, title) VALUES(1, 'ねじまき鳥クロニクル');
INSERT INTO books(author_id, title) VALUES(2, 'ハッカーと画家');
INSERT INTO books(author_id, title) VALUES(2, 'On Lisp');

-- SELECT / UPDATE
SELECT * FROM books, authors WHERE books.author_id = authors.id;
UPDATE books SET score = 5 WHERE title = '1Q84';
UPDATE books SET score = 4 WHERE title = 'ハッカーと画家';
SELECT * FROM books, authors WHERE books.author_id = authors.id;

-- DELETE
DELETE FROM authors;
DELETE FROM books;

早速実装してみたコードが以下になります。必要な部分のみ抜粋しています。(ソースがそのまま動作可能ではないのも、LLに比べてストレスがたまりますね。。) CRUD操作をそれぞれ別のメソッドとしています。またインスタンスを取得(正確にはファイルパスの取得)するのが割と面倒なので、_getDBとして括りだしています。

特に他のラッパーライブラリと比べても、変わったところはなく抵抗無く使えると思いますが、僕がハマった点としてはexecuteQuery, executeUpdateで使用するプレースホルダーに入れる値は、オブジェクト型だと言う事です。Cの文字列を指定してしまい、BAD_ACCESSでしばらく悩みました。

あと、SELECT COUNT(*)を使った時のカラム名は、COUNT(*)になります。他の集約関数を使った時もSQL中に書いた表現がそのままカラム名になります。醜いのでASを使った方が良いかと思います。

#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
#define DBFILE     @"fmdb_test.db"

// データベースインスタンスを返す
- (FMDatabase*)_getDB:(NSString*)dbName {
	NSArray*  pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString* docdir = [pathArray objectAtIndex:0];
	NSString* dbpath = [docdir stringByAppendingPathComponent:dbName];
	FMDatabase* db = [FMDatabase databaseWithPath:dbpath];
    if (![db open]) {
        @throw [NSException exceptionWithName:@"DBOpenException" reason:@"couldn't open specified db file" userInfo:nil];
    }

    return db;
}

// テーブル作成
- (IBAction)createTable {
    FMDatabase* db = [self _getDB:DBFILE];
    NSDictionary* tables = [NSDictionary dictionaryWithObjectsAndKeys:
        @"CREATE TABLE authors(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
        @"authors",
        @"CREATE TABLE books(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, author_id INTEGER, score INTEGER)",
        @"books",
        nil
    ];

    for (NSString* tableName in tables) {
        if (![db tableExists:tableName]) {
            if (![db executeUpdate:[tables objectForKey:tableName], nil]) {
                NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
            }
        }
    }

    // table already exists error を起こしてみる
    for (NSString* tableName in tables) { 
        if (![db executeUpdate:[tables objectForKey:tableName], nil]) {
            NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]); // error codeは1
        }
    }
    
    [db close];
}

// レコードの挿入
- (IBAction)insert {
    FMDatabase* db = [self _getDB:DBFILE];

    NSArray* fixture = [NSArray arrayWithObjects:
                             [NSDictionary dictionaryWithObjectsAndKeys:
                                           @"村上春樹", @"author",
                                           [NSNumber numberWithInt:1], @"id",
                                           [NSArray arrayWithObjects:@"1Q84", @"ねじまき鳥クロニクル", nil],
                                           @"books",
                                           nil
                             ],
                             [NSDictionary dictionaryWithObjectsAndKeys:
                                           @"ポールグレアム", @"author",
                                           [NSNumber numberWithInt:2], @"id",
                                           [NSArray arrayWithObjects:@"ハッカーと画家", @"On Lisp", nil],
                                           @"books",
                                           nil
                             ],
                             nil
        ];

    for (NSDictionary* item in fixture) {
        NSNumber* author_id = [item objectForKey:@"id"];
        NSString* author    = [item objectForKey:@"author"];
        NSArray*  books     = [item objectForKey:@"books"];
        if ([db executeUpdate:@"INSERT INTO authors(id, name) VALUES (?, ?)" , author_id, author]) {
            for (NSString* book in books) {
                // its too ugly, COUNT(*) as cnt should be used in SQL
                FMResultSet* rs = [db executeQuery:@"SELECT COUNT(*) FROM books WHERE author_id = ? and title = ?", author_id, book];
                if ([rs next] && [rs intForColumn:@"COUNT(*)"] == 0) {
                    [db executeUpdate:@"INSERT INTO books(title, author_id) VALUES (?, ?)" , book, author_id];
                }
                [rs close];
            }
        }
        else {
            // if duplicated, error code 19 will return (Constraint Error)
            NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
        }
    }

    [db close];
}

// レコードの取得
- (IBAction)select {
    FMDatabase* db = [self _getDB:DBFILE];
    FMResultSet* rs = [db executeQuery:@"SELECT * FROM authors, books WHERE authors.id = books.author_id"];
    while ([rs next]) {
        NSLog(@"%d %@ %d ",
              [rs intForColumn:@"id"],
              [rs stringForColumn:@"title"],
              [rs intForColumn:@"score"]);
    }
    [rs close];
    [db close];
}

// レコードの更新
- (IBAction)update {
    FMDatabase* db = [self _getDB:DBFILE];
    if (![db executeUpdate:@"UPDATE books SET score = 5 WHERE title = ?", @"1Q84"]) {
        NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
    }
    if (![db executeUpdate:@"UPDATE books SET score = 4 WHERE title = ?", @"ハッカーと画家"]) {
        NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
    }
    [db close];
}

// レコードの削除
- (IBAction)delete {
    FMDatabase* db = [self _getDB:DBFILE];

    NSArray* tables = [NSArray arrayWithObjects:@"authors", @"books", nil];
    for (NSString* table in tables) {
        if (![db executeUpdate:[NSString stringWithFormat:@"DELETE FROM %s", [table cStringUsingEncoding:NSUTF8StringEncoding]]]) {
            NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]);
        }
    }
    [db close];
}

あと仕方が無いと言えば無いのですが、objective-cの辞書と配列の使いにくさは非常にストレスを感じます。例えば、insertで作っている辞書と配列の混合物(51〜67行目)をpythonで書くと以下になります。

fixture = [
  {'author': '村上春樹',
   'books' : ['1Q84', 'ねじまき鳥クロニクル']},
  {'author': 'ポールグレアム',
   'books' : ['ハッカーと画家', 'On Lisp']}]

この差は結構重要で、LLだったら取りあえず辞書に入れといて、というところでもわざわざ扱うデータ専用のクラスを作るところから始める必要があり、LLってホンマに開発効率良かったんやというのを実感している次第であります。あとブログ幅も足りないですね。近いうちにテーマ修正しようと思います。

Leave a Reply

Your email address will not be published. Required fields are marked *