Core Data概述

        昨晚熬夜看发布会(本以为屌丝终于能买得起苹果了,谁知道。。。),因为看不了视频直播,所以就正好有空就把www.objc.io最新的一篇文章翻译了一下,同时感谢CocoaChina翻译组提供校对,以下为正文:

        Core Data可能是OS X和iOS里面最容易被误解的框架之一了,为了帮助大家理解,我们将快速的研究Core Data,让大家对它有一个初步的了解,对于想要正确使用Core Data的同学来说,理解它的概念是非常必要的。几乎所有对Core Data感到失望的原因都是因为对它工作机制的错误理解。让我们开始吧:

 Core Data是什么?

        大概八年前,2005的四月份,Apple发布了OS X10.4,正是在这个版本中Core Data框架发布了。那个时候YouTube也刚发布。

        Core Data是一个模型层的技术。Core Data帮助你建立代表程序状态的模型层。Core Data也是一种持久化技术,它能将模型对象的状态持久化到磁盘,但它最重要的特点是:Core Data不仅是一个加载、保存数据的框架,它还能和内存中的数据很好的共事。

        如果你之前曾经接触过 Object-relational maping (O/RM):Core Data不仅是一种O/RM。如果你之前曾经接触过SQL wrappers:Core Data不是一个SQL wrapper。它默认使用SQL,但是,它是一种更高级的抽象概念。如果你需要的是一个O/RM或者SQL wrapper,那么Core Data并不适合你。

        对象图形管理是Core Data提供最强大的功能之一。为了更好利用Core Data,这是你需要理解的一块内容。

        还有一点要注意:Core Data是完全独立于任何UI层级的框架。它是作为模型层框架被设计出来的。在OS X中,甚至在一些后台驻留程序中,Core Data都起着非常重要的意义。

堆栈

        Core Data有相当多可用的组件。这是一个非常灵活的技术。在大多数的使用情况下,设置都是相当简单。

        当所有的组件都捆绑到一起的时候,我们把它称作Core Data堆栈,这个堆栈有两个主要部分。一部分是关于对象图形管理,这正是你需要很好掌握的那一部分,并且知道怎么使用。第二部分是关于持久化,比如,保存你模型对象的状态,然后再恢复模型对象的状态。

        在两个部分之间,即堆栈中间,是持久化存储协调器(PSC)。它将对象图形管理部分和持久化部分捆绑在一起,当它们两者中的任何一部分需要和另一部分交流时,这便需要PSC来调节了。

stack-complex

        对象图形管理是你程序模型层的逻辑存在的地方。模型层的对象存在于一个context内。在大多数的设置中,存在一个context,并且所有的对象存在于那个context中。Core Data支持许多contexts,但是,对于更多高级的使用情况,每个context和其他context区分的都很清楚。需要记住的是,对象和他们的context是相关联的,每个被管理的对象都知道自己属于哪个context,并且每个context都知道自己管理着哪个对象。

        堆栈的另一部分就是持久化发生的地方了,即Core Data从文件系统中读或写的地方。每个持久化存储协调器(persistent store coordinator)都有一个属于自己的持久化存储,并且这个store在文件系统中与SQLite数据库交互。为了支持更高级的设置,Core Data可以将多个存储附属于同一个持久化存储协调器,并且除了存储SQL格式外,还有很多存储类型可供选择。

        最常见的解决方案如下图所示:

stack-simple

组件如何一起工作

        让我们快速的看一个说明组件如何协同工作的例子。在我们a full application using Core Data的文章中,我们正好有一个实体,即一种对象:我们有一个Item 实体对应一个title。每一个item可以拥有子items,因此,我们有一个父子关系。

        这是我们的数据模型,正如我们在Data Models and Model Objects文章中提到的一样,在Core Data中有一种特别的对象——实体。在这种情况下,我们只有一个实体:Item实体。同样的,我们有一个NSManagedObject的子类,叫做Item。这个Item实体映射到Item类上。在data models article中会详细的谈到这个。

        我们的程序仅有一个根Item。这并没有什么奇妙的地方。这是一个我们用来显示底层item等级的Item。这是一个我们永远不会为其设置父类的Item。

        当程序运行时,我们像上面图片描绘的一样设置我们的堆栈,一个存储,一个managed object context,持久化存储协调器来将他们两个关联。

        在第一次运行时,我们并没有任何items。我们需要做的第一件事就是创建根item。你通过将他们插入context来增加管理对象。

创建对象

        插入对象的方法似乎很笨重,我们通过NSEntityDescription的方法来插入:

