FMDB

FMDBは「SQLiteのC APIを薄くラップしたObjective-Cライブラリ」として開発された、iOS開発において長年愛用されているSQLiteラッパーです。「人間のためのSQLite」をコンセプトに、複雑なSQLiteのC APIをObjective-Cらしいシンプルで直感的なインターフェースで提供。トランザクション管理、カスタム関数、スレッドセーフティなど、iOS アプリ開発に必要な機能を包括的にサポートし、Core Dataほど重くない軽量なデータベースソリューションとして確固たる地位を築いています。

SQLite wrapperiOSObjective-CSwiftデータベースモバイル

GitHub概要

ccgus/fmdb

A Cocoa / Objective-C wrapper around SQLite

スター13,865
ウォッチ542
フォーク2,759
作成日:2010年7月6日
言語:Objective-C
ライセンス:Other

トピックス

なし

スター履歴

ccgus/fmdb Star History
データ取得日時: 2025/7/19 02:41

ライブラリ

FMDB

概要

FMDBは「SQLiteのC APIを薄くラップしたObjective-Cライブラリ」として開発された、iOS開発において長年愛用されているSQLiteラッパーです。「人間のためのSQLite」をコンセプトに、複雑なSQLiteのC APIをObjective-Cらしいシンプルで直感的なインターフェースで提供。トランザクション管理、カスタム関数、スレッドセーフティなど、iOS アプリ開発に必要な機能を包括的にサポートし、Core Dataほど重くない軽量なデータベースソリューションとして確固たる地位を築いています。

詳細

FMDB 2025年版はiOS SQLite操作の老舗ライブラリとして15年以上の開発実績により成熟したAPIと高い信頼性を誇ります。SQLiteの全機能に直接アクセス可能でありながら、メモリ管理やエラーハンドリングを自動化してObjective-C開発者に親しみやすいインターフェースを提供。3つの核となるクラス(FMDatabase、FMResultSet、FMDatabaseQueue)によるシンプルな設計で、SQLの知識を活かしつつ安全で効率的なデータベース操作を実現します。Swift互換性も良好で現代のiOS開発にも対応しています。

主な特徴

  • 軽量ラッパー: SQLiteのC APIを薄くラップ、パフォーマンス重視
  • Objective-Cネイティブ: iOS開発に最適化されたネイティブAPI
  • スレッドセーフティ: FMDatabaseQueueによる安全な並行処理
  • SQLパラメータバインディング: SQLインジェクション攻撃の防止
  • カスタム関数サポート: Blocksベースのカスタム関数実装
  • トランザクション管理: 自動トランザクション処理とロールバック対応

メリット・デメリット

メリット

  • SQLiteの全機能に直接アクセス可能で制限なし
  • Core Dataより軽量で学習コストが低く、SQLスキルを活用可能
  • 15年以上の実績による高い安定性とバグの少なさ
  • CocoaPods、Carthage、SPMなど複数のインストール方法に対応
  • Swift プロジェクトでも問題なく使用可能
  • メモリ効率が良くパフォーマンスが高い

デメリット

  • ORM機能なしで手動でのSQL記述とオブジェクトマッピングが必要
  • モデル変更時のマイグレーション管理が手動で煩雑
  • リレーションシップやオブジェクトグラフ管理は自実装が必要
  • SQLインジェクション対策を開発者が意識する必要
  • 複雑なクエリでは大量のボイラープレートコードが発生
  • モダンなORMライブラリと比較して開発効率が劣る場合がある

参考ページ

書き方の例

セットアップ

// CocoaPods
// Podfile
pod 'FMDB'
pod 'FMDB/SQLCipher'   // 暗号化が必要な場合

// Carthage
// Cartfile
github "ccgus/fmdb"

// Swift Package Manager
// Package.swift
.package(url: "https://github.com/ccgus/fmdb.git", from: "2.7.0")
// インポート
#import <FMDB/FMDB.h>

// Swift での使用
import SQLite3  // 場合によっては必要

基本的な使い方

// データベースの作成と接続
@interface DatabaseManager : NSObject
@property (nonatomic, strong) FMDatabase *database;
@end

@implementation DatabaseManager

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupDatabase];
    }
    return self;
}

- (void)setupDatabase {
    // ドキュメントディレクトリのパスを取得
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docsPath = [paths objectAtIndex:0];
    NSString *dbPath = [docsPath stringByAppendingPathComponent:@"database.sqlite"];
    
    // データベースを開く
    self.database = [FMDatabase databaseWithPath:dbPath];
    
    if ([self.database open]) {
        NSLog(@"Database opened successfully");
        [self createTables];
    } else {
        NSLog(@"Failed to open database: %@", [self.database lastErrorMessage]);
    }
}

