GRDBObjc
GRDB.swift 的 FMDB 兼容绑定。
GRDBObjc 帮助使用 SQLite 的 Objective-C 应用程序,在最小代价的情况下用 GRDB 替换 FMDB。
最新版本:2019 年 5 月 26 日 • 版本 0.8 • 发布说明
要求: iOS 9.0+ / macOS 10.9+ / watchOS 2.0+ • Swift 4.2+ / Xcode 10.0+
关注 Twitter 上的 @groue 以获取发布公告和使用技巧。
安装 • 示例 • FMDB 兼容性图表
用一点点 Swift 来享受自己
我们开发者维护和开发 Objective-C 应用程序,并希望能够将越来越多的 Swift 注入其中。
从头开始重写整个应用程序通常不是一个合理的选项,尽管Swift的吸引力很大。我们保留了代表多年开发的遗留Objective-C代码,并将Swift隔离在可以轻松插入Objective-C主体的少数功能中。
在这种情况下,具有Objective-C主干的混用应用程序中,少数Swift代码片段有时会因它们的外部基础而受阻。也许导入的Objective-C看起来不太像Swift。也许我们梦想着提供更优解决方案的Swift替代品。
FMDB在Swift的世界中
在 Pierlis(http://pierlis.com),我们对FMDB这种痒感相当强烈。FMDB做得非常出色,但GRDB做得更好。
2015年,GRDB是一个受FMDB大量启发的内部项目。三年和四版Swift之后,这个库已经达到API稳定性,提供了一个专注于应用程序开发的高强度工具箱。基础相同:GRDB与它值得尊敬的前辈一样精通SQL。它提供了相同的强大并发保证。然而,GRBD增加了独特的Swift风味,以及像数据库观察和记录类型这样的功能,在FMDB中是看不到的。
例如,让我们比较两段等价的代码片段,它们加载应用程序模型数组。使用GRDB,它会提供
struct Player: FetchableRecord {
...
}
let players: [Player] = try dbQueue.read { db in
try Player.fetchAll(db, sql: "SELECT * FROM player")
}
// use players array
FMDB创作了另一种诗歌
struct Player {
...
init(dictionary: [AnyHashable: Any]) { ... }
}
var queryError: Error? = nil
var players: [Player] = []
dbQueue.inDatabase { db in
do {
let resultSet = try db.executeQuery("SELECT * FROM player", values: nil)
while resultSet.next() {
let player = Player(dictionary: resultSet.resultDictionary!)
players.append(player)
}
} catch {
queryError = error
}
}
if let queryError = queryError {
throw queryError
}
// use players array
你可能想:“我从没用过FMDB这么做!”。确实,使用FMDB进行错误处理既难读又难写,而且几乎没有人这么做。然而,正是这种健壮性使得你的应用程序可以在锁定设备上安全运行,例如。GRDB简洁,不会遗漏任何错误,而且比FMDB运行上述代码要快得多。这里。
进一步使用GRDB
数据库观察是GRDB的一个现成功能,将节省你用FMDB在上面开发的数小时。
例如,GRDB附带高级工具,如FetchedRecordsController,以及配套库RxGRDB,让你可以用响应式方式观察数据库更改。由于观察发生在SQLite级别,因此它不会受到原始SQL更新、外键级联甚至SQL触发的欺骗。别错过任何一个提交。
let request = SQLRequest<Player>(
sql: "SELECT * FROM players ORDER BY score DESC LIMIT 10")
// Observe request changes with FetchedRecordsController:
let controller = FetchedRecordsController(dbQueue, request: request)
controller.trackChanges {
let players = $0.fetchedRecords // [Player]
print("Players have changed")
}
try controller.performFetch()
// Observe request changes in the reactive way:
request.rx.fetchAll(in: dbQueue)
.subscribe(onNext: { players: [Player] in
print("Players have changed")
})
GRDBObjc是GRDB和你的针对FMDB的Objective-C代码之间的粘合剂
不能简单地在同一应用程序中同时安装FMDB和GRDB,让Objective-C代码针对FMDB,而新的Swift代码使用GRDB。这不会很好工作,因为SQLite不允许两个不同的连接同时写入数据库。相反,一旦Objective-C线程和Swift线程同时修改数据库,就会失败并出现SQLITE_BUSY错误。想象一下,用户正在编辑某个值时下载操作完成的情况。
进入GRDBObjc。通常,你只需要删除FMDB,安装GRDB和GRDBObjc,然后替换#import
指令
-#import <fmdb/FMDB.h>
+#import <GRDBObjc/GRDBObjc.h>
这对于大多数针对FMDB的Objective-C代码能够在GRDB和GRDBObjc之上编译来说已经足够了。当然,魔鬼隐藏在细节之中,我们将在下面列出详细的兼容性图表。
FMDatabaseQueue
、FMResultSet
等类现在由GRDB支持。从Objective-C初始化的数据库队列可以从Swift中使用,反之亦然。有关详细说明,请参阅下面的安装程序。
安装
重要警告:GRDBObjc仍然是相对较新的软件。虽然已进行测试,但仍未在许多现实生活中的应用程序中使用。
请熟悉Swift文档中的“在同一个项目中混合Swift和Objective-C”章节。
查看FMDB兼容性图表。
如果缺少你需要的某些API,或者当你发现一个错误或异常行为时,请准备发起一个pull request。
可以使用Cocoapods安装GRDObjc
-
请确保你的应用程序目标使用自动引用计数编译Objective-C:
CLANG_ENABLE_OBJC_ARC = YES
。 -
在你的应用程序目标的构建设置中指定:
SWIFT_VERSION = 4.0
。 -
在Podfile中用GRDBObjc替换FMDB
-pod 'FMDB' +pod 'GRDBObjc'
-
运行
pod install
。 -
替换Objective-C导入指令
-#import <fmdb/FMDB.h> +#import <GRDBObjc/GRDBObjc.h>
-
运行并测试你的应用程序:确保你的Objective-C代码能够很好地处理GRDBObjc。
-
通过桥接头将Objective-C的
FMDatabaseQueue
暴露给Swift不要在Swift中使用FMDB API:这不是这个库的目标!相反,从
FMDatabaseQueue
中提取真正的GRDBDatabaseQueue
,并确保Swift代码只使用GRDBimport GRDB import GRDBObjc // Some FMDatabaseQueue exposed to Swift via the bridging header: let fmdbQueue: FMDatabaseQueue = ... // The genuine GRDB's DatabaseQueue: let dbQueue: DatabaseQueue = fmdbQueue.dbQueue
有关示例设置,请参阅演示应用。
示例
这个示例应用程序使用FMDB兼容API在Objective-C中设置数据库,并用GRDB从Swift中访问数据库。
要运行此示例应用程序:
- 下载此仓库的副本。
- 从
Tests/CocoaPods/TestApp
目录运行pod install
。 - 在Xcode 9中打开
Tests/CocoaPods/TestApp/TestApp.xcworkspace
。 - 运行应用程序。
- Objective-C和Swift之间的桥接发生在DataStore类和桥接头中。
FMDB兼容性图表
GRDBObjc目标与FMDB 2.7.2兼容。
GRDB和FMDB通常表现完全相同。如有差异,GRDBObjc优先考虑FMDB兼容性而非原生GRDB行为。
一些FMDB功能目前不适用于GRDBObjc。例如,GRDBObjc需要通过线程安全的FMDatabaseQueue访问数据库:您无法创建裸露的FMDatabase实例。
一些其他FMDB功能尚未移植到GRDBObjc。如果您需要的某些API缺失,请提交pull request。
一般兼容性警告
NSDate:默认情况下,FMDB按数值Unix时间戳存储和读取日期。出于兼容性的考虑,GRDBObjc做了相同操作。但是,GRDB将日期存储为“YYYY-MM-DD HH:MM:SS.SSS”字符串。尽管如此,它可以原生地从Unix时间戳中加载数据。
FMDatabaseQueue
-
完整兼容性
@property (atomic, retain, nullable) NSString *path; + (instancetype)databaseQueueWithPath:(NSString * _Nullable)aPath; - (instancetype)initWithPath:(NSString * _Nullable)aPath;
-
低级兼容性警告
// FMDB lets you escape a FMDatabase connection from its database // queue's protected blocks. You shouldn't do that, but it's // possible. With GRDBObjc, it is a programmer error, with undefined // consequences, to do so. Don't use an FMDatabase connection // outside of a protected block. - (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block; - (void)inTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block; - (void)inDeferredTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block;
FMDatabase
-
完整兼容性
// Properties @property (nonatomic, readonly) void *sqliteHandle; @property (nonatomic, readonly) int64_t lastInsertRowId; @property (nonatomic, readonly) int changes; /// Retrieving error codes @property (atomic, assign) BOOL logsErrors; @property (atomic, assign) BOOL crashOnErrors; - (NSError *)lastError; // Perform updates - (BOOL)executeStatements:(NSString*)sql; // Transactions - (BOOL)beginTransaction; - (BOOL)beginDeferredTransaction; - (BOOL)commit; - (BOOL)rollback; // Save points - (BOOL)startSavePointWithName:(NSString*)name error:(NSError * _Nullable *)outErr; - (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError * _Nullable *)outErr; - (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError * _Nullable *)outErr; - (NSError * _Nullable)inSavePoint:(__attribute__((noescape)) void (^)(BOOL *rollback))block; // Date formatter + (NSDateFormatter *)storeableDateFormat:(NSString *)format; - (BOOL)hasDateFormatter; - (void)setDateFormat:(NSDateFormatter *)format; - (NSDate * _Nullable)dateFromString:(NSString *)s; - (NSString *)stringFromDate:(NSDate *)date;
-
低级兼容性警告
// This property reflects actual SQLite state instead of relying on // the balance of beginTransaction/commit/rollback methods: // // - Some transaction errors have FMDB return true when GRDBObjc // returns false. // // - Opening a transaction with a savepoint method has FMDB return // false when GRDBObjc returns true. @property (nonatomic, readonly) BOOL isInTransaction; // When an NSDecimalNumber parameter contains a value that can be // exactly represented as int64_t, GRDBObjc presents it to SQLite as // an integer. FMDB presents all decimal numbers as doubles. // // When an NSNumber parameter contains an unsigned 64-bit integer // higher than the maximum signed 64-bit integer, GRDBObjc crashes // with a fatal error, when FMDB stores a negative value. - (BOOL)executeUpdate:(NSString * _Nonnull)sql, ...; - (BOOL)executeUpdate:(NSString * _Nonnull)sql withErrorAndBindings:(NSError * _Nullable * _Nullable)outErr, ...; - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments; - (BOOL)executeUpdate:(NSString*)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error; - (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments; - (FMResultSet * _Nullable)executeQuery:(NSString*)sql, ...; - (FMResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments; - (FMResultSet * _Nullable)executeQuery:(NSString *)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error; - (FMResultSet * _Nullable)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary * _Nullable)arguments;
FMResultSet
-
完整兼容性
@property (nonatomic, readonly, nullable) NSDictionary *resultDictionary; - (void)close; - (BOOL)next; - (BOOL)nextWithError:(NSError * _Nullable *)outErr; - (int)columnIndexForName:(NSString*)columnName;
-
低级兼容性警告
// GRDBObjc crashes with a fatal error when those methods are called // after the result set has been exhausted, closed, or had an error. // FMDB would always return a value. - (BOOL)columnIndexIsNull:(int)columnIdx; - (BOOL)columnIsNull:(NSString*)columnName; - (NSString * _Nullable)stringForColumn:(NSString*)columnName; - (NSData * _Nullable)dataForColumn:(NSString*)columnName; - (NSData * _Nullable)dataNoCopyForColumn:(NSString *)columnName NS_RETURNS_NOT_RETAINED; - (NSDate * _Nullable)dateForColumn:(NSString*)columnName; - (id _Nullable)objectForColumn:(NSString*)columnName; - (id _Nullable)objectForColumnName:(NSString * _Nonnull)columnName __deprecated_msg("Use objectForColumn instead"); - (id _Nullable)objectForKeyedSubscript:(NSString *)columnName; - (long)longForColumn:(NSString*)columnName; - (long long int)longLongIntForColumn:(NSString*)columnName; - (BOOL)boolForColumn:(NSString*)columnName; - (double)doubleForColumn:(NSString*)columnName; // On top of the previous compatibility warning, GRDBObjc will crash with // a fatal error when indexes are out of range. FMDB would always return a // value. - (NSData * _Nullable)dataForColumnIndex:(int)columnIdx; - (NSData * _Nullable)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED; - (NSDate * _Nullable)dateForColumnIndex:(int)columnIdx; - (id _Nullable)objectForColumnIndex:(int)columnIdx; - (id _Nullable)objectAtIndexedSubscript:(int)columnIdx; - (long)longForColumnIndex:(int)columnIdx; - (long long int)longLongIntForColumnIndex:(int)columnIdx; - (BOOL)boolForColumnIndex:(int)columnIdx; - (double)doubleForColumnIndex:(int)columnIdx; - (NSString * _Nullable)stringForColumnIndex:(int)columnIdx; // On top of the previous compatibility warnings, GRDBObjc also crashes // with a fatal error when database contains a value that is not // representable in the requested type. FMDB would always return a value. - (int)intForColumnIndex:(int)columnIdx; - (int)intForColumn:(NSString*)columnName; - (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx; - (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName;
Happy GRDB!