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