Magical Record 全面解析

Magical Record是用来操作Core Data的一个第三方工具,在介绍Magical Record 之前必须要先了解一下Core Data的基本概念

Core Data基本介绍

Core Data Stack

核心数据堆栈是由一个或多个与单个persistent store coordinator关联的managed object contexts 组成,而persistent store coordinator是和一个或多个persistent stores关联在一起。堆栈包含了CoreData的所有组件查询,创建,操作managed objects.
简单来说包含了:

  • 一个包含了记录的persistent store.类似于数据库
  • 一个在本地数据和对象之间的persistent object store
  • 一个聚合了所有存储的persistent store coordinator
  • 一个描述实体的managed object model
  • 一个容器包含managed objectsmanaged object context容器

可能有点绕,不过一看图世界就清晰了

如下图:


Magical Record 全面解析_第1张图片

Managed Object

Managed Object是一个模型对象(模型-视图-控制器的意义上),它代表了一个持久存储的记录。管理对象是实例NSManagedObject或子类NSManagedObject。

管理对象有一个实体的描述对象,告诉它代表着什么实体的引用。以这种方式,NSManagedObject可以表示任何实体不需要每个实体的唯一的子类。如果要实现自定义行为,例如计算派生属性值,或者为了实现验证逻辑可以使用一个子类。

还是来看图:

Magical Record 全面解析_第2张图片

Managed Object Model

Magical Record 全面解析_第3张图片

Manage Context Object

Manage Context Object代表单个对象的空间,,在核心数据的应用程序。管理对象上下文的一个实例的NSManagedObjectContext。它的主要职责是管理管理对象的集合。这些管理对象代表一个或多个持久存储的一个内部一致的看法。上下文是在管理对象的生命周期核心作用。

上下文是在核心数据堆栈中的中心对象。这是你用它来创建和获取管理对象和管理撤消和恢复操作的对象。内的给定范围内,有至多一个被管理目标代表在永久存储器的任何给定的记录。

Magical Record 全面解析_第4张图片

上下文被连接到一个父对象存储这通常是一个持久存储协调,但可能是另一个管理对象上下文。当你获取对象,上下文要求其父对象存储返回那些符合提取请求的对象。您对管理对象的修改,直到您保存的背景下不被提交到父store。

在某些应用中,你可能想保持独立组来管理对象和编辑这些对象的; 或者你可能需要执行使用一个上下文,同时允许用户与另一个对象交互的后台操作

Persistent Store Coordinator

哎!翻译太累了。直接上图吧

Magical Record 全面解析_第5张图片

这张图把这个的架构解释得非常清楚

Fetch Request

Magical Record 全面解析_第6张图片

官方文档

开始使用Magical Record

导入MagicalRecord.h在项目的预编译文件*.pch中。这保证了可以全局访问所需要的头文件。

使用了CocoaPods或者MagicalRecord.framework,用如下方式导入:

// Objective-C
#import 
// Swift
import MagicalRecord

如果是把源文件直接放到项目中,则直接#import "MagicalRecord.h"

接下里,在app delegate的某些地方,比如- applicationDidFinishLaunching: withOptions:或者-awakeFromNib,使用下面的某一个方法来配置MagicalRecord.

+ (void)setupCoreDataStack;
+ (void)setupAutoMigratingCoreDataStack;
+ (void)setupCoreDataStackWithInMemoryStore;
+ (void)setupCoreDataStackWithStoreNamed:(NSString *)storeName;
+ (void)setupCoreDataStackWithAutoMigratingSqliteStoreNamed:(NSString *)storeName;
+ (void)setupCoreDataStackWithStoreAtURL:(NSURL *)storeURL;
+ (void)setupCoreDataStackWithAutoMigratingSqliteStoreAtURL:(NSURL *)storeURL;

每次调用Core Data的堆栈的实例,提供给了这些实例的getter,setter方法。这些实例被MagicalRecord很好的管理,被识别为默认方式。

当通过DEBUG模式标识使用SQLite数据库,不创建新的model版本来改变model将会引起MagicalRecord自动的删除老的数据库并且自动的创建一个新的。这样可以节约很多时间--不需要每次都卸载重装app来让data model改变,确保你的app不是用的DEBUG模式:当删除app数据的时候不告诉用户真的是一种很糟糕的方式

在你的app退出之前,你应该调用类方法+cleanUp

[MagicalRecord cleanUp];

