你的位置:首页 > 操作系统

[操作系统]Core Data(数据持久化)


Core Data可能是OS X和iOS中最容易被误解的框架之一了。为了帮助大家理解,我们将快速研究Core Data,来看一下它是关于什么的。为了正确使用Core Data, 有必要理解其概念。几乎所有Core Data引起的挫败,都是因为不理解它能做什么和它是怎么工作的。让我们开始吧。

Core Data是什么?

大概8年前,在2005年4月,Apple发布了OS X 10.4版本,第一次引入了Core Data框架。那时YouTube也刚发布。

Core Data是模型层的技术。Core Data帮助你构建代表程序状态的模型层。Core Data也是一种持久化技术,它可以将模型的状态持久化到磁盘。但它更重要的特点是:Core Data不只是一个加载和保存数据的框架,它也能处理内存中的数据。

如果你曾接触过Object-relational mapping(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也起着重要的意义。

 

堆栈The Stack

Core Data中有不少组件,它是一种非常灵活的技术。在大多数使用情况里,设置相对来说比较简单。

当所有组件绑定在一起,我们把它们称为Core Data Stack. 这种堆栈有两个主要部分。一部分是关于对象图管理,这是你需要掌握好的部分,也应该知道怎么使用。第二部分是关于持久化的,比如保存模型对象的状态和再次恢复对象的状态。

在这两部分的中间,即堆栈中间,是持久化存储协调器(Persistent Store Coordinator, PSC),也被朋友们戏称做中心监视局。通过它将对象图管理部分和持久化部分绑在一起。当这两部分中的一部分需要和另一部分交互,将通过PSC来调节。

对象图管理是你的应用中模型层逻辑存在的地方。模型层对象存在于一个context里。在大多数设置中,只有一个context,所有的对象都放在这个context中。Core Data支持多个context,但是是针对更高级的使用情况。需要注意的是,每个context和其他context区分都很清楚,我们将要来看一点这部分内容。有个重要的事需要记住,对象和他们的context绑定在一起。每一个被管理的对象都知道它属于哪个context,每一个context也知道它管理着哪些对象。

堆栈的另一部分是持久化发生的地方,比如Core Data从文件系统读或写。在所有情况下,持久化存储协调器(PSC)有一个属于自己的的持久化存储器(persistent store),这个store在文件系统和SQLite数据库交互。为了支持更高级的设置,Core Data支持使用多个存储器附属于同一个持久化存储协调器,并且除了SQL,还有一些别的存储类型可以选择。

一个常见的解决方案,看起来是这个样子的:

 

组件如何一起工作

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

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

我们的应用仅有一个根item。这里面没有什么奇妙的地方。这只是个简单的我们用在底层的item。这是一个我们永远不会为其设置父类的item.

当app启动,我们没有任何item。我们需要做的第一件事是创建一个根item。你通过插入对象到context里来添加可管理的对象。

 

创建对象

可能看起来有点笨重。我们通过NSEntityDescription的方法来插入:

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



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

1
2
3
4
5
6
7
8
9
10
+ (NSString *)entityName
{
   return @“Item”;
}
 
+ (instancetype)insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)moc;
{
   return [NSEntityDescription insertNewObjectForEntityForName:[self entityName]
                                        inManagedObjectContext:moc];
}



现在,我们可以插入我们的根对象:

1
Item *rootItem = [Item insertNewObjectInManagedObjectContext:managedObjectContext];



现在在我们的managed object context里有了一个唯一的item. context知道这个新插入的被管理对象,这个被管理对象rootItem也知道这个context(它有 -managedObjectContext方法)。

 

保存改变

到目前为止,虽然我们还没有碰到持久化存储协调器或者持久化存储器。这个新的模型对象,rootItem,只是在内存中。如果我们想保存我们模型对象的状态(我们的情况里就是rootItem),我们需要这样保存context:

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



