1.2.0

1.2.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最新发布2017年10月
SwiftSwift 版本4.0
SPM支持 SPM

Jason Fieldman 维护。



1.2.0

镉是为 Swift 编写的 Core Data 框架,它强制执行最佳实践,并精确地在您创建它们的位置抛出常见的 Core Data 陷阱的异常。

镉是为了应对处理多个管理对象上下文的复杂性而编写的。仍然很重要理解什么是管理对象上下文以及它们是如何使用的,但对于典型的 CRUD 风格的 Core Data 使用来说,这是一个完全的麻烦。

使用镉,用户永远不会看到 NSManagedObjectContext 或其派生类。您只与无参数的事务交互,以及对象检索/操作任务。上下文由后台管理,这使得 Core Data 感觉更像 Realm。

设计目标

  • 创建一个简约/简洁的框架 API,它提供大部分 Core Data 使用案例,并指导用户朝着最佳实践的方向前进。
  • 积极地保护用户免受执行常见的 Core Data 陷阱的影响,并在违反的语句上立即抛出异常,而不是等待上下文保存事件。

以下是一个 Cadmium 事务的示例,它会给您的所有员工对象涨薪

Cd.transact {
    try! Cd.objects(Employee.self).fetch().forEach {
        $0.salary += 10000
    }
}

您可能会注意到以下几点

  • 事务使用极其简单。您无需在块内声明任何参数。
  • 您永远不需要引用管理对象上下文,我们为您管理它。
  • 更改将在完成时自动提交(您可以选择禁用此功能。)

Cadmium 不是什么

Cadmium 并非为了成为 Core Data 的100%的包装器而设计的。一些更高级的 Core Data 功能被隐藏在 Cadmium API 之后。如果您正在创建一个企业级应用程序,该应用程序需要精确操作 Core Data 数据存储和上下文以优化繁重工作,则 Cadmium 不适合您。

如果您想要一个智能包装器,该智能包装器极大地简化了大多数 Core Data 任务并在您意外地以错误的方式操作数据时立即警告您,那么 Cadmium 是为您准备的。

安装

您可以通过将其添加到您的 CocoaPods Podfile 中来安装 Cadmium。

pod 'Cadmium'

或者您可以使用多种方法将此项目的 Cadmium.framework 文件包含到您的项目中。

Swift 版本支持

Swift 3.0: 使用 Cadmium 1.0

Swift 2.3: 使用 Cadmium 0.13.x

Swift 2.2: 使用 Cadmium 0.12.x

Cocoapods

pod 'Cadmium', '~> 1.1'  # Swift 3.1 
pod 'Cadmium', '~> 1.0'  # Swift 3.0
pod 'Cadmium', '~> 0.13' # Swift 2.3
pod 'Cadmium', '~> 0.12' # Swift 2.2

如何使用

上下文架构

Cadmium 使用与 CoreStore 相同的基本上下文架构,有一个根存储上下文在私有队列上运行,该队列有一个只读子上下文在主队列上,以及在后台队列上运行的任意数量的可写子上下文。

Cadmium Core Data Architecture

这意味着您的主线程永远不会在写事务上过载,只会用来合并更改(在内存中)和更新依赖于您数据的任何 UI 元素。

这也意味着您不能在主线程上对托管对象进行修改!您所有的写操作都必须存在于后台线程中发生的交易中。您需要设计您的应用程序以支持异步写操作的概念,这在数据库修改时应该正是您应该做的事情。

托管对象模型

托管对象模型创建和使用与典型的 Core Data 流程非常相似。按常规创建您的托管对象模型,并生成相应的 NSManagedObject 类。然后,只需更改层次结构,使得您的类实现从 CdManagedObject 继承,而不是从 NSManagedObject 继承。

CdManagedObjectNSManagedObject 的子类。

初始化

使用单次初始化调用设置 Cadmium

do {
    try Cd.initWithSQLStore(momdInbundleID: nil,
                            momdName:       "MyObjectModel.momd",
                            sqliteFilename: "MyDB.sqlite",
                            options:        nil /* Optional */)
} catch let error {
    print("\(error)")
}

这将加载对象模型,设置持久化存储协调器,并初始化重要上下文。

如果您的对象模型在一个框架(而不是您的应用程序包)中,您必须将框架的标识符传递给第一个参数。

