前言
Core Data可能是OS X与iOS中最容易被误解的框架。这篇文章的意义在于让你理解Core Data的本质以及正确的使用Core Data。
Core Data是啥
大苹果发布OSX 10.4的时候第一次把Core Data框架丢了出来。那时YouTube刚出来。
Core Data是数据层技术(毫无疑问)。CD帮你创建数据层并反映你APP的状态。CD同时也是一个永久化(固化)技术,它可以将数据对象值存储到磁盘。但是重要一点是CD绝不仅仅是一个用来读取保存数据的框架。在设计到内存的时候,它同样能够对数据进行操作。
如果你曾经整过关系型数据:CD绝对不属于这个范畴。它意味着更多。如果你用过SQL封装:CD据对不是类SQL得封装。CD确实是可以使用SQL做存储,但是它是更高层次的一种封装。如果你是奔着前两者去的,那么CD不适合你。
CD最牛逼的一个功能是它提供了对象图管理。这块CD的特性是你需要去了解以便后续使用CD的时候能发挥出它得功效。
另外一点:CD与UI级别的框架是完全解耦的。这说明它设计之初就是完全的数据层框架。同时在OSX平台上还设计了很多驻后台类似的功能。
栈
CD是有几个模块组成的,是一种十分灵活的技术。在大多数的使用场景,创建过程相对简单。
当所有的模块组合到一起的时候,我们一般叫它们为CD栈。这个栈主要由两部分组成。一部分是关于对象图管理,而且这部分是你需要很好理解并知道怎么使用的一块。第二部分是关于固化,比如保存数据对象的状态(值)并且再次去检索(获取)该状态(值)。
在两个模块之间,栈的中间件是Persistent Store Coordinator(PSC,容我翻译成永久层切换者,属于桥接模式)
。PSC将对象图管理部分与固化部分联系在了一起。当这两个模块需要对话的时候,这时候PSC就上场了。
对象图管理正是你应用数据层逻辑活动的区域。数据层对象活动在数据上下文之中。大多数创建,只有一个数据上下文并且所有对象都在之中进行操作。CD支持多数据上下文来适应更高级的使用场景。注意到每个上下文都是区别开来的(马上可以接触到)。你需要记住的一个重要点是对象需要跟它们的上下文绑定在一起。每一个被管理的对象知道自己在哪个上下文,每个上下文知道自己管理哪些对象。
栈的另外一部分是固化发生现场,举个栗子,CD从文件系统进行读写的发生现场。几乎所有得场景,PSC只连接这一个所谓得固化仓库,并且这个固化仓库与文件系统中的SQLite数据库进行交互。更高级一点,CD支持一个PSC连接多个数据仓库,这些数据仓库可以是不同的存储方式而不仅仅是SQLite。
最常见得场景,如下:
如何让各组件协作
举个栗子快速解释如何协作。(这里拿作者工程。。这里不提供),假设我们有一个实体,比如我们拿一个实体去存一个标题。每一个单件都有一个子单件,这样我们就有数据的父子关系。
(大意)我们通过继承NSManagedObject
实例化出与实体同名对象,这样可以让单件实体可以映射到单件对象上。
我们的应用有单一的根单件。它没有什么神奇的地方。它只是简单的单件让我们用来展示单件层级的底端。当然它不再会有父亲结点。
当应用启动的时候,我们创建上面所描述的栈,使用一个仓库,一个管理对象上下文,然后用一个固化仓库切换器连接它们。
在我们第一次启动的时候,我们没有任何数据。我们要做的是创建根单件。你需要插入这些管理对象到上下文中。
创建对象
看上去可能很笨重,当我们要插入对象(数据)的时候需要调用
+ (id)insertNewObjectForEntityForName:(NSString *)entityName
inManagedObjectContext:(NSManagedObjectContext *)context
方法作用在NSEntityDescription
。我们建议你添加两个便利的方法到模型类:
+ (NSString *)entityName
{
return @“Item”;
}
+ (instancetype)insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)moc;
{
return [NSEntityDescription insertNewObjectForEntityForName:[self entityName]
inManagedObjectContext:moc];
}
从而我们可以这样添加我们的根对象:
Item *rootItem = [Item insertNewObjectInManagedObjectContext:managedObjectContext];
现在我们管理对象上下文有了一个单件。上下文也知道新添加得对象,并且新添对象也知道自己的上下文。
保存变化
说到这个,虽然我们还没有接触PSC以及固化仓库。新添对象,rootItem
已经在内存中了。如果我们需要保存我们的模型对象,我们需要保存上下文。
NSError *error = nil;
if (! [managedObjectContext save:&error]) {
// Uh, oh. An error happened. :(
}
说到这时,有很多即将发生的。首先管理对象上下文计算出哪里发生了变化。上下文责任所在就是记录下其中任何管理对象任意以及所有的变化。在我们的场景里,我们做的变化就是插入了rootItem
.
然后管理对象上下文传递这些变化到固化仓库切换器PSC并且让它将变化传递到数据仓库去。PSC将我们插入的对象写到SQL中去并在磁盘中保存起来。NSPersistentStore
类是真正与SQL进行交互的实体并且生成要执行的SQL代码。PSC的角色仅仅是连接仓库与上下文。在我们的栗子中,这个角色相对简单,但是复杂的创建将涉及到多仓库多上下文。
更新关系
CD的强大之处在于管理数据关系。让我们举个简单得栗子通过添加一个新的第二个单件来让它成为rootItem
的子单件。
Item *item = [Item insertNewObjectInManagedObjectContext:managedObjectContext];
item.parent = rootItem;
item.title = @"foo";
再提一次这些变化只在上下文中发生改变。一旦我们存储了上下文,上下文会通知PSC去添加一个新增的对象到数据库文件就想我们第一次添加对象那样。但是它也会更新上述第二单件与第一单件的关系以及第一单件与第二单件的关系。记得怎么使单件实体之间有父子关系。同样倒置过来他们关系相反。因为我们设置了第一个单件是第二个单件的父亲,那么第二单件是第一个单件的孩子。管理对象上下文记录这些关系然后PSC和数据仓库保存这些关系到磁盘上。
获取对象
我们已经创建一些子单件以及子单件的子单件。然后我们重启当我们的APP。CD已经在数据库文件存储了这些单件之间的关系。对象图已经被固化了。我们需要获取我们的根单件,这样我们可以展示单件列表的底层。有两种方式可以获取,首先我们先来看第一种。
当我们创建我们的根单件对象的时候,一旦我们保存了它,我们可以通过它的NSManagedObjectID
来访问到它。NSManagedObjectID
是个晦涩的对象仅仅表示根对象。同样我们可以将它保存到像NSUSerDefaults
中去,比如:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setURL:rootItem.managedObjectID.URIRepresentation forKey:@"rootItem"];
现在当我们的应用重启的时候,我们可以这样获取我们想要的对象:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSURL *uri = [defaults URLForKey:@"rootItem"];
NSManagedObjectID *moid = [managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:uri];
NSError *error = nil;
Item *rootItem = (id) [managedObjectContext existingObjectWithID:moid error:&error];
显然,在实际编码的时候我们需要去确定NSUserDefaults
是否返回一个有效值。
这里发生的是管理数据上下文命令PSC去数据库获取特定的对象。根对象被取回到了上下文。但是其他对象还没有到内存中。
根单件有一个关系叫做孩子。但是现在啥都没有。我们要展示根单件的子单件,所以我们继续调用:
NSOrderedSet *children = rootItem.children;
此时发生的是上下文根单件的孩子关系出现了错误。CD将这关系标注作为即将解决的的事项。然后当我们访问到这里时候,上下文会自动地根据PSC指向来获取子单件到上下文中来。
这些听起来都没啥感觉,但是这里确确实实有许多工作要做。如果碰巧所有得子对象都在内存中,那么CD会确定它会重用这些对象。这就是唯一化。在上下文中,不会有多余(仅有一个)的对象来表示已有的单件。
其次,PSC有其关于对象值内部缓存。如果上下文需要一个特定的对象(比如子单件),如果PSC在其缓存中已有这个对象,那么这个对象不用再去数据仓库加载而是被直接添加到上下文中。这十分重要,因为访问仓库意味着调用SQL的代码,这样显然从速度上跟读取已在内存的对象相比就慢多了。
我们继续遍历我们子单件的子单件,我们将整个对象图搬到管理对象上下文中。一旦它们全在内存中了,操作这些对象以及遍历对象之间的关系就变得超级快了,因为我们仅仅是在上下文中进行对象操控。我们不需要跟PSC打交道了。这时候访问单件标题,父亲,孩子属性非常的快速而且效率很高。
理解场景中这些数据怎么抓取十分重要,因为它影响到性能。同来来说,它无关紧要,因为我们不会处理大量的数据。但是只要涉及到了,你必须了解到底其中的过程是什么。
当你遍历关系的时候,一件事情会发生:1)对象已经在上下文中此时遍历消耗代价很小。2)对象不在上下文中,PSC缓存这个对象,因为你最近从仓库中获取过该对象。代销还是比较小的。3)代价比较大的是对象第一次从SQL数据库获取,这样的消耗远大于1与2.
如果你知道你不得不去从数据仓库获取对象,那么你是否限制每次抓取的数据量将产生巨大的差别。在我们栗子中,我们可能一次性抓取所有得孩子而不是一个接一个。这活儿可以靠特定类NSFetchRequest
来搞定。但是我们必须注意仅仅是当我们需要的时候才去调用抓取请求,因为抓取请求一样会带来上文3的消耗;它通常是要访问数据库的。所以,当涉及到性能问题,确认对象是否已经存在就变得有意义了。你可以调用来-[NSManagedObjectContext objectRegisteredForID:]
来判断。
改变对象的值
现在,我们来试着改变我们单件对象的标题值:
item.title = @"New title";
当我们这么做的时候,单件标题会发生改变。但是附带说下,管理对象上下文将标记特定的管理对象(这个单件)为修改过的,这样它将会被保存到PSC和附带的数据仓库中当我们在上下文上调用-save:
的时候。上下文的一个职责就是记录改变。
上下文知道哪一个对象被插入,改变,或者是删除自从上一次的保存动作后。你可以进行对应的操作:-insertedObjects, -updatedObjects, and -deletedObjects
。同样地,你可以让被改变值的管理对象调用-changedValues
方法。你也可以不用这么做。但是这是CD通常要推送反馈到数据仓库的你所做的变化。
当我们插入一个新的单件对象,这是CD如何知道它需要推送这些变化到仓库。同样我们改变了标题,同样的事情也就发生了。
此处略三段。。。。(偷懒)
总结
CD看上去令人生畏,那是因为它提供了需要便利却表达方式比较复杂。准则:让每件事尽量简单。它可以让开发变得简单以及让你与你的用户避免麻烦。只有在你觉得更复杂的功能对你有帮助的时候才使用它们比如后台上下文。
当你使用一个简单的CD栈,并且你如上文所述那样使用它,你讲快速的学会欣赏CD为你带来的一切,并且能加速你的开发周期。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。