第三遍看Core Data Programming Guide

虽然从接触iOS开发开始,做的每一个项目都在用Core Data,但是一些比较底层的东西都是boss写的或者用的是Restkit这个开源项目。所以虽然之前看过两遍Core Data Programming Guide,还有很多不理解或者不熟练或者做得不好的地方。所以第三遍看,想要把以前不理解的地方弄得清晰一点。

Core Data框架是独立于Cocoa的,所以对于没有界面的程序,也可以应用Core Data。为了方便,这里我自己试验的代码都是在一个Command Line tool工程里的0.0


关于Persistent Stack



对象和外部数据存储,这两者之间的媒介,被整体叫做persistence stack。其中,managed object context位于栈顶,persistent object store位于栈底,中间的是persistent store coordinator。

Persistent stack

实际上,是persistent store coordinator决定着这个栈。它使用了facade模式,使得栈底的多个persistent store,在呈现给context的时候,就像一个整体一样。
一个coordinator只能和一个managed object model相关联。


关于Managed Object Model



一个managed object model是NSManagedObjectModel类的实例。它描述了第三方app中需要使用到的一系列entity,和多个entity之间的关系。
一个model中可能有很多NSEntityDescription对象来代表这个model的各个entity。对于每个entity来说,有两个很重要的特性,一个是这个entity的名字,另一个是在运行时,表示这个entity的类的名字

一个entity可能会有attribute、relationship,也可能有fetched property,这三者统称为property。需要注意的是,property不能和NSObject或NSManagedObject已有的方法名重叠,比如,不能给某个property起名为“description”。
比较特殊的一种property叫做transient property,它是不会被保存到persistent store中去的。

多个entity之间可能会有继承关系,也可能某个entity会被指定为抽象的。

大多数model中的元素(比如entity、attribute、relationship)都会有一个对应的user info。


创建一个model


使用Xcode创建model



在Xcode中,选择File->New->File->Core Data->Data Model就可以创建一个扩展名为.xcdatamodeld的“源文件”了(实际上应该是一个目录)。其中包含了一个扩展名为.xcdatamodel的“源文件”。可以使用Xcode的Core Data model editor,在xcdatamodel文件中编辑model的内容,比如其中包含什么样的entity,每个entity中有什么样的attribute,以及各个entity之间的关系,等等。

如果App更新时,需要对model进行改动,就需要创建一个新的model version。在Xcode中,选中xcdatamodeld,选择Editor->Add Model Version,可以继续创建其中的xcdatamodel“源文件”。

除了model中关于entity和property的各种信息,xcdatamodel还会包含一些其他信息,比如绘制的图表的宽高排列之类的,但这些信息在运行时并没有什么意义。所以,model文件的编译工具momc会把运行时没有意义的信息去掉,将xcdatamodel文件编译成mom文件,将xcdatamodeld目录编译成momd目录。

在Xcode中找到编译好的.app文件,右键Show in Finder,打开里面的内容后,可以看到其中的.momd文件夹,和这个文件夹里面的.mom文件。

如果写的是iOS上的app,则在需要程序员自己加载model文件。有这样两种方法:

  1. 使用NSManagedObjectModel的initWithContentOfURL:方法。
    这是一种比较普遍使用的方法。
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:modelName withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
  1. 使用mergedModelFromBundles:方法.
    如果参数是nil,则会搜索main bundle,把其中的所有model给merge起来。
在代码中创建\修改model



在model被一个managed object context或者一个persistent store coordinator使用之前,这个model是可以在代码中被修改的。这允许程序员动态的创建或修改model。

试了一下在代码中创建model:

NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init];
NSEntityDescription *launchInfoEntity = [[NSEntityDescription alloc] init];
[launchInfoEntity setName:@"LaunchInfo"];

NSAttributeDescription *dateAttribute = [[NSAttributeDescription alloc] init];
[dateAttribute setName:@"date"];
[dateAttribute setAttributeType:NSDateAttributeType];
[dateAttribute setOptional:NO];

[launchInfoEntity setProperties:@[dateAttribute]];

[model setEntities:@[launchInfoEntity]];

如果model是在被一个managed object context或者一个persistent store coordinator使用之后,受到改动,则会抛出exception:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Can't modify an immutable model.'

Fetch Request Template



程序员可以使用NSFetchRequest类来描述从持久化存储中取得一些对象的请求。在实际的开发中,同样或相似的请求往往会被执行多次,所以,程序员可以自定义一些fetch request template,并把它们存到model中。可以使用Xcode的Core Data model editor,也可以在代码中定义。

使用Core Date model editor定义fetch request template



Editor->Add FetchRequest来新建一个fetch request。

填写Predicate,可以使用变量。右边栏还可以指定一些高级选项。

指定Predicate

在需要使用时,只要在代码中取出对应的fetch request template:

NSManagedObjectModel *managedObjectModel = [[context persistentStoreCoordinator] managedObjectModel];
NSFetchRequest *fetchRequest = [managedObjectModel fetchRequestFromTemplateWithName:@"fetchLaunchInfoBeforeSomeDate"
                                                              substitutionVariables:@{@"DATE" : [NSDate date]}];