options 参数传达给传递给 NSPersistentStoreCoordinator 的 addPersistentStoreWithType: 所需选项。

您可以将 sqliteFilename 参数传递 nil 以创建一个 NSInMemoryStoreType 数据库。

查询

Cadmium 提供链式查询机制。这可以用来从主线程(以只读方式)或从事务内部查询对象。

查询从 Cd.objects(..) 开始,看起来像这样

do {
    for employee in try Cd.objects(Employee.self)
                          .filter("name = %@", someName)
                          .sorted("salary", ascending: true)
                          // See CdFetchRequest for more functions
                          .fetch() {
        /* Do something */
        print("Employee name: \(employee.name)")
    }
} catch let error {
    print("\(error)")
}

您首先通过将托管对象类型传递到 Cd.objects(..) 的参数中来开始。这将为此类型创建一个 CdFetchRequest

按照您想要的次数链式排列过滤/排序/修改调用,然后使用 fetch()fetchOne() 完成。 fetch() 返回对象数组,而 fetchOne() 返回单个可选对象(如果没有找到符合条件的对象,则为 nil)。

事务

您只能从事务内部开始对数据的更改。您可以使用以下方式开始一个事务:

Cd.transact {
    //...
}
Cd.transactAndWait {
    //...
}

Cd.transact 异步执行事务(调用线程在事务的工作执行期间继续)。Cd.transactAndWait 同步执行事务(它会阻塞调用线程,直到事务完成)。

为了确保最佳实践和避免可能的死锁,不允许您从主线程中调用 Cd.transactAndWait(这将引发异常)。

隐式事务提交

当事务完成时,事务上下文会自动提交您对数据存储所做的任何更改。对于大多数事务,这意味着您不需要调用任何额外的提交/保存命令。

如果您想禁用事务的隐式提交(例如执行回滚并忽略所做的任何更改),您可以在事务内部调用 Cd.cancelImplicitCommit()。一个典型的用法如下

Cd.transact {

    modifyThings()

    if someErrorOccurred {
        Cd.cancelImplicitCommit()
        return
    }

    moreActions()
}

您也可以通过调用 Cd.commit() 来在事务中间强制提交。在需要保存更改并在可能带有取消的隐式提交的情况下返回的长事务期间,您可能想这么做。一个用法案例可能如下

Cd.transact {

    modifyThingsStepOne()
    Cd.commit() //changes in modifyThingsStepOne() cannot be rolled back!

    modifyThingsStepTwo()

    if someErrorOccurred {
        Cd.cancelImplicitCommit()
        return
    }

    moreActions()
}

强制串行事务

注意:高级功能

Core Data 和 Cadmium 本质上是异步 API。您通常在主线程上异步发起获取和修改数据。如果发现您执行的事务上下文修改经常互相冲突,这种与异步行为的紧密耦合可能是有害的。

例如,以下事务可能会在用户点击按钮以访问地点时发生

Cd.transact {
    if let place = try! Cd.objects(Place.self).filter("id = %@", myID).fetchOne() {
        place.visits += 1
    }
}

如果用户不断点击访问按钮怎么办?因为事务发生在不同的上下文队列中,无法保证 place.visits 会按顺序递增。存在一种可能性较大的竞态条件,在递增之前两个上下文都可能看到的 place.visits 是相同的值。

为了帮助解决这个问题并确保事务按顺序执行,您可以在事务中传递一个可选的 serial 参数

Cd.transact(serial: true) {
    if let place = try! Cd.objects(Place.self).filter("id = %@", myID).fetchOne() {
        place.visits += 1
    }
}

这确保了在执行下一个之前,您的交易将按顺序发生(甚至等待上下文保存最终化)——因此您的交易可以被考虑为原子的。

请注意,这种原子行为限于顶级事务。事务内的事务不在串行队列上执行,以防止死锁。

/* In this odd case, the inside transactAndWait will ignore the serial parameter, since it is already
   inside a transaction.  The prevents deadlocks waiting on the serial queue. */

Cd.transact(serial: true) {
    Cd.transactAndWait(serial: true) {
    }
}

每次都传递串行参数很烦人,尤其是如果您大多数情况下都需要这样。如果您想让事务默认按顺序执行,在 Cd.init 函数中将 serialTX 参数传递为 true

try Cd.initWithSQLStore(momdInbundleID: "org.fieldman.CadmiumTests",
                        momdName:       "CadmiumTestModel",
                        sqliteFilename: "test.sqlite",
                        serialTX:       true)

