CoreStore:使用Swift包装Core Data框架

CoreStore

CoreStore 做得更好

  • 每个数据栈支持多个 persistent store。如 .xcdatamodeld 文件设计的那样,CoreStore还会将一个数据栈作为默认栈,但是你可以创建和管理多个栈。
  • 渐进式迁移 :只需要告诉数据栈模型版本的顺序,CoreStore 会自动使用渐进式迁移。
  • 创建自己的日志框架。
  • 其他Core Data的封装都有个局限,那就是实体名字一定要和相应的类名一致。CoreStore从管理对象模型文件中导入实体到类的映射,因此实体名字和相应的类名无需一致。
  • 提供类型安全、配置简单的观察者模式,替代 NSFetchedResultsController 和 KVO 。
  • 不只有 fetching 的API接口,还可以查询聚集值和属性类型的API。
  • 为了减少常见并发性错误的发生,NSManagedObjectContext任务在封装成更安全,更高层次的抽象。与此同时还保留了NSManagedObjectContext任务灵活性和可定制性。
  • 基于Swift语言的优雅以及类型安全性,CoreStore提供了干净方便的API。
  • 提供文档。这里没有魔法,类、方法、特征都是公开的。这篇README中也介绍了大量的概念以及解释了大量CoreStore的原理。
  • 高效导入机制。

摘要

初始化设置渐进式迁移

添加存储

开始进行数据处理

获取对象

查询值

好了我写这儿长的README是有原因的,接下来我们来看看其中的具体实现。

结构

为了最大程度的安全和性能表现,CoreStore将强制编码模式(别担心,没有听起来那么可怕)。

最好在使用CoreStore之前先了解一些运作原理。

如果你已经非常熟悉 CoreData 的内部工作原理,这里是一张CoreStore的概念对照表。

屏幕快照 2016-07-08 下午6.00.09

一些有名的开发库比如 RestKit 和 MagicalRecord 是这样布置他们的NSManagedObjectContexts:

屏幕快照 2016-07-08 下午6.00.20

嵌套的上下文存储路径从 context 子类到 context根类,最大程度地确保上下文间数据完整性,同时不阻塞主线程。

但是根据 Florian Kugler的研究,合并上下文远比嵌套上下文的存储速度快。

CoreStore 的 DataStack 集两种方式的长处,将主要 NSManagedObjectContext 作为一个只读 context,只允许子类 NSManagedObjectContext进行数据处理。

屏幕快照 2016-07-08 下午6.00.25

这样我们既能拥有一个黄油般顺滑的主线程,以及安全的嵌套上下文的优势。

初始化

初始化 CoreStore 最简单的方法就是在默认的栈中加入默认的仓库。

这行代码做了以下几件事情:

  • 使用一个默认 DataStack 来对 CoreStore.defaultStack 进行懒加载。
  • 设置该栈的 NSPersistentStoreCoordinator,底层存储 NSManagedObjectContext,以及一个只读主要 NSManagedObjectContext。
  • 在Application Support(或者是tvOS的Caches文件中)文件中添加一个SQLite仓库,文件名是 “[App bundle name].sqlite”。
  • 创建并且成功返回 NSPersistentStore 的实例,或者返回失败的NSError。

大多数情况下,这些配置就已经足够使用了。但是对需要更加多的设置,请参阅以下例子:

(如果你从来没有听过“Configurations”, 你会在 .xcdatamodeld 文件中找到)

屏幕快照 2016-07-08 下午6.03.01

上面的示例代码中,可以不需要写 CoreStore.defaultStack = dataStack。而是像下面这样声明一个 DataStack 实例,然后直接调用它的实例方法:

不同点在于,当你把这个 stack 设置为 CoreStore.defaultStack,你就可以直接从 CoreStore 调用该 stack 的方法。

迁移

到现在为止,我们只是使用了 addSQLiteStoreAndWait(…) 来初始化我们的持久化仓库。正如该方法名字的后缀 “AndWait” 提示的那样,该方法会引起阻塞,所以不能用来做一些长时间的任务,比如存储迁移(实际上CoreStore根本不会尝试去做,如果发生了 model 不匹配会被当做错误进行报告),如果需要进行数据迁移应当使用该方法的异步版本 addSQLiteStore(… completion:)。

在 completion 块中返回一个 PersistentStoreResult 值来表明创建是成功还是失败。

存储指定的URL与已经存在的仓库存在冲突,一个存在的sqlite文件不能读取都会引起 addSQLiteStore(…) 报错。如果报错 completion 块不会运行,该方法同样会返回一个可选的 NSProgress。如果是 nil,则不需要迁移。如果返回nil,你可以使用KVO的 “fractionCompleted” 值来跟踪迁移过程,或者通过 NSProgress+Convenience.swift 中暴露的闭包