- (void)createTables {
    NSString *createTableSQL = @"CREATE TABLE IF NOT EXISTS users ("
                              @"id INTEGER PRIMARY KEY AUTOINCREMENT, "
                              @"name TEXT NOT NULL, "
                              @"email TEXT UNIQUE NOT NULL, "
                              @"age INTEGER, "
                              @"created_at DATETIME DEFAULT CURRENT_TIMESTAMP)";
    
    if ([self.database executeUpdate:createTableSQL]) {
        NSLog(@"Table created successfully");
    } else {
        NSLog(@"Failed to create table: %@", [self.database lastErrorMessage]);
    }
}

@end

クエリ実行

// データの挿入
- (BOOL)insertUserWithName:(NSString *)name email:(NSString *)email age:(NSInteger)age {
    NSString *insertSQL = @"INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
    
    BOOL success = [self.database executeUpdate:insertSQL, name, email, @(age)];
    
    if (success) {
        NSLog(@"User inserted with ID: %lld", [self.database lastInsertRowId]);
    } else {
        NSLog(@"Failed to insert user: %@", [self.database lastErrorMessage]);
    }
    
    return success;
}

// データの取得
- (NSArray *)getAllUsers {
    NSMutableArray *users = [[NSMutableArray alloc] init];
    NSString *selectSQL = @"SELECT * FROM users ORDER BY created_at DESC";
    
    FMResultSet *results = [self.database executeQuery:selectSQL];
    
    while ([results next]) {
        NSDictionary *user = @{
            @"id": @([results intForColumn:@"id"]),
            @"name": [results stringForColumn:@"name"],
            @"email": [results stringForColumn:@"email"],
            @"age": @([results intForColumn:@"age"]),
            @"created_at": [results dateForColumn:@"created_at"]
        };
        [users addObject:user];
    }
    
    [results close];
    return [users copy];
}

// 条件付き検索
- (NSArray *)getUsersWithAgeGreaterThan:(NSInteger)age {
    NSMutableArray *users = [[NSMutableArray alloc] init];
    NSString *selectSQL = @"SELECT * FROM users WHERE age > ? ORDER BY name";
    
    FMResultSet *results = [self.database executeQuery:selectSQL, @(age)];
    
    while ([results next]) {
        NSDictionary *user = @{
            @"id": @([results intForColumn:@"id"]),
            @"name": [results stringForColumn:@"name"],
            @"email": [results stringForColumn:@"email"],
            @"age": @([results intForColumn:@"age"])
        };
        [users addObject:user];
    }
    
    [results close];
    return [users copy];
}

// データの更新
- (BOOL)updateUserEmail:(NSString *)newEmail forUserId:(NSInteger)userId {
    NSString *updateSQL = @"UPDATE users SET email = ? WHERE id = ?";
    
    BOOL success = [self.database executeUpdate:updateSQL, newEmail, @(userId)];
    
    if (!success) {
        NSLog(@"Failed to update user: %@", [self.database lastErrorMessage]);
    }
    
    return success;
}

// データの削除
- (BOOL)deleteUserWithId:(NSInteger)userId {
    NSString *deleteSQL = @"DELETE FROM users WHERE id = ?";
    
    BOOL success = [self.database executeUpdate:deleteSQL, @(userId)];
    
    if (!success) {
        NSLog(@"Failed to delete user: %@", [self.database lastErrorMessage]);
    }
    
    return success;
}

データ操作

// 使用例
- (void)demonstrateUsage {
    DatabaseManager *dbManager = [[DatabaseManager alloc] init];
    
    // ユーザーの追加
    [dbManager insertUserWithName:@"太郎" email:@"[email protected]" age:25];
    [dbManager insertUserWithName:@"花子" email:@"[email protected]" age:30];
    [dbManager insertUserWithName:@"次郎" email:@"[email protected]" age:22];
    
    // 全ユーザーの取得
    NSArray *allUsers = [dbManager getAllUsers];
    NSLog(@"All users: %@", allUsers);
    
    // 条件付き検索
    NSArray *adultUsers = [dbManager getUsersWithAgeGreaterThan:24];
    NSLog(@"Users older than 24: %@", adultUsers);
    
    // ユーザー情報の更新
    if ([allUsers count] > 0) {
        NSDictionary *firstUser = allUsers[0];
        NSInteger userId = [firstUser[@"id"] integerValue];
        [dbManager updateUserEmail:@"[email protected]" forUserId:userId];
    }
    
    // ユーザーの削除
    if ([allUsers count] > 1) {
        NSDictionary *secondUser = allUsers[1];
        NSInteger userId = [secondUser[@"id"] integerValue];
        [dbManager deleteUserWithId:userId];
    }
}

設定とカスタマイズ