+ (id)insertNewObjectForEntityForName:(NSString *)entityName inManagedObjectContext:(NSManagedObjectContext *)context;

        我们建议你增加两个方便的方法到你的模型类中:

+ (NSString *)entityName
{
    return @“Item”;
}
+(instancetype)insertNewObjectInManagedObjectContext(NSManagedObjectContext *)moc;
{
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:moc];
}

        现在,我们可以像这样插入我们的根对象了:

Item *rootItem = [Item insertNewObjectInManagedObjectContext:managedObjectContext];

        现在,在我们的managed object context(MOC)中有一个唯一的item。Context知道这是一个新插入进来需要被管理的对象,并且被管理的对象rootItem知道这个Context(它有一个-managedObjectContext方法)。

保存改变

        这时候,可是我们还是没有接触到持久化存储协调器或持久化存储。新的模型对象—rootItem,仅仅在内存中。如果我们想要保存模型对象的状态(在这种情况下只是一个对象),我们需要保存context:

NSError *error = nil;
if (! [managedObjectContext save:&error]) {
    // Uh, oh. An error happened.
}

        这个时候,很多事情将要发生。首先发生的是managed object context计算出改变的内容。这是context的职责,追踪出任何你在context管理对象中做出的改变。在我们的例子中,我们到现在做出的唯一改变就是插入一个对象,即我们的rootItem。

        然后Managed object context将这些改变传给持久化存储协调器,让它将这些改变传播给store。持久化存储协调器会协调store来将我们插入的对象写入到磁盘上的SQL数据库。NSPersistentStore类管理着和SQLite的实际交互,并且产生需要被执行的SQL代码。持久化存储协调器的规则就是简单的调整store和context之间的交互。在我们的例子中,这个规则相当简单,但是,复杂的设置可以有多个stores和多个contexts。

更新关系

Core Data的能力在于管理关系。让我们着眼于简单的情况:增加我们第二个item,并且使它成为rootItem的子item:

Item *item = [Item insertNewObjectInManagedObjectContext:managedObjectContext];
item.parent = rootItem;
item.title = @"foo";

        好了。这些改变再次的仅仅存在于managed object context中。一旦我们保存了context, managed object context将会告诉持久化存储协调器,像增加第一个对象一样增加新创建的对象到数据库文件中。但这也将会更新从第二个item到第一个item之间的关系,或从第一个到第二个item的关系。记住Item实体是如何有一个父子关系的。同时他们之间有相反的关系。因为我们设置第一个item为第二个item的父类时,第二个item将会变成第一个item的子类。managed object context追踪这些关系,持久化存储协调器和store保存这些关系到磁盘。

弄清对象

        我们已经使用我们程序一会儿了,并且已经为rootItem增加了一些sub-items,甚至增加sub-items到sub-items。然后,我们再次启动我们的程序。Core Data已经将这些items之间的关系保存到了数据库文件。对象图形是持久化的。我们现在需要取出根item,所以我们可以显示底层items的列表。有两种方法。我们先看简单点的方法。

        当根Item对象创建并保存之后我们可以向它请求它的NSManagedObjectID,这是一个不透明的对象,可以唯一代表该根Item。我们可以保存这个对象到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是否真正返回一个有效值。

managed object context要求持久化存储协调器从数据库取得指定的对象。根对象现在被恢复到context中。然而,其他所有的items仍然不在内存中。

rootItem有一个子关系。但那儿还没有什么。我们想要显示rootItem的子item,因此我们需要调用:

