GRDBObjcCore 0.8

GRDBObjcCore 0.8

Gwendal RouéGwendal RouéGwendal Roué 维护。



  • 作者
  • Gwendal Roué

GRDBObjc Swift 4.2 Swift 5License

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之上编译来说已经足够了。当然,魔鬼隐藏在细节之中,我们将在下面列出详细的兼容性图表

FMDatabaseQueueFMResultSet等类现在由GRDB支持。从Objective-C初始化的数据库队列可以从Swift中使用,反之亦然。有关详细说明,请参阅下面的安装程序。

安装

重要警告:GRDBObjc仍然是相对较新的软件。虽然已进行测试,但仍未在许多现实生活中的应用程序中使用。

请熟悉Swift文档中的“在同一个项目中混合Swift和Objective-C”章节。

查看FMDB兼容性图表

如果缺少你需要的某些API,或者当你发现一个错误或异常行为时,请准备发起一个pull request

可以使用Cocoapods安装GRDObjc

  1. 请确保你的应用程序目标使用自动引用计数编译Objective-C:CLANG_ENABLE_OBJC_ARC = YES

  2. 在你的应用程序目标的构建设置中指定:SWIFT_VERSION = 4.0

  3. 在Podfile中用GRDBObjc替换FMDB

    -pod 'FMDB'
    +pod 'GRDBObjc'
  4. 运行pod install

  5. 替换Objective-C导入指令

    -#import <fmdb/FMDB.h>
    +#import <GRDBObjc/GRDBObjc.h>
  6. 运行并测试你的应用程序:确保你的Objective-C代码能够很好地处理GRDBObjc。

  7. 通过桥接头将Objective-C的FMDatabaseQueue暴露给Swift

    不要在Swift中使用FMDB API:这不是这个库的目标!相反,从FMDatabaseQueue中提取真正的GRDB DatabaseQueue,并确保Swift代码只使用GRDB

    import 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;

:bowtie: Happy GRDB!