这个闭包在主线程执行。

渐进式迁移

一般来说,CoreStore 使用的是 Core Data 默认的自动迁移机制,也就是说 CoreStore 会尝试将先手的 persistent store 迁移到 .xcdatamodeld 文件的当前版本。如果没有 store 版本到 model 版本的映射,CoreStore就会放弃并且报错。

DataStack 可以通过 MingrationChain 实现迁移,这点在 DataStack 实例初始化的时候进行设置,并且用于 DataStack 通过 addSQLiteStore(…) 及其变体添加的所有仓库。

最常用的方法如上按顺序传入 .xcdatamodeld 版本名。

更加复杂的迁移路径,你可以用源/目标的键值对形式传入版本树。

上面的案例对应的版本路径。

  • MyAppModel-MyAppModelV3-MyAppModelV4
  • MyAppModelV2-MyAppModelV4
  • MyAppModelV3-MyAppModelV4

初始化的时候使用空值表示不是用渐进式迁移而是使用默认迁移方式(当前.xcdatamodel版本作为最后版本)。

除非是空值,当 MigrationChain 传值给 DataStack 后就生效了,但是下面情况发生时会触发断言:

  •  一个版本数组中出现两次
  • 一个版本在字典中两次作为key出现
  • 路径中出现循环

记住如果指定了MigrationChain、.xcdataModeld的当前版本就会被设为旁路,MigrationChain的枝末版本将作为DataStack的基本模型版本。

预测式迁移

有时候迁移量很大,app需要做个 loading 界面或者其他形式和用户进行交互。CoreStore提供了 requiredMigrationsForSQLiteStore(…) 方法用来在调用 addSQLiteStore(…) 前检查persistent store。

requiredMigrationsForSQLiteStore(…) 返回MigrationType的数组,可能的值:

每个 MigrationType 指明 MigrationChain 中每一步迁移的类型。

保存和处理数据

为了确保只读 NSManagedObjectContext 的对象不被随意改变,CoreStore并没有开放直接更新和保存主 context 和其他 context 的API。你只能通过 DataStack 实例进行数据处理。

或者直接调用CoreStore的默认stack。

commit() 这个方法将数据变化保存到 persistent store。如果在数据处理块完成后没有调用 commit(),所有的变化都无效被丢弃。

上面的例子使用的是 beginAsynchronous(…),实际上我们提供了三种同步、异步、不安全事务类型。

事务类型

异步事务

相应的方法:beginAsynchronous(…)。该方法立即返回并在后台串行队列执行其闭包内容

其中的 transaction 是 AsynchronousDataTransaction 类型。

同步事务

相应的方法 beginSynchronous(…)。句法与异步处理类似,但是同步处理是在transaction块完成后才返回值。

上面的 transaction 是 SynchronousDataTransaction 类型。

技术上来说 beginSynchronous(…) 会阻塞两条线程(回调线程和事务后台线程),这更不安全更容易形成死锁。注意这个闭包并不会阻塞其他线程。

不安全事务

不在闭包内进行数据更新:

该方法允许不连续的更新。但是同时你需要手动管理事件的并发性,这就是“能力越大责任越大”。

上面的例子我们还可以看出,只有不安全事务可以多次调用 commit(),在同步和异步事务中这么做会触发断言。

我们已经知道怎么创建事务,接下来我们要来介绍 BaseDataTransaction 的三种子类事务——创建、更新、删除。

创建对象

create(…) 方法接收一个Into分句,Into分句指明了你想要创建的对象实体:

这个句法非常直接,CoreStore并不只是插入一个新对象,这句代码做了以下几件事

  •  检查实体类型是否存在
  •  如果该实体只属于一个persistent store,新的对象就被插入该仓库并且通过 create() 返回
  •  如果不属于任何仓库,断言被触发。这是个不该发生的错误。
  •  如果该实体属于多个仓库,断言也会被触发。这也是个不该发生的错误。一般在这种情况下,使用Core Data你可以插入一个对象但是保存Context的时候会发生错误。CoreStore则在创建的时候就帮你判断这一项。

如果该实体存在在多个persistent store中,你需要提供 store 的名字:

如果使用 nil 这表示是默认的persistent store:

注意如果指明了配置名,CoreStore只会尝试在该 store 中插入新创建的对象,如果该 store 没有找到返回失败,并不会倒回查找该实例应该从属的 store。

更新对象

创建完对象后就可以更新了。

要更新已经存在的对象,首先要从事务中获取对象的实例:

