Core Data
它是一个完全面向对象的API,负责在数据库中存储数据,底层也是由类似于SQL的技术来实现的。
在高级语言这一层,如何使用Core Data?在xcode中,有个工具可以建立对象之间的映射,这些对象会存储在你的数据库里,它们是NSObject的子类,实际上是NSManagedObject的子类,然后Core Data负责管理这些对象之间的关系。一旦在xcode中建立了visual map,你就可以新建对象,并存到数据库里或在数据库里删除、查询,实际起作用的是底层的SQL。可以用property访问数据库中对象内部的数据。Core Data负责管理底层的通信。
如何建立visual map?打开New File界面,在左边找到Core Data,这里选择Data Model,然后点Next,这样就建立了一个数据库的图形化model。通常会给visual map一个和应用相同的名字。
map的内部结构是怎样的?由3个不同的部分组成:一是entities,它们将映射到class;还有attributes,它映射到properties上;然后是relationship,这个属性用来指向数据库中的其他对象。
新建两个entity,分别是photo和photographer,它们之间会有一个明显的relationship。在代码中,entity实际上是一个NSManagedObject。
接下来要做的是如何创建NSManagedObject的子类,有了这些子类就可以调用数据库中的entity了。即使创建了子类,管理这些对象的底层机制仍然是NSManagedObject。
记住,所有的attribute都是对象,Core Data只知道在数据库中如何读写对象,所有的attribute都是各种不同类型的对象。有几种方法可以获取这些对象的值,一种方法是可以用NSKeyValueCoding协议,valueForKey和setValueForKey是这个协议的一部分,所有对象都可以使用它们,用valueForKey和setValueForKey设置property;另一种访问attribute的方法是新建一个NSManagedObject的子类,数据库的所有对象在代码中都是NSManagedObject。
不仅可以以表格的形式查看entity和attribute,还可以用图的方式。点击右下角的Editor Style,看到的内容与刚才一样,但是是以图的方式。可以在entities之间按住control拖动,来建立它们之间的relationship。一旦建立了关系,可以双击它,然后在inspector里改变它的名字,有个开关叫To-Many Relationship,就是设置两者间一对多的关系,注意其中的Delete Rule,意思是如果删除其中一个,那么会对这个relationship指向的东西有什么影响?其实就是把指针设为空。relationship的property类型:whoTook这个property的类型是NSManagedObject *;photos的类型是NSSet,它是一个内部数据类型为NSManagedObject *的NSSet。NSSet就是一堆对象的集合,它是无序的。
怎么在代码中使用visual map的数据呢?要获得数据,最重要的一点是,需要使用一个NSManagedObjectContext的东西,这是一个类,需要实例化。可以给这个实例发消息,比如查询之类。
怎么得到NSManagedObjectContext呢?需要它来往数据库里添加数据或进行查询操作,有两种基本方法可以获得NSManagedObjectContext:其一是创建UIManagedDocument,它有个属性叫managedObjectContext,获取它并使用就好了;第二种方法是在你新建一个工程的时候,有个复选框Use Core Data,选中它,就会在AppDelegate中生成一些代码,添加一个managedObjectContext的property。
UIManagedDocument
UIManagedDocument类继承自UIDocument,UIDocument有一套机制来管理一个或一组与磁盘相关的文件。UIManagedDocument实际上是一个装载Core Data数据库的容器,而且这个容器提供一些功能,比如写入、打开数据库。
怎么创建UIManagedDocument呢?它只有一个intializer,叫做initWithFileURL:
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url];
这个url几乎总是在文档目录下。现在还不可以用,还需要打开它,或者是创建,来使用。alloc init之后,它实际上并没有在磁盘上打开或创建。怎么打开或创建document?要调用以下方法来打开它:
- (void)openWithCompletionHandler:(void (^)(BOOL success))completionHandler;
CompletionHandler就是一个简单的block,这是一个没有返回值的block,它只处理一个表明是否成功打开文件的布尔值。如果文件不存在,不得不检查一下,必须调用fileExistsAtPath来检查这个文件是否存在:
[[NSFileManager defaultManager] fileExistsAtPath:[url path]]
如果这个文件存在,就可以用openWithCompletionHandler。但是如果不存在,需要创建它,需要调用UIManagedDocument里的这个方法来创建:
- (void)saveToURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)operation competionHandler:(void (^)(BOOL success))completionHandler;
创建完之后,如果要保存需要调用UIDocumentSaveForCreating。这边也有一个CompletionHandler。
为什么会有一个CompletionHandler呢?open和save方法是异步的,这些操作要花费一些时间,它们会立刻返回,但文件此时还没打开或创建好,只有在之后CompletionHandler被调用的时候,才能用这个document。异步的意思是这些操作需要花费一些时间,当这些操作完成之后调用你的block。
这是一个典型的例子:
self.document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url]; if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) { [document openWithCompletionHandler:^(BOOL success) { if (success) [self documentIsReady]; if (!success) NSLog(@“couldn’t open document at %@”, url); }]; } else { [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success) [self documentIsReady]; if (!success) NSLog(@“couldn’t create document at %@”, url); }];}
在这还不能对文档进行任何操作,因为这两个调用是异步的,必须等block被调用后,并激活一些条件才可以。
如果document打开了或创建好了,documentIsReady被调用了,你就可以使用它了:
- (void)documentIsReady { if (self.document.documentState == UIDocumentStateNormal) { NSManagedObjectContext *context = self.document.managedObjectContext; // do something with the Core Data context }}
document中有一个documentState的东西,通常在使用它之前都会检查这个documentState,最重要的状态就是UIDocumentStateNormal,意思就是已经打开好了,可以用了。如果状态是normal的话,我要做的是获得document context,然后就可以做Core Data的操作了,创建对象,查询,或从数据库在读取一些东西等等。
其他一些状态:UIDocumentStateClosed,这是document开始时的状态,当alloc initWithFileURL时,它的状态就是closed的;UIDocumentStateSavingError,这是指当保存文件时调用CompletionHandler出现了success等于NO,就会出现这种状态;UIDocumentStateEditingDisabled,这个状态是一个瞬时的状态,或许document正在重置,重置回以前保存的状态,或者保存操作正在进行,不能进行编辑;UIDocumentStateInConflict,这是处理iCloud时可能遇到的情况。
documentState的状态通常处于observed(监听)中,这是指,在ios中有一种方法,当documentState改变时,就告诉我,或者当有一个冲突出现了,马上告诉我,我好立刻解决问题。这个observed怎么用,它由NSNotification这个机制来管理。
NSNotification
有一种通信方式是广播站模式的,这种模式有点像广播,其他人可以接进这个广播站来并收听消息,这就是NSNotification。有一种办法可以让一个对象注册成为radio station,然后其他对象收听这个radio station。
需要一个NSNotificationCenter,就像交换中心似的,也可以把它想象成一个广播站注册机构。最简单的方式是调用[NSNotificationCenter defaultCenter],然后给NSNotification传递一个方法:
- (void)addObserver:(id)observer // you (the object to get notified) selector:(SEL)methodToSendIfSomethingHappens name:(NSString *)name // what you’re observing (a constant somewhere) object:(id)sender; // whose changes you’re interested in (nil is anyone’s)
addObserver就是你自己,你把自己设置为observer。selector是指当广播站广播时,会被调用的方法。name是指radio station的名字,是一个常量字符串,几乎总是常量类型的,一些类会告诉你它们广播站的名字,好让你注册。object是指你想收听的对象,你可以注册收听广播站上的任何广播,或者只收听某个特定的广播,如果是nil,就是收听所有的广播。
必须指定selector的名字,它的参数总是NSNotification *。NSNotification有三个property,一个是name,就是radio station的name,和上面一样;object,就是给你发送通知的那个对象,和上面一样。然后是userInfo,它就是个ID,可以是任何东西,由广播员负责告诉你现在正在播放什么内容,通常它会像一个词典或者某种容器来保存数据。
- (void)methodToSendIfSomethingHappens:(NSNotification *)notification { notification.name // the name passed above notification.object // the object sending you the notification notification.userInfo// notification-specific information about what happened }
下面来看一个例子,是关于documentState的:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];[center addObserver:self selector:@selector(documentChanged:) name:UIDocumentStateChangedNotification object:self.document];
把自己添加成observer。这边要注册的广播站是UIDocumentStateChangedNotification,这是在UIManagedDocument中定义的一个NSString,其实是在UIDocument.h中。object是我想收听的对象,所以在这写self.document。只要把这个消息传递给center,只要documentState有变化,我就会得到一个documentChanged的消息,这个消息会有一个NSNotification *参数。
当你不再需要监听广播时,要删除自己的observer身份。原因是,NSNotification不会维护一个指向你的weak指针,它维护一个unsafe或者是unretained的指针。这并不安全,如果被指向的对象消失,unsafe或者unretained指针会指向堆上的一块无用的内存,必须要确保在对象消失之前解除你的observer身份。
[center removeObserver:self];or[center removeObserver:self name:UIDocumentStateChangedNotification object:self.document];
很有可能,会在viewDidAppear或者viewWillDisappear中传递add或者remove消息,这边有一个例子:
- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [center addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextObjectsDidChangeNotification object:self.document.managedObjectContext];} - (void)viewWillDisappear:(BOOL)animated{ [center removeObserver:self name:NSManagedObjectContextObjectsDidChangeNotification object:self.document.managedObjectContext]; [super viewWillDisappear:animated];}
这里监听Core Data数据库是否有变化。记住,可以由多个不同的managedObjectContext改变数据库,这样会造成混淆,如果多线程就容易解决。广播者是managedObjectContext,如果数据库中添加,删除,或者有一些更改,它就会向你广播。广播站叫NSManagedObjectContextObjectsDidChangeNotification。
contextChanged是这个样子的:
- (void)contextChanged:(NSNotification *)notification { The notification.userInfo object is an NSDictionary with the following keys: NSInsertedObjectsKey // an array of objects which were inserted NSUpdatedObjectsKey //anarrayofobjectswhoseattributeschanged NSDeletedObjectsKey //anarrayofobjectswhichweredeleted}
userInfo是一个词典,这个词典有三个键,这些键是否存在取决于NSManagedObjectContext中出现了什么变化,这些键的值是NSArray,它的内部数据类型为一个有过更改的NSManagedObject,你可以获得context中所发生的更改的完整描述。
UIManagedDocument
打开或者创建document,获取它的context,对数据库做了很多更改,怎么保存这些更改呢?UIManagedDocument是自动保存的,但不会依赖这种自动保存机制,可以用以下这个方法来保存数据:
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) { if (!success) NSLog(@“failed to save document %@”, self.document.localizedName);}];
关闭document同样是异步的,什么时候需要关闭document呢?在完成更改后都需要关闭它,同时撤销所有指向UIManagedDocument的strong指针。如果没有strong指针指向UIManagedDocument时,它会自动关闭。
[self.document closeWithCompletionHandler:^(BOOL success) { if (!success) NSLog(@“failed to close document %@”, self.document.localizedName);}];
它是异步的,得等到block执行了,它才会关闭。
可以有UIManagedDocument的多个实例指向磁盘上的同一个document吗?完全可以,但要小心,这些实例是没有关系的。
Core Data
现在从document中获得了一个NSManagedObjectContext,就可以进行插入和删除操作,可以进行查询。
通过调用NSEntityDescription中的方法来插入数据,这是一个类方法:
NSManagedObject *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObjectContext:(NSManagedObjectContext *)context];
数据库中的所有对象都是由NSManagedObject表示的,NSEntityDescription insert的返回值是一个NSManagedObject *,它返回一个指向新创建对象的指针。
现在有这个对象了,需要设置它的attribute,怎么访问这些attribute呢?可以用NSKeyValueObserving协议,注意NSKeyValueObserving协议中的Observing,可以观察任何支持这个协议的对象的setting和geting这两个property,你希望观察这些property,这看起来和NSNotificationCenter很相似,可以说添加一个观察者,来观察这个对象的某个property,只要这个对象为这个property实现了这个协议。
- (id)valueForKey:(NSString *)key; - (void)setValue:(id)value forKey:(NSString *)key;
如果使用valueForKeyPath:/setValue:forKeyPath:方法,它就会跟踪那个relationship。key是attribute的名字,而value是所存的内容。
对UIManagedDocument做的所有修改都是在内存中进行的,直到做了save操作。
但是调用valueForKey:/setValueForKey:会使代码变得很乱,这么做没有任何的类型检查,所以通常不用这种方法。用property,但是如何给NSManagedObject添加一个property,并且它的类型是Photographer *,而不是NSManagedObject *,而且是在NSManagedObject不认识这些东西的情况下。方法是创建NSManagedObject的子类,比如创建一个名为Photo的NSManagedObject的子类来表示photo entity,它在头文件里生成的就是@property,这个@property对应着所有的attribute,在实现文件中采用的不是@synthesize,因为@synthesize是给它生成一个实例变量,但这些property并不是以实例变量存储的,它是存储在SQL数据库里的。
怎么生成NSManagedObject的子类呢?只需到xcode中的model file,选中它们,然后到Editor菜单,点击下面的Create NSManagedObject subclasses。生成后可以看到Photographer.h和.m文件,还有Photo.h和.m文件。
它创建了一个category,可以用来设置NSSet中的值。怎么往photos relationship中添加图片呢?有两种方法:一种是可以用它自动生成的add;另一种是用photos这个set,调用mutableCopy,这样就有一个mutable set了,然后往里面加东西,然后把photos设置回来就行了,通过调用这个property的setter。
在Photo.h中可以看到whoTook,它的类型是NSManagedObject *,应该是Photographer *才对。这是xcode的问题,在xcode生成代码时,它先生成Photo,然后生成Photographer。怎么修改这个错误呢?回到xcode,再生成一下就行了。
再看.m文件,很简洁,它所做的就是在所有property前面加上@dynamic,@dynamic的作用是告诉编译器我清楚我不需要对这个property进行@synthesize,请不要发出警告。如果这些子类不实现这些property,会有什么后果?这就不确定了。NSManagedObject的做法是,如果你传递一个property,它就会查找自己是否有个相同名字的属性,如果有,它就调用valueForKey:,或者setValueForKey:。如果添加一些额外的property,会出现错误。
有了Photographer.h、Photographer.m文件、Photo.h和Photo.m文件,那如何访问property呢?用“.”的方式调用就可以。
Photo *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObj...]; NSString *myThumbnail = photo.thumbnailURL; photo.thumbnailData = [FlickrFetcher urlForPhoto:photoDictionary format:FlickrPhotoFormat...]; photo.whoTook = ...; // a Photographer object we created or got by queryingphoto.whoTook.name = @“CS193p Instructor”; // yes, multiple dots will follow relationships
如果更改schema,得重新生成子类。要是往其中加入一些代码呢,这么做就得修改Photo.m,那下次改变schema并在xcode中重新生成时,代码就没了。怎么解决这个问题呢?用一个Objective-C语言的一个新特性,叫category。
Categories
Categories可以让你在不使用子类的情况下往一个类中添加方法或者属性,语法是这样的:
@interface Photo (AddOn) - (UIImage *)image; @property (readonly) BOOL isOld; @end
这就是@interface,它会在Photo+AddOn.h中。不仅需要声明这些方法,还要实现它们,这里是一个.m文件可能的写法:
@implementation Photo (AddOn) -(UIImage*)image //imageisnotanattributeinthedatabase,butphotoURLis { NSData *imageData = [NSData dataWithContentsOfURL:self.photoURL]; return [UIImage imageWithData:imageData];} -(BOOL)isOld //whetherthisphotowasuploadedmorethanadayago { return [self.uploadDate timeIntervalSinceNow] < -24*60*60;} @end
把它们加入到Photo类,isOld是只读的,只添加isOld的getter方法,self就是Photo。
使用category有一个很大的限制就是,它自己是不能添加实例变量的。所以在实现一个category时,内部是不能有@synthesize。
向NSManagedObject的子类,添加的最常用的category是Create:
@implementation Photo (Create) + (Photo *)photoWithFlickrData:(NSDictionary *)flickrData inManagedObjectContext:(NSManagedObjectContext *)context{ Photo *photo = ...; // see if a Photo for that Flickr data is already in the database if (!photo) { photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObjectContext:context]; // initialize the photo from the Flickr data // perhaps even create other database objects (like the Photographer) } return photo;} @end
要使用这个方法,只需import Photo+Create.h。
Core Data
如何在数据库上删除对象,只要调用以下方法:
[self.document.managedObjectContext deleteObject:photo];
必须要保证如果删除数据库中的某个对象时,数据要维持在一个稳定的状态。
有一个prepareForDeletion方法,而且可以在category中实现它,这个方法必须由一个NSManagedObject的子类来实现,才可以调用。在将要进行删除操作的时候,就会调用它。就是说,如果有谁调用了deletePhoto,这个过程的前期就是调用这个prepareForDeletion。
@implementation Photo (Deletion) - (void)prepareForDeletion { // we don’t need to set our whoTook to nil or anything here (that will happen automatically) // but if Photographer had, for example, a “number of photos taken” attribute, // we might adjust it down by one here (e.g. self.whoTook.photoCount--).} @end
在对象删除后,就不要保留strong指针了。
怎么查询呢?通过创建、执行NSFetchRequest对象来完成。首先要创建,然后请求NSManagedObjectContext替你执行这个fetch。
在建立NSFetchRequest时,有四点很重要:
首先,要指明想获取的那个entity;
还有,NSPredicate,这个指明你想从哪些entities中获取数据,就是查询条件;
再有,NSSortDescriptors,因为fetch会返回一个array,就是一个有序列表,所以要指明排序规则;
最后,可以控制每次查询的返回值的数量,或者每个batch有多少。
这是查找和建立一个fetch请求,大概的写法:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photo”]; request.fetchBatchSize = 20; request.fetchLimit = 100; request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor]; request.predicate = ...;
首先是指明entity,当你查询Core Data时,只返回一类entity,从数据库角度讲,只能在一个表上查询,每次只能从一个表中获取数据。NSSortDescriptor,它指明了你在执行这个查询后返回的array的排列顺序,通过以下方法来创建sortDescriptor:
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@“thumbnailURL” ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
key就是排序时要参照的那个属性,ascending用来指定是升序还是降序,然后是selector,它并非一定得是Objective-C selector。排序是在数据库中进行的,也就是SQL做排序的工作,然后返回排列好的数据。fetch request的sortDescriptor不是只能有一个,可以是一个sortDescriptor的组合。
predicate用来表明你想得到什么样的对象,它看起来就像一个NSString:
NSString *serverName = @“flickr-5”; NSPredicate *predicate = [NSPredicate predicateWithFormat:@“thumbnailURL contains %@”, serverName];
还有一些例子:
@“uniqueId = %@”, [flickrInfo objectForKey:@“id”] // unique a photo in the database @“name contains[c] %@”, (NSString *) // matches name case insensitively @“viewed > %@”, (NSDate *) // viewed is a Date attribute in the data mapping @“whoTook.name = %@”, (NSString *) // Photo search (by photographer’s name) @“any photos.title contains %@”, (NSString *) // Photographer search (not a Photo search)
contain的意思就是是否有子字符串,注意这个[c],意思是区分大小写。
这还有一个例子,如果想查询所有Photographer,查询会在Photographer表上进行:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photographer”];... who have taken a photo in the last 24 hours ...NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow:-24*60*60]; request.predicate = [NSPredicate predicateWithFormat:@“any photos.uploadDate > %@”, yesterday]; ... sorted by the Photographer’s name ... NSSortDescriptor *sortByName = [NSSortDescriptor sortDescriptorWithKey:@“name” ascending:YES]; request.sortDescriptors = [NSArray arrayWithObject:sortByName];
这个请求建好了,接下来是如何执行这个查询?我向managedObjectContext发送一个消息,这个managedObjectContext是从document中获取的,消息的名字叫executeFetchRequest,发送请求的时候,还跟了一个error指针,这样也能接收到error消息。
NSManagedObjectContext *moc = self.document.managedObjectContext; NSError *error; NSArray *photographers = [moc executeFetchRequest:request error:&error];
如果返回值是nil,表示出错了,要查看一下这个error。如果返回的array是空的,是指没有查询到符合条件的对象。
所有的数据并不会一次返回,它会有选择的存储你想要的对象