虽然从接触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 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文件。有这样两种方法:
- 使用NSManagedObjectModel的initWithContentOfURL:方法。
这是一种比较普遍使用的方法。
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:modelName withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
- 使用
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,可以使用变量。右边栏还可以指定一些高级选项。
在需要使用时,只要在代码中取出对应的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中定义:
每当给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:
Deny
如果至少有一个relationship的目的对象存在,源对象是不能被删除的;Nullify
在删除当前对象的同时,将relationship的目的对象的反向关系设置为null;Cascade
在删除当前对象的同时,也删除relationship的目的对象;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的类型,分别是:
- property层次的validation
- property之间的validation
Core Data允许程序员在managed object model中设定简单的validation逻辑。比如,可以设置数字和日期的最大最小值,可以设置字符串的最大最小长度、需要匹配的正则表达式,还可以设置to-many relationship中数目的最大最小值。
除了可以对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