现在,有很多事将要发生。首先,managed object context算出改变的内容。Context有责任去记录你对context里任何被管理的对象做出的改变。在我们的例子里,我们迄今为止唯一的改变是往里插入了一个对象,我们的rootItem.

这个managed object context把这些变化传递给持久化存储协调器,让它把改变传递给store。持久化存储协调器协调store(在我们的例子里,是一个SQL存储器)把我们新插入的对象写入磁盘中的SQL数据库里。NSPersistentStore类管理着和SQLite的真正交互,并且生成需要被执行的SQL代码。持久化存储协调器的角色只是简单的协调store和context之间的交互。在我们的例子里,这个角色相对简单,但是更复杂的应用里可以有多个store和多个context.

 

更新关系

Core Data的重要能力是它可以管理关系。我们看一个简单的例子,加第二个item,把它设为rootItem的子item。

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



好了。再次注意,这些改变只是在managed object context里面。一旦我们保存了context,managed object context就会告诉持久化存储协调器去把那个新建的对象添加到数据库文件中,像我们的第一个对象一样。但是它也同样会更新从我们第二个item到第一个的关系,或从第一个对象到第二个的关系。记住一个Item实体是如何有了父子关系。同时他们也有相反的关系。因为我们把第一个item设为第二个的父类,第二个就会是第一个的子类。Managed object context记录了这些关系,持久化存储协调器和store持久化(比如保存)这些关系到磁盘。

 

弄清对象

假设我们已经使用了我们的app一段时间,并且已经添加了一些子items到根item,甚至一些子items到子items。我们再次启动app,Core Data已经在数据库文件中存储了这些item之间的关系,对象图已经存在了。现在我们需要取出我们的根item, 这样我们可以显示底层items列表。我们有两种办法来实现这个,我们先来看一个简单的。

当根Item对象创建并保存后,我们可以获取它的NSManagedObjectID。这是一个不透明的对象,只代表根Item对象。我们可以把它保存到NSUserDefaults, 像这样:

1
2
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setURL:rootItem.managedObjectID.URIRepresentation forKey:@"rootItem"];



现在,当我们的app运行中,我们可以像这样取回这个对象:

1
2
3
4
5
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];



当然,在一个真正的app中,我们需要检查NSUserDefaults是否返回了一个有效的值。

刚才发生的事情是,managed object context让持久化存储协调器去从数据库里获取指定的对象。那个root对象现在就被恢复到了context中。但是所有其他的item还不在内存中。

rootItem有一个关系叫子关系,但是那儿还什么也没有。我们想显示rootItem的所有子item,所以我们调用:

1
NSOrderedSet *children = rootItem.children;



现在发生的是,context注意到要获取rootItem有关联的children,会得到一个所谓的故障。Core Data已经标记了这个关系作为一件需要被解决的事。既然我们已经在这个时候访问了它,context现在会自动和持久化存储协调器去协调,来载入这些子item到context里。

这个听起来可能是不重要的,但是事实上这个地方有很多事情发生。如果子对象中碰巧有一些已经在内存中了,Core Data需要保证它会重用这些对象。这就叫做唯一性。在context中,从来不会有多于一个对象对应于一个给定的item.
其次,持久化存储协调器有它自己内部对象值的缓存。如果context需要一个指定的对象(比如一个子item),并且持久化存储协调器已经在内存中有它需要的值了,这个对象可以被直接加到context,不需要通过store。这个很重要,因为使用store意味着执行SQL语句,这要比直接用内存中的值慢很多。

我们继续从一个item到子item再到子item,我们慢慢的把整个对象图加载到managed object context。但是一旦它们都在内存中,操作对象,或者获取它们的关系都是很快的,因为我们只是在managed object context中工作。我们完全不需要和持久化存储协调器打交道。这时候获取我们的Item对象的title, parent, children属性都很快也很方便。

理解在这些情况中数据是怎么获取的很重要,因为它影响性能。在我们的这个例子里,它不太重要,因为我们没有使用很多数据。但是一旦你开始使用,你就需要理解背后发生了什么。