这将会清理MagicalRecord,比如自定义的错误处理,让通过MagicalRecord创建的Core Data堆栈为nil.

使用Managed Object Contexts

创建新的上下文

一些简单的类方法用来帮助快速的你创建新的上下文

    • [NSManagedObjectContext MR_newMainQueueContext]:
    • [NSManagedObjectContext MR_newPrivateQueueContext]:
    • [NSManagedObjectContext MR_newContextWithStoreCoordinator:…]: 允许你具体化persistent store coordinator为新的上下文,有一个NSPrivateQueueConcurrencyType

默认上下文

当使用CoreData,你将不断的和两个主要的对象打交道,NSManagedObjectNSManagedObjectContext.

MagicalRecord提高了一个简单的类方法来获取默认的NSManagedObjectContext,这个上下文贯穿了你的app始终,这个上下文的操作会在在主线程中进行,并且对于单线程的app比较适合

通过如下方式访问到默认的上下文:

NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];

这个上下文将在MagicalRecord任何使用了上下文的方法中使用,但是没有提供一个具体的NSManagedObjectContext参数。

如果你需要创建一个不再主线程中使员工的上下文,使用:

NSManagedObjectContext *myNewContext = [NSManagedObjectContext MR_newContext];

这种方式将会创建一个和default context有相同的对象和persistent store.安全在其他线程使用。它将会默认的将default context作为它的父上下文

如果你想默认让myNewContext实例化所有的fetch request.使用类方法的方式

[NSManagedObjectContext MR_setDefaultContext:myNewContext];

注意:高度建议default context使用类型为NSMainQueueConcurrencyType的上下文来创建并设置在主线程。

在后台线程中执行

MagicalRecord提供了方法来设置,协调上下文在后台线程中使用。后台保存操作受到了UIView动画使用Block的方式,但也存在了一些不同

  • 在你对实体进行改变了的block,绝对不在主线程中执行

  • 单个的NSManagedObjectContext提供了block使用。

举个例子,你有一个Person实体,并且需要设置firstName和lastName,下面的代码展示了你怎样通过MagicalRecord来设置后台上下文进行使用。

Person *person = ...;

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){

  Person *localPerson = [person MR_inContext:localContext];
  localPerson.firstName = @"John";
  localPerson.lastName = @"Appleseed";

}];

在这个方法,具体的block提供了一个合适的上下文让你进行操作,不需要担心去设置上下文,以便它告诉default context已经做了。并且应该更新,因为是在其他线程里面改变进行的。

当执行完了saveBlock,你可以在completion block做些操作

Person *person = ...;

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){

  Person *localPerson = [person MR_inContext:localContext];
  localPerson.firstName = @"John";
  localPerson.lastName = @"Appleseed";

} completion:^(BOOL success, NSError *error) {

  self.everyoneInTheDepartment = [Person findAll];

}];

completion block在主线程中被调用,为了UI更新更安全。

创建实体

在默认的上下文中插入一个实体,如下:

Person *myPerson = [Person MR_createEntity];

在具体的上下文中插入一个实体
Person *myPerson = [Person MR_createEntityInContext:otherContext];

删除一个实体

在默认上下文中删除:
[myPerson MR_deleteEntity];

在具体上下文中删除:
[myPerson MR_deleteEntityInContext:otherContext];

截断所有实体在默认上下文
[Person MR_truncateAll];

截断所有实体在具体上下文
[Person MR_truncateAllInContext:otherContext];

查询实体

基本查找

MagicalRecord大多数方法是返回一个NSArray数组。

举例,如果你有一个person实体和department实体关联,你可以查询所有的person实体从persistent store通过如下方式实现:

NSArray *people = [Person MR_findAll];

传入一个具体的参数返回一个排序后的数组:

NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName" ascending:YES];

传入多个具体的参数返回一个排序后的数组

NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName,FirstName" ascending:YES];

传入多个不同参数值得到排序结果,如果你不提供任何一个参数的默认值,就会默认使用你在model中的设置。

NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName:NO,FirstName"
                                         ascending:YES];

// OR

NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName,FirstName:YES"
                                         ascending:NO];

如果你有一种唯一从数据库中查询单个对象的方法(比如作为唯一属性),你可以通过下面的方法:

Person *person = [Person MR_findFirstByAttribute:@"FirstName" withValue:@"Forrest"];

高级查找

如果想去具体化你的搜索,你可以使用谓词

NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", @[dept1, dept2]];
NSArray *people = [Person MR_findAllWithPredicate:peopleFilter];

返回NSFetchRequest

NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];
NSFetchRequest *people = [Person MR_requestAllWithPredicate:peopleFilter];

关于每一行的调用, NSFetchRequestNSSortDescriptor作为排序的标配。

自定有Requset

Predicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];

NSFetchRequest *peopleRequest = [Person MR_requestAllWithPredicate:peopleFilter];
[peopleRequest setReturnsDistinctResults:NO];
[peopleRequest setReturnPropertiesNamed:@[@"FirstName", @"LastName"]];

NSArray *people = [Person MR_executeFetchRequest:peopleRequest];

查询实体数量

可以执行所有实体类型输血量在persistent store

NSNumber *count = [Person MR_numberOfEntities];

或者基于查询的数量
NSNumber *count = [Person MR_numberOfEntitiesWithPredicate:...];

这里有一组方法来返回NSUInteger而不是NSNumber

+ (NSUInteger) MR_countOfEntities;
+ (NSUInteger) MR_countOfEntitiesWithContext:(NSManagedObjectContext *)context;
+ (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter;
+ (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter 
                                     inContext:(NSManagedObjectContext *)context;

聚合操作

NSNumber *totalCalories = [CTFoodDiaryEntry MR_aggregateOperation:@"sum:" 
                                                      onAttribute:@"calories" 
                                                    withPredicate:predicate];

NSNumber *mostCalories  = [CTFoodDiaryEntry MR_aggregateOperation:@"max:" 
                                                      onAttribute:@"calories" 
                                                    withPredicate:predicate];

NSArray *caloriesByMonth = [CTFoodDiaryEntry MR_aggregateOperation:@"sum:" 
                                                       onAttribute:@"calories" 
                                                     withPredicate:predicate
                                                           groupBy:@"month"];

在具体的上下文中查找实体

所有的 find, fetch, request方法都有一个inContext:,方法参数允许具体使用哪一个上下文查询:

NSArray *peopleFromAnotherContext = [Person MR_findAllInContext:someOtherContext];

Person *personFromContext = [Person MR_findFirstByAttribute:@"lastName" 
                                                  withValue:@"Gump" 
                                                  inContext:someOtherContext];

NSUInteger count = [Person MR_numberOfEntitiesWithContext:someOtherContext];

存储实体

什么时候应该保存

总的来说,当数据发生改变的时候应该保存到persistent store(s).一些应用选择在应用终止的时候才保存。然而,在大多数场景下是不需要的。事实上,只在应用终止的时候保存,会有数据丢失的风险。万一你的应用崩溃了怎么办?用户将丢失他对数据所做的改变。那样的话是一种相当糟糕的体验,可以简单的避免。

如果你觉得执行保存话费了大量的时间,有几件事情需要考虑:

  • 1.在后台线程保存:MagicalRecord提高了非常简单的API来让改变的实例按顺序的在后台保存,举个例子:
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {

    // Do your work to be saved here, against the `localContext` instance
    // Everything you do in this block will occur on a background thread

} completion:^(BOOL success, NSError *error) {
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];
  • 2.将任务分解为多个小任务保存:比如大量数据的导入应被分解为小块,没有确切的原则来决定一次导入多少数据,你需要测量你应用的性能比如通过苹果的Instruments和tune.

解决长期运行中的保存

在iOS上

当应用在iOS上终止运行,有一个很小的机会去清理,保存数据到磁盘。如果你知道保存操作可能会花一段时间,最好的方式就是去申请一个额外的截止时间。比如:

UIApplication *application = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {

    // Do your work to be saved here

} completion:^(BOOL success, NSError *error) {
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

确保仔细的读过[

  • beginBackgroundTaskWithExpirationHandler:](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/index.html#//apple_ref/occ/instm/UIApplication/beginBackgroundTaskWithExpirationHandler:),不合适或者不需要的延长应用的存活时间可能让你的应用被拒。

使用模式

  • 保存数据
  • 查找实体
  • 导入数据
  • 线程安全

日志

MagicalRecord建立了在和Core Data交互的时候的日志。当错误发生的时候,这些错误将会被捕获。并且将打印到控制台。

日志被配置为输出调试信息(MagicalRecordLoggingLevelDebug)在deug编译的时候默认的,将会输出日志信息MagicalRecordLoggingLevelError在realease下。

日志通过[MagicalRecord setLoggingLevel:]配置,使用下面的几种预定义的日主等级。

  • MagicalRecordLogLevelOff:不开始日志
  • MagicalRecordLoggingLevelError:记录所有错误
  • MagicalRecordLoggingLevelWarn:记录警告和错误
  • MagicalRecordLoggingLevelInfo:记录日志有用的信息,错误,警告
  • MagicalRecordLoggingLevelDebug:所有调试信息
  • MagicalRecordLoggingLevelVerbose:日志冗长的诊断信息,有用的信息,错误,警告

日志默认等级是 MagicalRecordLoggingLevelWarn

关闭日志

大多数人而言,这个不需要,设置日志等级为MagicalRecordLogLevelOff将会保证不再打印日志信息

甚至当使用了MagicalRecordLogLevelOff,快速检测检查可能被调用无论何时日志被调用。如果想绝对的关闭日志。你可以定义如下,当编译MagicalRecord的时候
#define MR_LOGGING_DISABLED 1

请注意:这个之后再增加源码到项目中才会起作用。你也可以增加MagicalRecord项目的OTHER_CFLAGS为-DMR_LOGGING_DISABLED=1

日志在2.3.0版本有问题,不能正常的显示到控制器

google到了解决的方法副在下面

For the development branch (version 2.3.0 and higher) of Magical Record logging seems to still not work correctly. When imported like this: pod 'MagicalRecord', :git => 'https://github.com/magicalpanda/MagicalRecord', :branch => 'develop'

I have no logging output on my Xcode console. But I altered the post_install script of the Cocoapod. The following should enable logging: https://gist.github.com/Blackjacx/e5f3d62d611ce435775e

With that buildsetting included in GCC_PREPROCESSOR_DEFINITIONS logging of Magical Record can be controlled in 2.3.0++ by using [MagicalRecord setLoggingLevel:]
  • 脚本:
post_install do |installer|
installer.project.targets.each do |target|
    target.build_configurations.each do |config|
        # Enable the loggin for MagicalRecord
        # https://github.com/magicalpanda/MagicalRecord/wiki/Logging
        if target.name.include? "MagicalRecord"
            preprocessorMacros = config.build_settings['GCC_PREPROCESSOR_DEFINITIONS']
            if preprocessorMacros.nil?
                preprocessorMacros = ["COCOAPODS=1"];
            end
            preprocessorMacros << "MR_LOGGING_ENABLED=1"

config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = preprocessorMacros
end
end
end
end

## 自己尝试遇到的坑

### 日记记录

2.3.0版本同样遇到了日志不能正常输出到控制台的问题,虽然能够拿到解决问题的脚步,但是自己在taget,buildsetting里面都设置了还是没有用。自己对cocopods管理的原理还不是很明白。

### 上下文的坑

``NSManagedObjectContext``这个类是CoreData里面非常重要的类。它有父上下文和子上下文的概念。经过了漫长的爬坑,终于在苹果官方文档中找到了关于它详细的介绍。

这里只截取``parent store``这节来讲

> ``Managed object contexts ``有一个父存储,通过它来检索数据,提交改变
> 
> 最开始在iOS5的之前,父存储一直是``persistent store coordinator``。在iOS5之后。父存储的类型可以是其他的``Managed object contexts ``。但是最终的根context必须是`persistent store coordinator``。协调者提高被管理的对象模型,调用各种对数据库的请求。
> 
> 如果父存储是一个``Managed object contexts ``。查询,保存的操作是被父存储来协调的而不是``persistent store coordinator``。这种方式有两个好处,
>
>* 1.在其他线程中执行操作
>* 2.管理废弃的编辑,比如监视窗口、view
> 第一种场景,父上下文能够通过不同的线程从子中获得请求,

> * 重点部分:当在上下文中保存所做的改变的时候,改变只会被提交一次存储,如果有子的上下文,改变将会推到他的父上下文,改变不会直接保存到数据库,直到根上下文被保存才会保存到数据库(根管理对象的上下文的父上下文为空)。除此之外,父上下文在保存之前不会从子中拉取数据的改变。如果你想最后提交数据的改变,必须保存子上下文,这样就可以推到父上下文中。


## 测试代码

上下文的创建时通过线程来控制,也就是上下文和线程相关。``[[NSThread currentThread] threadDictionary];``返回的字典就是处理数据方面的。

if ([NSThread isMainThread])
{
return [self MR_defaultContext];
}
else
{
int32_t targetCacheVersionForContext = contextsCacheVersion;

    NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary];
    NSManagedObjectContext *threadContext = [threadDict objectForKey:kMagicalRecordManagedObjectContextKey];
    NSNumber *currentCacheVersionForContext = [threadDict objectForKey:kMagicalRecordManagedObjectContextCacheVersionKey];

    // 保证两者同时存在,或者同时不存在
    NSAssert((threadContext && currentCacheVersionForContext) || (!threadContext && !currentCacheVersionForContext),
             @"The Magical Record keys should either both be present or neither be present, otherwise we're in an inconsistent state!");
    // 不存在上下文
    if ((threadContext == nil) || (currentCacheVersionForContext == nil) || ((int32_t)[currentCacheVersionForContext integerValue] != targetCacheVersionForContext))
    {
        // 创建新的上下文
        threadContext = [self MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
        [threadDict setObject:threadContext forKey:kMagicalRecordManagedObjectContextKey];
        [threadDict setObject:[NSNumber numberWithInteger:targetCacheVersionForContext]
                       forKey:kMagicalRecordManagedObjectContextCacheVersionKey];
    }
    return threadContext;
}

在配置的时候就会默认创建两种上下文,一个根上下文,和协调者直接通信的,一个是主线程相关的默认上下文。默认上下文是根上下文的子。

* 有必要说一说``MR_saveWithBlock``这个方法,自己在写的时候就犯错了。

开看看实现
  • (void)MR_saveWithBlock:(void (^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion;
    {
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextWithParent:self];

    [localContext performBlock:^{
    [localContext MR_setWorkingName:NSStringFromSelector(_cmd)];

      if (block) {
          block(localContext);
      }
      
      [localContext MR_saveWithOptions:MRSaveParentContexts completion:completion];
    

    }];
    }

是在当前的上下文中新建子然后通过子去保存,注意这里的保存方法有个参数``MRSaveParentContexts ``,会连同父上下文一起通常,

在保存的方法中有一段:

// Add/remove the synchronous save option from the mask if necessary
MRSaveOptions modifiedOptions = saveOptions;

            if (saveSynchronously)
            {
                modifiedOptions |= MRSaveSynchronously;
            }
            else
            {
                modifiedOptions &= ~MRSaveSynchronously;
            }

            // If we're saving parent contexts, do so
            [[self parentContext] MR_saveWithOptions:modifiedOptions completion:completion];
类似于递归调用,最终会调用根上下文,也就是保存到了数据库。

但是在这之前有个逻辑想到重要。也就是保存的上下文该没有改变。如果被确定是没有改变的,那就不会中保存的逻辑。

 __block BOOL hasChanges = NO;

if ([self concurrencyType] == NSConfinementConcurrencyType)
{
    hasChanges = [self hasChanges];
}
else
{
    [self performBlockAndWait:^{
        hasChanges = [self hasChanges];
    }];
}

if (!hasChanges)
{
    MRLogVerbose(@"NO CHANGES IN ** %@ ** CONTEXT - NOT SAVING", [self MR_workingName]);

    if (completion)
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(NO, nil);
        });
    }

    return;
}

最后来一段有问题的代码。

 // 在默认的上下文中创建实体
Person *person = [Person MR_createEntity];
// 改变person,引起上下文的改变
person.name = @"test";
person.age = @(100);
[[NSManagedObjectContext MR_defaultContext] MR_saveWithBlock:^(NSManagedObjectContext * _Nonnull localContext) {

} completion:^(BOOL contextDidSave, NSError * _Nullable error) {

}];

这段代码不会保存成功。

因为在``MR_saveWithBlock``创建一个继承自上下文的心的localContext。然而person所做的改变是在默认上下文中,也即是localContext的父上下文。判断是否改变是根据localContext来判断的,结果就是hasChanges为NO。最终导致保存不成功。

那么改变一下就可以了。也即是我们自己来控制保存。
如下:

// 在默认的上下文中创建实体
Person *person = [Person MR_createEntity];
// 改变person,引起上下文的改变
person.name = @"test";
person.age = @(100);
[[NSManagedObjectContext MR_defaultContext] MR_saveWithOptions:MRSaveParentContexts
                                                    completion:^(BOOL contextDidSave, NSError * _Nullable error) {

}];


## 总结:

多看官方文档,多看三方库wiki,多总结。

养成有耐心的习惯。勿急躁。

你可能感兴趣的:(Magical Record 全面解析)