当您传递 true 到 init 中时,您可以传递 falseserial 参数来覆盖此设置

Cd.transact(serial: false) {
    ...
}

未指定 serial 参数或传递 nil,将使用初始化期间确定的默认设置。

大多数用例将设置默认串行使用为 true 并保持这种方式。在您尝试执行大量并发或长时间运行的事务以及与其无关的对象时(因为这些都将顺序执行而不是并行执行),您将承受性能损失。

作为对此问题的更高级工作方式,如果您有多个关于您的数据存储的不同子集上的并发长期运行事务,您可以将自己的分发队列传递到 on 参数

Cd.transact(on: mySerialDispatchQueue) {
    ...
}

事务块本身将在上下文队列中发生——但这发生在您提供的分发队列中的同步中。您可以为影响不同对象的交易提供不同的分发队列。

请注意,您提供的分发队列将针对内部默认串行队列,该队列无论使用哪个队列都保持保存上下文同步。这也意味着,在默认串行队列上运行的任何交易都将阻塞您传递的所有其他队列(因此请避免在默认串行队列上运行非常长的事务)。

创建和删除对象

对象可以在事务内创建和删除。

Cd.transact {
    let newEmployee    = try! Cd.create(Employee.self)
    newEmployee.name   = "Bob"
    newEmployee.salary = 10000
}

Cd.transact {
    Cd.delete(try! Cd.objects(Employee.self).filter("name = %@", "Bob").fetch())
}

您还可以直接从 CdFetchRequest 中删除对象

Cd.objects(Employee.self).filter("salary > 100000").delete()

如果被调用,

  • 事务外:将在后台事务中异步删除对象。
  • 事务内:将同步在事务内执行删除操作。

从其他上下文修改对象

您通常需要从另一个上下文中修改一个管理对象。最常见的情况是,当您想修改从主线程(只读)查询到的对象时。

您可以使用 Cd.useInCurrentContext 获取一个当前上下文中适用于修改的对象副本

/* Acquire a read-only employee object somewhere on the main thread */
guard let employee = try! Cd.objects(Employee.self).fetchOne() else {
    return
}

/* Modify it in a transaction */
Cd.transact {
    guard let txEmployee = Cd.useInCurrentContext(employee) else {
        return
    }

    txEmployee.salary += 10000    
}

请注意,一个对象必须在事务中插入和提交后才能从另一个上下文中访问。如果暂时对象尚未插入,则该方法将无法使用。

如果您只从另一个上下文中使用一个对象,则考虑使用 Cd.transactWith

/* Acquire a read-only employee object somewhere on the main thread */
guard let employee = try! Cd.objects(Employee.self).fetchOne() else {
    return
}

/* Modify it in a transaction */
Cd.transactWith(employee) { txEmployee in
    if let txEmployee = txEmployee {
        txEmployee.salary += 10000
    }
}

您还可以将数组传递给 Cd.transactWith 以在新的上下文中获取对象数组。

通知主线程

由于事务发生在事务上下文的私有队列中,对 Cd.commit() 的调用是同步的,并且只有当保存已传播到持久存储后才会返回。

您可以使用这一事实来通知主线程,您的交易已提交

Cd.transact {

    modifyThings()
    Cd.commit()

    /* only called after the commit saves up to the persistent store */
    DispatchQueue.main.async {
        notifyOthers()
    }    
}

如果您想将事务中创建的对象派发回主线程,也必须调用 Cd.commit(),因为调用 Cd.commit() 将将创建的对象保存到持久存储,并赋予它们永久ID。

Cd.transact {
    let newItem = try! Cd.create(ExampleItem.self)
    newItem.name = "Test"

    /* Synchronously saves newItem to the persistent store */
    try! Cd.commit()

    Cd.onMainWith(newItem) { mainItem in
        guard let item = mainItem else {
            return
        }

        print("created item in transaction: \(item.name)")        
    }
}

获取结果控制器

对于 NSFetchedResultsController 的典型用法,您应使用内置子类 CdFetchedResultsController。此子类将 NSFetchedResultsController 的普通功能包装到受保护的main queue上下文中。