不要更新不存在或者不是从事务中获取出来的实例。获取到该对象都就可以通过 transaction 的 edit(…) 方法获取其可编辑的实例。

还可以用来更新对象之间的关系。也要确保对象所属的关系是事务创造或者从中获取:

删除对象

删除对象简单很多,你只需要直接调用 transaction 的 delete 方法而不必获取可编辑版本:

还可以一次删除多个对象:

transaction还提供了deleteAll(…)方法来设置查询条件确定需要删除的对象

安全地传递对象

记住 DataStack 和单独的事务管理和 NSManageObjectContext 是不同的,所以 transaction 有一个 edit() 方法:

但是CoreStore,DataStack和BaseDataTransaction有个一非常灵活的fetchExisting方法,可以用来来回传值

fetchExisting(…) 还可以用于多个 NAManagedObject 或者 NSManagedObjectID:

导入数据

有时候我们存入Core Data的值是来自外部文件,例如网络服务器或者其他文件。所以如果你有一个JSON格式的字典,你可能会这样取值:

但是如果属性值很多,这一项重复工作量很大。使用 CoreStore 你只需要写一次,只要调用 importObject(…) 或者 importUniqueObject(…):

为了导入数据,你需要在 NSManagedObject 的子类中实现 ImportableObject 或者 ImportableUniqueObject 协议:

  •  ImportableObject:如果对象之间没什么不同点,新的对象通过调用 importObject(…) 进行添加
  •  ImportableUniqueObject:该协议在创建或者更新的时候为对象分配一个唯一ID,调用 importUniqueObject(…) 方法

这两种协议都要求制定一个ImportSource,可以使任何类型用于对象导入数据:

你可以从第三方JSON库或者直接从元组导入数据。

ImportableObject

ImportableObject是一个非常简单的协议。

首先将ImportSource设置成相应的数据类型,

这里将调用 importObject(_:source:)。其中,source 将使用 [String: AnyObject] 类型:

具体的数据提取和分配在 ImportableObject 协议的 didInsertFromImportSource(…) 方法中实现:

你还可以使用importObjects(_:sourceArray:)一次导入多个对象

这样迭代导入数组中的资源,然后再调用 shouldInsertFromImportSource(…) 来确定该创建哪个实例。你还可以进行验证,并通过shouldInsertFromImportSource(…) 返回 false 来跳过数组中其中一个数据的导入。

如果一个资源导入出现错误,所有的都会被取消。但是现在你可以在 didInsertFromImportSource(…) 中抛出一个错误。

这么做你可以直接放弃一个无效事务。

ImportableUniqueObject

一般我们不止导入数据还会进行更新数据。实现 Importable 协议可以让你给导入的每个数据添加一个唯一ID 用于搜索。

注意其插入方法与 ImportableObject 相同,另外添加了更新和设置唯一ID 的方法。

在 ImportableUniqueObject 中数据的提取和分配在 updateFromImportSource(…) 中实现,默认情况下 didInsertFromImportSource(…) 调用updateFromImportSource(…),但是你也可以分开插入和更新过程。

然后,你可以调用 transaction 的 importUniqueObject(…)  方法实现创建和更新对象。

或者调用 importUniqueObjects(…) 一次创建或更新多个对象。

在 ImportableObject 中,你可以通过实现 shouldInsertFromImportSource(…) 和 shouldUpdateFromImportSource(…) 选择跳过导入一个对象。或者通过向 uniqueIDFromImportSource(…)、didInsertFromImportSource(…) 或者 updateFromImportSource(…) 抛出一个错误来删除所有对象。

获取和查询

CoreStore中获取和查询是分开的:

  • 获取的过程是从指定事务或者数据栈中进行搜索,这表示fetch包含未提交的数据(transaction 调用 commit 之前)在下面几种情况下使用 fetch
  • 结果要是 NSManagedObeject 实例
  • 搜索结果要包含未保存的对象
  •  查询直接从 persistent store 中获取数据,这意味着在计算总和最大最小的时候速度更快,使用场景:
  • 需要使用聚集函数
  • 结果是原始数据类型,例如NSStrings、NSNumbers、Ints、NSDates、键值对NSDictionary等
  • 结果返回指定属性值
  • 不包括未保存的对象

From分句

获取和查询的条件使用分句进行设置,必须要设置From 分句,知名目标实体类型。

上述例子中,people 是 MYPersonEntity 类型的数组。From(MyPersonEntity) 表示获取所有的 MyPersonEntity 对象。

如果该实体存在在多个配置中,需要指明目标 persistent store 的配置名。

或者使用 nil 表示默认的配置名。

现在已经知道怎么使用 From 了。

