• Core Data 的使用


    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 querying
    photo.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是空的,是指没有查询到符合条件的对象。

    所有的数据并不会一次返回,它会有选择的存储你想要的对象

  • 相关阅读:
    分分钟用上C#中的委托和事件
    AutoResetEvent详解
    C#跨线程调用
    多线程编程的注意事项
    C#中WinForm程序退出方法技巧总结
    用c#实现单链表(程序代码已经验证,完全正确)
    event & EventHandler
    Kubernetes理论02
    centos7 日志文件
    CentOS7 FTP安装与配置
  • 原文地址:https://www.cnblogs.com/wuwangchuxin/p/3686127.html
Copyright © 2020-2023  润新知