NSOrderedSet *children = rootItem.children;

        现在发生的是,context标注这个rootItem的子item为默认。Core Data已经标注这个关系为仍需要被解决。既然我们已经在这个时候访问了它,context将会自动配合持久化存储协调器来将这些子 items载入到context中。

        这听起来可能非常不重要,但是在这个时候真正发生了很多事情。如果任何子对象偶然发生在内存中,Core Data保证会复用那些对象。这是Core Data独一无二的功能。在context内,从不会存在第二个相同的单一对象来代表一个给定的item。

        第二,持久化存储协调器有它自己内部对象值的缓存。如果context需要一个指定的对象(比如一个子item),并且持久化存储协调器在缓存中已经有需要的值,那么,对象可以不通过store而被直接加到context。这很重要,因为访问store就意味着执行SQL代码,这比使用内存中存在的值要慢很多。

        随着我们遍历item的子item,以及子item的子item,我们慢慢地把整个对象图形引用到了managed object context。而这些对象都在内存中之后,操作对象以及传递关系就会变得非常快,既然我们只是在managed object context里操作。我们跟本不需要访问持久化存储协调器。在我们的Item对象上访问title,父子属性是非常快而且高效的。

        由于它会影响性能,所以了解数据在这些情况下怎么取出来是非常重要的。在我们特定的情况下,由于我们并没接触到太多的数据,所以这并不算什么,但是一旦你接触了,你将需要了解在背后发生了什么。

        当你遍历一个关系时(比如在我们例子中的父关系或子关系)下面三种情况将有一种会发生:(1)对象已经在context中,这种操作基本上是没有任何代价的。(2)对象不在context中,但是因为你最近从store中取出过对象,所以持久化存储协调器缓存了对象的值,这个操作还算廉价(但是,一些操作会被锁住)。操作耗费最昂贵的情况是(3),当context和持久化存储协调器都是第一次访问这个对象,这中情况必须通过store从SQLite数据库取回。第三种情况比(1)和(2)需要付出更多代价。

        如果你知道你必须从store取回对象(你没有这些对象),当你限制一次取回多少个对象时,将会产生很大的不同。在我们的例子中,我们希望一次性取出所有child items,而不是一个接一个。我们可以通过一个特别的技巧NSFetchRequest,但是我们要注意,当我们需要做这个操作时,我们只需要执行一次取出请求,因为一次取出请求将会造成选项(3)发生;这将总是访问SQLite数据库。因此,当需要显著提升性能时,检查对象是否已经存在将变得非常有意义。你可以使用-[NSManagedObjectContext objectRegisteredForID:] 来检测一个对象是否已经存在。

改变对象的值

现在,我们可以说,我们已经改变我们一个Item对象的title:

item.title = @"New title";

当我们这样做时,items的title改变了,此外,managed object context会标注这个item已经被改变,这样当我们在context中调用-save:时,这个对象将会通过持久化存储协调器和附属的store保存起来。context最关键的职责之一就是跟踪任何改变。

从最后一次保存开始,context知道哪些对象被插入,改变,删除。你可以通过-insertedObjects, -updatedObjects, and –deletedObjects方法来执行这些操作。同样的,你可以通过-changedValues方法来询问一个被管理的对象哪些值被改变了。这个方法正是Core Data能够将你做出的改变推入到数据库的原因。

当我们插入一个新的Item对象时,Core Data知道需要将这些改变存入store。那么,将你改变对象的title时,也会发生同样的事情。

保存values需要协调持久化存储协调器和持久化store依次访问SQLite数据库。当和在内存中操作对象比起来,取出对象和值,访问store和数据库是非常耗费资源的。不管你保存了多少更改,一次保存的代价是固定的。并且每个变化都有成本,这仅仅是SQLite的工作。当你做很多更改的时候,需要将更改打包,并批量更改。如果你保存每一次更改,将要付出很高的代价,因为你需要经常做保存操作。如果你很少做保存,那么你将会有一大批更改交给SQLite处理。

同样需要注意到,保存操作是原子性的。他们都是事务。要么所有的更改会被提交给store/SQLite数据库,要么任何更改都不被保存。当实现自定义NSIncrementalStore基类时,这一点一定要牢记在心。要么确保保存永远不会失败(比如说不会发生冲突),要么当保存失败时,你store的基类需要恢复所有的改变。否则,在内存中的对象图形最终和保存在store中的对象不一致。

如果你使用一个简单的设置,保存操作通常不会失败。但是Core Data允许每个持久化存储协调器有多个context,所以你可能陷入持久化存储协调器层级的错误。改变是对于每个context的,另一个context的更改可能导致冲突。Core Data甚至允许完全不同的堆栈访问磁盘上相同的SQLite数据库。这明显也会导致冲突(比如,一个context想要更新一个对象的值,而另一个context想要删除这个对象)。另一个导致保存失败的原因可能是验证。Core Data支持对象复杂的验证政策。这个问题比较高级。一个简单的验证规则可能是:Item的标题不能超过300个字符。但是Core Data也支持通过属性进行复杂的验证政策。

结束语

        如果Core Data看起来让人害怕,这最有可能是因为它的灵活性允许你可以通过非常复杂的方法使用它。始终记住:尽量保持简单,它会让开发变得更容易,并且把你和你的用户从麻烦中拯救出来。除非你确信它会带来帮助,才去使用更复杂的东西,比如说是background contex。去使用一个简单的Core Data堆栈,并且使用我们在这篇文章中讲到的知识吧,你将很快会真正体会到Core Data能为你做什么,并且学到它是怎么缩短你开发周期的。