NSArray *fetchResult = [context executeFetchRequest:fetchRequest error:&error];

就可以正常使用了。

直接在代码中创建fetch request template



也可以完全动态的创建fetch request template:

NSManagedObjectModel *managedObjectModel = [[context persistentStoreCoordinator] managedObjectModel];
NSFetchRequest *fetchRequestTemplate = [[NSFetchRequest alloc] initWithEntityName:@"LaunchInfo"];
[fetchRequestTemplate setPredicate:[NSPredicate predicateWithFormat:@"date > $DATE"]];
[managedObjectModel setFetchRequestTemplate:fetchRequestTemplate forName:@"fetchLaunchInfoAfterSomeDate"];

关于Configuration



如果程序员想要把不同的entity存放到不同的persistent store中去,应该怎么做呢?一个coordinator只能对应一个managed object model,所以在默认情况下,每一个与这个coordinator相关联的persistent store,都存放了同样的entity。为了避免这样的限制,可以使用Configuration来指定每个persistent store中应该存放哪些entity。
指定了Configuration之后,当程序员取这些对象的时候,它们会自动从不同的文件中被取出;保存时,它们也会被自动保存到不同的文件。

一个configuration由名字和若干entity组成。可以在代码中用

setEntities:forConfiguration:

方法动态的定义configuration;

也可以在Core Data editor tool中定义:


指定Configuration

每当给coordinator增加persistent store的时候,只用在configuration参数中指定对应的configuration即可以使用:

if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
                               configuration:@"ExitInfoConfiguration"
                                         URL:exitInfoStoreURL
                                     options:nil
                                       error:&error]) {
    //Handle error
}

if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
                               configuration:@"LaunchInfoConfiguration"
                                         URL:launchInfoStoreURL
                                     options:nil
                                       error:&error]) {
    //Handle error
}

关于Managed Object



一个managed object代表的是一个entity的实例。

每个managed object与一个managed object context相关联。在一个特定的context中,持久化存储中的一个特定的记录,只能有一个对应的managed object,这种技术叫做Uniquing。但是,也可能有多个context,每个context都持有一个表示同一条记录的managed object。


关于accessor方法



可以使用Xcode根据xcdatamodel中的内容自动生成NSManagedObject的子类。在子类的实现中,我们能看到,property被@dynamic修饰了。那是因为Core Data会在运行时动态生成accessor方法,这样生成的accessor方法是比较高效的,也就是说,程序员一般不需要写自定义的accessor方法。

也可以通过key-value的形式来获取或设置attributes的值,但是在性能上KVC不如accessor方法,所以只应该在必要的情况下使用。

如果这个managed object有to-many relationship,很多时候,程序员可能会需要增添、删除或改动这个to-many relationship中的某几个元素,这个时候则应该使用mutableSetValueForKey:方法或者动态生成的relationship mutator方法。


关于Managed Object的生命周期



一个managed object的生命周期和标准的Cocoa对象的生命周期不太一样,因为那是由Core Data来管理的。一个managed object表示的数据的生命周期,和这个manged object的实例的生命周期是独立的。

可以通过一个managed object得到它所在的context,也可以通过一个context得到其中的managed object。但是默认情况下,managed object和context之间的引用是弱引用。然而有一种例外情况,context会对“被改动过的”managed object持强引用,这里的改动包括插入、删除和修改,直到context被save、reset或者rollback。同时,undo manager也会用强引用来维持被改动过的managed object。
可以用setRetainsRegisteredObjects:方法改变这种默认情况,使得context对managed object持强引用。

当managed object有relationship的时候,它会对这个关联的对象持强引用,这也意味着可能有强引用循环出现。所以,当使用完一个managed object的时候,应该用refreshObject:mergeChanges:方法让它成为一个fault。

在一个managed object被创建的时候,其中每个property的值是在对应的entity中的default value。如果需要做一些自定义的初始化,建议重写:awakeFromInsert或者awakeFromFetch方法。

其中,awakeFromInsert会在调用了initWithEntity:insertIntoManagedObjectContext:或者insertNewObjectForEntityForName:inManagedObjectContext:方法之后立刻被调用。所以,重写这个方法,主要是可以为managed object中的property提供特殊的默认值,比如这个对象被创建的时间。

awakeFromFetch方法会在managed object从一个持久化存储中被取出来的时候调用。重写这个方法,可以用于建立transient值和缓存。需要注意的是,如果在这个方法中,改变了managed object中某些property,context不会被认为是dirty的。这也就意味着不应该在这个方法中操纵relationship,因为目标对象不会为此做出应有的改变。

initWithEntity:insertIntoManagedObjectContext: 这个方法也可以重写,但是并不鼓励这样做。因为在重写的这个方法中改变的状态,可能会不支持undo和redo。

在需要“析构”的时候,不应该重写dealloc方法,而是应该重写didTurnInfoFault方法。这个方法会在managed object变成fault的时候被调用,也就是说会比真正的析构早一些。


关于Relationship