您可以使用 CdFetchedResultsController,就像使用 NSFetchedResultsController 一样,以下注意事项

  • 获取结果中的对象存在于主线程只读上下文中,无法修改。使用 Cd.useInCurrentContext 在事务中修改它们。
  • 您可以将一个 UITableView 传递给 automateDelegation 方法,当您的获取结果控制器有更改时进行标准的插入/删除命令。这可以帮助减少您自己的视图控制器中的代码行。

使用更新处理程序

每个 CdManagedObject 实例都有一个名为 updateHandler 的属性,可以存储在对象更新时要调用的块。您只能在属于主线程上下文的对象上将块附加到 updateHandler。这在您想监视对象而不使用 NSFetchedResultsController 的情况下非常有用。

一个例子可能如下所示

/* ... from the example above, transferring a new item to the main thread: */
DispatchQueue.main.async {
    if let mainItem = Cd.useInCurrentContext(newItem), name = mainItem.name {
        print("created item in transaction: \(name)")
        mainItem.updateHandler = { event in
            print("event occurred on object \(name): \(event)")
        }
    }
}

请注意,每个实例只能安装一个 updateHandler。如果您需要一个需要向更多听众分发的解决方案,您可以使用处理程序发布一个 NSNotification,或使用另一个工具包,如 ReactiveCocoa(请参阅示例目录中的文件 Cadmium+ReactiveCocoa.swift)。

积极识别编码陷阱

大多数使用 Core Data 的开发者都经历了同样的一关又一关的发现,在创建多线程 Core Data 应用程序的过程中遇到的各种陷阱和复杂情况。

即使是经验丰富的老手也难免会遇到偶尔的 1570: 操作无法完成13300: NSManagedObjectReferentialIntegrityError

许多常见问题出现的原因是因为标准的Core Data框架对允许执行错误操作代码宽容,并且只在实际保存发生时才抛出错误(这可能与错误代码并不接近。)

Cadmium对托管对象操作执行积极检查以确保您编写的代码正确,并且会在发生错误的代码行引发异常,而不是等待保存发生。

PromiseKit 扩展

您可以通过添加以下内容来启用Cadmium的PromiseKit扩展:

pod 'Cadmium/PromiseKit'

到您的Podfile中。这将启用以下功能

事务式Promise

使用PromiseKit扩展后,Cd.transactCd.transactWith现在拥有promise返回值覆盖。在大多数情况下,只要您像处理promise一样处理事务的返回值,编译器应该能够推断出这些。

Cd.transact {
    //...
}.then { _ -> Void in

}

Cd.transactWith(obj) { txObj in
    //...
}.then { _ -> Void in

}

实现方式是在promise解决之前提交事务更改,因此事务Promise从promise链的角度来看是完全原子的。

请注意,编译器可能要求您对promise链进行一些明确的声明。如果您遇到奇异的错误,请尝试显式定义promise签名,而不是让编译器尝试推断它们(例如,上述示例添加了_ -> Void in而不是将其留空)。

链内的交易

如果您想在promise链内使用事务,有一些额外的选项可供您选择。

firstly {
    somePromise()
}.thenTransact(serial: ..., on: ...) { // Optionally use serial: and on: arguments
    // This block occurs inside a Cadmium transaction
    // Commit is called before the promise is fulfilled.
}.then {

}

// Or use the transactWith variant when the previous
// promise is fulfilled with a CdManagedObject

firstly {
    return employeeVarFromMainThread // <- CdManagedObject
}.thenTransactWith(serial: ..., on: ...) { (employee: Employee) -> Void in
    // This block occurs inside a Cadmium transaction with a
    // version of the argument belonging to the current context.
}.then {

}

有时您需要将CdManagedObject从事务中传递回主线程。为此,请使用thenOnMainWith

firstly {
    return employeeVarFromMainThread
}.thenTransactWith(serial: ..., on: ...) { (employee: Employee) -> Employee in
    employee.salary += 10000
    return employee
}.thenOnMainWith { (employee: Employee) -> Void
    // Here, employee is a read-only CdManagedObject from the main thread context.
    print("The salary is \(employee.salary)")
}

应注意,thenTransactWith的promise块可以接收管道中的可选值,但它不会作为其代码块参数传递可选值。如果thenTransactWith的解决值是nil,或者内部Cd.useInCurrentContext返回一个nil值,则认为这是一个promise错误。在这些情况下,链将被拒绝,并抛出CdPromiseError.NotAvailableInCurrentContext(value)