获取

CoreStore 有五种,可以通过 DataStack 实例或者 BaseDataTransaction 实例用来获取的方法。这些方法所需参数都相同:必须要有一个 From 分句,可选的有Where、OrderBy和Tweak分句:

  •  fetchAll(…) :返回所有符合要求的值
  •  fetchOne(…) :返回符合条件的第一个值
  •  fetchCount(…) :返回符合条件值的数量
  • fetchObjectIDs(…):返回一个包含所有符合条件对象的NSManagedObjectID的队列
  •  fetchObjectID(…):返回符合条件的第一个值的NSManagedObjectID

每个方法的目的都很直接,下面我们来介绍这些分句。

Where 分句

Where 分句是对 NSPredicate 的包装,是获取的过滤器。除了CoreData不支持的 -predicateWithBlock:,其他初始化和NSPredicate一样。

如果你有一个 NSPredicate 实例,你可以将其传入 Where 分句:

Where 分句还支持 && || !这些逻辑运算,可以替代 AND、OR、NOT这些逻辑。

如果不调用 Where 分句,所有 From 条件下的对象都会被返回。

OrderBy 分句

OrderBy 是对 NSSortDescriptor 的封装,根据指定属性值进行排序。

OrderBy 中接收一连串 SortKey 枚举值,可以是 .Ascending 也可以是 .Descending。

你可以使用 + 和 += 运算符将两个 OrderBy 组合。

Tweak 分句

Tweak 闭包中暴露一个 AFetchRequest 值,让你可以改变属性值。

分句的执行时根据其在 fetch/query 中的顺序,因此一般你要把 Tweak 分句放在最后。

Tweak 闭包就是获取命令之前执行,所以要确保闭包中占用的任何值不存在竞争状态。

Tweak 只是让你最小程度上地对 NSFetchRequest 进行配置。记住,CoreStore 已经帮你预配置 NSFetchRequest 最合适的默认状态。当你对自己在做什么了解的时候才能使用Tweak。

查询

Core Data 的包装中所忽视的一个功能就是获取原始值。如果你对 NSDictionaryResultType 和 -[NSFetchedRequest propertiesToFetch] 比较熟悉,你就会知道ß获取原始值和聚合值的查询是多么麻烦。CoreStore通过暴露下面两个方法解决了这个问题:

  • queryValue(…) :然后属性或者聚合值的原始值,如果有多个值只返回第一个。
  •  queryAttributes(…) :返回一个队列其中包含的字典组成是属性key和相应的值。

上面的方法的参数都相同。一个必须的 From 分句,可选的 Select<T> 分句和 Where、OrderBy、GroupBy 或者 Tweak 分句。

From、Where、OrderBy 和 Tweak 分句与上面获取一样,接下来介绍 Select<T> 分句和 GroupBy 分句。

Select<T> 分句

该分句指定返回值类型和 key 属性:

上面的例子查询的是 “age” 这个属性,返回值是第一个符合条件的对象。johnsAge 会被界定在 Int 类型中,正如 Select<Int> 所示。在queryValue(…) 中以下是可设置的返回类型:

  •  Bool
  •  Int8
  •  Int16
  •  Int32
  •  Int64
  •  Double
  •  Float
  •  String
  •  NSNumber
  •  NSString
  •  NSDecimalNumber
  •  NSDate
  •  NSDate
  • NSManagedObjectID

对于 queryAttributes(…),Select 只能返回 NSDictionary,因此可以省略类型。

如果你只需要特殊特征值,下面几个聚合函数可以作为参数传入 Select:

  •  .Average(…)
  •  .Count(…)
  •  .Maximum(…)
  •  .Minimum(…)
  •  .Sum(…)

queryAttributes(…) 返回的是字典组成的数组,你可以在 Select 中指定多个属性:

上面的代码返回:

还可以包含聚合函数:

返回值如下:

count(friends) 你还可以用别名:

这个的返回值:

GroupBy 分句

GroupBy 分句将结果按照所指定的属性进行分组,该分句只可用于 queryAttributes(…),因为 queryValue(…) 值返回第一个值:

这段代码返回以下字典格式,显示每个 “age” 属性的数量:

日志记录和错误处理

在使用第三方库的时候,一个特别容易不爽的点就是控制台的输出依照的是他们特有的日志机制。CoreStore提供了默认的日志类,但是同时你还可以用过实现 CoreStoreLogger 进行自定义。

然后将这个类的实例传给CoreStore:

为了保持调用栈信息的完整,都没有进行线程管理。因此你要确保你的 logger 是线程安全的或者将日志实现分配到串行队列。