当你获取一种关系(比如我们的例子是父子关系),下面三种情况中的一个会发生: (1) 这个对象已经在context中,获取基本上没有开销。(2)这个对象不在context中,但是因为你最近从store中获取过这个对象,持久化存储协调器缓存了它的值。这种情况适当便宜一些(虽然一些操作会被锁住)。开销最大的情况是:(3)当这个对象被context和持久化存储协调器都第一次访问,这样它需要被store从SQLite数据库中取出来。这种情况要比1和2开销大很多。

如果你知道你需要从store中获取对象(因为你还没有它们),当你限制一次取回多少个对象时,将会产生很大不同。在我们的例子里,我们可能需要一次获取所有的子item,而不是一个接一个。这可以通过一个特殊的NSFetchRequest来实现。但是我们一定要小心只是在我们需要的时候才执行一次取出请求。因为一个取出请求将会引起(3)发生,它总是需要通过SQLite数据库来获取。因此,如果性能很重要,检查对象是否已经存在就很必要。你可以使用 -[NSManagedObjectContext objectRegisteredForID:]来检测一个对象是否已经存在。

 

改变对象的值

现在,比如我们要改变一个item对象的title:

1
item.title = @"New title";



当我们这么做的时候,这个item的title就改变了。但是同时,managed object context会把这个item标记为已经改变,这样当我们调用context的-save:,它将会通过持久化存储协调器和相应的store保存起来。context的一个重要职责就是标记改变。

context知道从上次保存后,哪些对象已经被插入,改变和删除。你可以通过-insertedObjects, -updateObjects, -deletedObject这些方法来获取。同样的,你也可以通过-changedValues方法问一个被管理的对象,它的哪些值变了。你可能从来都不需要这么做。但是这是Core Data可以把改变保存到支持的数据库中的方式。

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

保存值需要和持久化存储协调器还有持久化store依次访问SQLite数据库。当恢复对象和值时,使用store和数据库比直接在内存中操作数据相对耗费资源。保存有一个固定的开销,不管你要保存的变化有多少。并且每次变化都有成本,这只是SQLite的工作。当你改变很多值时,需要将变更打包,并批量更改。如果你每次改变都保存,需要付出昂贵的代价,因为你需要经常保存。如果你保存次数少一些,你将会有有一大批更改交由SQLite来处理。

需要注意的是保存是原子性的。它们都是事务。或者所有的改变都被提交到store/SQLite数据库,或者任何改变都不会被保存。当你实现自定义的NSIncrementalStore子类的时候,这点很重要应该要记住。你可以保证保存永远不会失败(比如因为冲突),或者你的store子类需要在保存失败时恢复所有改变。否则,内存中的对象图会和store中的不一致。

如果你只是使用简单的设置,保存通常不会失败。但是Core Data允许多个context对应一个持久化存储协调器,所以你可能会在持久化存储协调器中遇到冲突。改变是每一个context的,另一个context可能会引入有冲突的改变。Core Data甚至允许完全不同的堆栈都访问同一个在磁盘中的SQLite数据库文件。这显然也可能引发冲突(比如,一个context想更新一个object的一个值,但是这个object已经被另一个context删除了)。另一个导致保存失败的原因可能是校验。Core Data支持对对象的复杂校验规则。这是个高级的话题。一个简单的校验可以是,一个Item的title长度一定不能超过300字符。但是Core Data也支持对属性的复杂的校验规则。

 

结束语

如果Core Data看起来让人畏缩,可能主要因为它可以让你通过复杂的方式来灵活使用。始终记住:尽可能让事情保持简单。这会让开发更容易,让你和你的用户免于麻烦。只在你确定会有帮助的情况下,才使用像background contexts这些更复杂的东西。

当你使用一个简单的Core Data堆栈,并且你用我们在本文中提到的方法来使用managed objects,你会很快学会欣赏Core Data可以为你做的事情,和它怎么帮你缩短开发周期。