// トランザクション管理
- (BOOL)performBulkInsert:(NSArray *)userData {
    __block BOOL success = YES;
    
    [self.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
        for (NSDictionary *user in userData) {
            NSString *insertSQL = @"INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
            
            if (![db executeUpdate:insertSQL, user[@"name"], user[@"email"], user[@"age"]]) {
                NSLog(@"Failed to insert user: %@", [db lastErrorMessage]);
                success = NO;
                *rollback = YES;
                return;
            }
        }
    }];
    
    return success;
}

// スレッドセーフなデータベース操作
@interface ThreadSafeDatabaseManager : NSObject
@property (nonatomic, strong) FMDatabaseQueue *databaseQueue;
@end

@implementation ThreadSafeDatabaseManager

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupDatabaseQueue];
    }
    return self;
}

- (void)setupDatabaseQueue {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docsPath = [paths objectAtIndex:0];
    NSString *dbPath = [docsPath stringByAppendingPathComponent:@"thread_safe_database.sqlite"];
    
    self.databaseQueue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
    
    [self.databaseQueue inDatabase:^(FMDatabase *db) {
        NSString *createTableSQL = @"CREATE TABLE IF NOT EXISTS products ("
                                  @"id INTEGER PRIMARY KEY AUTOINCREMENT, "
                                  @"name TEXT NOT NULL, "
                                  @"price REAL, "
                                  @"category TEXT)";
        
        if (![db executeUpdate:createTableSQL]) {
            NSLog(@"Failed to create products table: %@", [db lastErrorMessage]);
        }
    }];
}

- (void)insertProductsAsync:(NSArray *)products completion:(void(^)(BOOL success))completion {
    [self.databaseQueue inDatabase:^(FMDatabase *db) {
        BOOL allSuccess = YES;
        
        for (NSDictionary *product in products) {
            NSString *insertSQL = @"INSERT INTO products (name, price, category) VALUES (?, ?, ?)";
            
            if (![db executeUpdate:insertSQL, product[@"name"], product[@"price"], product[@"category"]]) {
                NSLog(@"Failed to insert product: %@", [db lastErrorMessage]);
                allSuccess = NO;
            }
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) {
                completion(allSuccess);
            }
        });
    }];
}

@end

エラーハンドリング

// 包括的なエラーハンドリング
@interface SafeDatabaseManager : NSObject
@property (nonatomic, strong) FMDatabase *database;
@end

@implementation SafeDatabaseManager

- (BOOL)executeQuery:(NSString *)sql withParameters:(NSArray *)parameters resultHandler:(void(^)(FMResultSet *results))resultHandler {
    if (!self.database.isOpen) {
        NSLog(@"Database is not open");
        return NO;
    }
    
    FMResultSet *results = [self.database executeQuery:sql withArgumentsInArray:parameters];
    
    if (!results) {
        NSLog(@"Query execution failed: %@", [self.database lastErrorMessage]);
        NSLog(@"SQL: %@", sql);
        NSLog(@"Parameters: %@", parameters);
        return NO;
    }
    
    if (resultHandler) {
        resultHandler(results);
    }
    
    [results close];
    return YES;
}

- (BOOL)executeUpdate:(NSString *)sql withParameters:(NSArray *)parameters {
    if (!self.database.isOpen) {
        NSLog(@"Database is not open");
        return NO;
    }
    
    BOOL success = [self.database executeUpdate:sql withArgumentsInArray:parameters];
    
    if (!success) {
        NSLog(@"Update execution failed: %@", [self.database lastErrorMessage]);
        NSLog(@"SQL: %@", sql);
        NSLog(@"Parameters: %@", parameters);
        NSLog(@"Last error code: %d", [self.database lastErrorCode]);
    }
    
    return success;
}

// カスタム関数の作成
- (void)setupCustomFunctions {
    // カスタム関数の定義(例:文字列の長さを返す関数)
    [self.database makeFunctionNamed:@"STRLEN" maximumArguments:1 withBlock:^(FMDatabase *db, NSArray *args) {
        if ([args count] > 0 && args[0] != [NSNull null]) {
            NSString *str = args[0];
            return [NSNumber numberWithInteger:[str length]];
        }
        return [NSNumber numberWithInteger:0];
    }];
    
    // カスタム関数の使用例
    NSString *testSQL = @"SELECT name, STRLEN(name) as name_length FROM users";
    [self executeQuery:testSQL withParameters:nil resultHandler:^(FMResultSet *results) {
        while ([results next]) {
            NSString *name = [results stringForColumn:@"name"];
            NSInteger nameLength = [results intForColumn:@"name_length"];
            NSLog(@"Name: %@, Length: %ld", name, (long)nameLength);
        }
    }];
}

@end