观察改变和本地通知

  • ObjectMonitor:用来监视单个 NSManagedObject 实例的变化(替代KVO)
  • ListMonitor:用来监视一连串 NSManagedObject 实例的变化(替代NSFetchedResultsController)

观察单一对象

为了观察某一对象,需要实现 ObjectObserver 协议并且指明 EntityType。

然后我们需要声明一个 ObjectMonitor 实例,然后将我们的 ObjectObserver 注册为观察者。

当该对象的属性发生变化时,控制器就会通知我们的观察者。你可以向一个 objectMonitor 添加多个 ObjectObserver。这就意味着多个屏幕分享一个 ObjectMonitor 实例。

你可以通过对象特征来获取 ObjectMonitor 的对象,如果对象被删除,object 将会返回 nil。

同时 ObjectMonitor 也暴露了 removeObserver(…) 这个方法。它只储存弱引用的观察者,并且可以安全注销释放观察者。

观察对象序列

为了观察对象序列, 需要实现 ListObserver 协议并且指明 EntityType。

包括 ListObserver 在内,有三个你可以实现的观察者协议,你可以根据你的具体需求选择实现:

  •  ListObserver:包含的回调方法。

  •  ListObjectObserver:ListObserver的补充,可以用来处理对象的增加删除和插入事件。

  •  ListSectionObserver:ListObjectObserver的补充,用来处理 section 的增加和删除。

然后创建一个 ListMonitor 实例并且将 ListObserver 注册为观察者。

同样的,一个 ListMonitor 也可以注册多个 ListObserver。

你应该注意到了,monitorList(…) 方法支持Where、orderBy 和 Tweak 分句,和fetch一样。

ListMonitor 的观察对象需要有确切的顺序,至少 From 和 OrderBy 子句是需要实现的。

通过 monitorList(…) 创建的 ListMonitor 实例是单一 section 的 list,因此你可以如下获取其中的内容:

如果list需要分成多个section,则需要通过monitorSectionedList(…)创建ListMonitor,并实现SectionBy分句。

以这种方式创建的 list 控制器会根据 SectionBy 分句中的属性值对对象进行分组。另外,OrderBy 的整理方式应该与 SectionBy 一样(与 NSFetchedResultsController 的原则相同)

SectionBy分句还可以通过闭包将section name转换成一个用于展示的字符串:

这在设置 TableView 的 header 上非常实用。

使用 NSIndexPath 或者元组获取对象。

路线图

  • 支持iCloud存储
  • CoreSpotlight能进行自动索引(实验性的)

安装

  • 需要: iOS 7及以上、Swift2.2(Xcode 7.3)
  • 依赖库: GCDKit

使用CocoaPods安装(不支持iOS7)

这里讲CoreStore作为一个框架进行安装。在你的 swift 文件中 import CoreStore 来使用这个库。

使用Carthage安装

在你的Cartfile中添加

然后运行

使用Git Submodule安装

  1. 将 CoreStore.xcodeproj 拖放入你的工程
  2. 作为一个框架进行安装(iOS7不支持)
  3. 将 CoreStore.xcodeproj 拖放入你的工程
  4. 直接加入你的 app 模块
  5. 将所有的 swift 文件添加进你的工程

v0.2.0到1.0.0的一些变化

  • 重新命名了一些类和协议,名字变得更短、关联性更强、更便于记忆:
  • ManagedObjectController 改为 ObjectMonitor
  • ManagedObjectObserver 改为 ObjectObserver
  • ManagedObjectListController 改为 ListMonitor
  • ManagedObjectListChangeObserver 改为 ListObserver
  • ManagedObjectListObjectObserver 改为 ListObjectObserver
  • ManagedObjectListSectionObserver 改为 ListSectionObserver
  • SectionedBy 改为 SectionBy(与OrderBy和GroupBy更匹配),为了保留自然语言语义,以上这些协议中的方法也进行了重命名。
  • 有些方法取消了返回enum结果,取而代之的是抛出错误
  • 新的迁移机制!请查看DataStack+Migration.swift和CoreStore+Migration.swift中的新方法,以及DataStack.swift新的初始化方法。

开源地址:https://github.com/JohnEstropia/CoreStore

1 1 收藏

资源整理者简介:西西里的仔仔

简介还没来得及写 :) 个人主页 · 贡献了12个资源 · 13 ·   


直接登录

推荐关注

按分类快速查找

关于资源导航
  • 伯乐在线资源导航收录优秀的工具资源。内容覆盖开发、设计、产品和管理等IT互联网行业相关的领域。目前已经收录 1439 项工具资源。
    推送伯乐头条热点内容微信号:jobbole 分享干货的技术类微信号:iProgrammer