大多数的relationship天生就是双向的(一个主要的例外就是fetched property)。一般来说,在使用Core Data的时候,也应该为relationship指定反向关系,这样可以确保object graph的一致性。

一个relationship是有delete rule的。这指定了当这个对象即将被删除的时候应该发生的行为。有这样几种delete rule:

  1. Deny
    如果至少有一个relationship的目的对象存在,源对象是不能被删除的;

  2. Nullify
    在删除当前对象的同时,将relationship的目的对象的反向关系设置为null;

  3. Cascade
    在删除当前对象的同时,也删除relationship的目的对象;

  4. No Action
    在删除当前对象的同时,对relationship的目的对象不做任何操作。在使用这个delete rule的时候,程序员有责任自行维护object graph,所以应该将对应的反向关系设置成有意义的值。


关于Object ID



一个NSManagedObjectID对象是managed object的全局ID。Object ID有临时和持久之分。当一个managed object刚刚被创建时,它将获得一个临时的object ID;只有当它被保存到持久化存储中时,它才会被赋予一个持久的ID。

Object ID也可以被转化成URI。可以使用 managedObjectIDForURIRepresentation:方法或objectWithID:方法通过URI或ID获取对应的managed object。


关于Validation



Validation机制用于检验managed object的property的值是否满足一定条件。有两种validation的类型,分别是:

  1. property层次的validation
  2. property之间的validation

Core Data允许程序员在managed object model中设定简单的validation逻辑。比如,可以设置数字和日期的最大最小值,可以设置字符串的最大最小长度、需要匹配的正则表达式,还可以设置to-many relationship中数目的最大最小值。

在Core Data Model editor中可以设置一些validation逻辑

除了可以对model设置这些validation逻辑,还可以在代码中进行自定义。

如果想要自定义property层次的validation,程序员不应该重写validateValue:forKey:error:方法,而是应该实现validate<Key>:error:方法。
然而,如果想要自行检查某个property是否符合规定,应该调用的是validateValue:forKey:error:方法,这个方法会将定义在managed object model中的validation逻辑也考虑进去。

也可以自定义property之间的validation。这可以通过重写validateForUpdate:validateForInsert:validateForDelete:方法来实现。在重写的这三个方法中,应该首先调用父类的实现。

所有的validation限制都只有在保存操作的过程中会被应用。因为managed object context的本意就是一块草稿板,所以应该允许其中的对象有临时性的“不合理”。


关于Faulting



一个managed object通常会用于表示被持久化存储的数据,但是在有些情况下,一个managed object可能是fault的,也就是说它的property还没有从外部数据存储中载入进来。这是Core Data用于减少内存占用的一种机制。

当访问到一个managed object的某个持久化的property的时候,fault被触发了,如果内存中的cache没有被击中的话,数据会被自动从持久化存储中取过来,这里的开销是比较昂贵的。

需要注意的是,description方法是不会触发fault的,所以打印刚刚取出来的managed object可以看到“<fault>”字样。
比如这样:

"<LaunchInfo: 0x10060b450> (entity: LaunchInfo; id: 0x40000b <x-coredata://4973AB39-0CD8-4480-AA07-7A3A877BE87D/LaunchInfo/p1> ; data: <fault>)"

如果重写description方法,并在其中访问了某个持久化的property,则fault会被触发。所以应该尽量避免这样的做法。

可以使用refreshObject:mergeChanges:并传人参数no让一个managed object变成fault。但是必须保证其中的relationship没有被改变。


关于Fetching


取得指定的对象

如果app使用了多个context,那么程序员可能就需要测试一个对象是否已经从persistent store中被删除了。这时,可以创建一个fetch request,其中这样指定predicate:

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self == %@", targetObject];

这样就可以通过判断fetch到的对象的数目是否为0来判断目标对象是否已被删除。其中的targetObject可以是一个managed object,也可以是一个manged object ID。
如果一次需要测试多个目标对象是否被删除,可以使用更高效的IN操作符:

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self IN %@", arrayOfManagedObjectIDs];
获取特定的值



有的时候,程序员可能不需要获取整个managed object,而是只是需要其中的某个attribute。NSExpressionDescription可以帮助程序员取得需要的值。
这时,需要使用setResultType:方法来指定这个fetch返回的结果类型是NSDictionaryResultType;还需要创建NSExpressionDescription的实例,来指定哪些property是需要取得的。
官方文档里有示例代码,偷个懒。


还欠缺的部分



这篇博客真是拖着写了好久。
但是还有好多内容没有理解,因为偷懒+之前在工作中对这些部分接触不多没什么感受,所以先放在这里,等下一遍看的时候,再慢慢理解好了。

Localizing a Managed Object Model
Copying and Copy and Paste
Drag and Drop
Undo Management
Ensuring Data Is Up-to-Date
Change and Undo Management
Fetched Properties
Non-Standard Persistent Attributes
Associate Metadata With a Store to Provide Additional Information and Support Spotlight Indexing
Core Data and Cocoa Bindings
Change Management
Persistent Store Features
Core Data Performance
Troubleshooting Core Data
Efficiently Importing Data

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容