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 objects
的managed object context
容器
可能有点绕,不过一看图世界就清晰了
如下图:
Managed Object
Managed Object是一个模型对象(模型-视图-控制器的意义上),它代表了一个持久存储的记录。管理对象是实例NSManagedObject或子类NSManagedObject。
管理对象有一个实体的描述对象,告诉它代表着什么实体的引用。以这种方式,NSManagedObject可以表示任何实体不需要每个实体的唯一的子类。如果要实现自定义行为,例如计算派生属性值,或者为了实现验证逻辑可以使用一个子类。
还是来看图:
Managed Object Model
Manage Context Object
Manage Context Object代表单个对象的空间,,在核心数据的应用程序。管理对象上下文的一个实例的NSManagedObjectContext。它的主要职责是管理管理对象的集合。这些管理对象代表一个或多个持久存储的一个内部一致的看法。上下文是在管理对象的生命周期核心作用。
上下文是在核心数据堆栈中的中心对象。这是你用它来创建和获取管理对象和管理撤消和恢复操作的对象。内的给定范围内,有至多一个被管理目标代表在永久存储器的任何给定的记录。
上下文被连接到一个父对象存储。这通常是一个持久存储协调,但可能是另一个管理对象上下文。当你获取对象,上下文要求其父对象存储返回那些符合提取请求的对象。您对管理对象的修改,直到您保存的背景下不被提交到父store。
在某些应用中,你可能想保持独立组来管理对象和编辑这些对象的; 或者你可能需要执行使用一个上下文,同时允许用户与另一个对象交互的后台操作
Persistent Store Coordinator
哎!翻译太累了。直接上图吧
这张图把这个的架构解释得非常清楚
Fetch Request
开始使用Magical Record
导入MagicalRecord.h
在项目的预编译文件*.pch
中。这保证了可以全局访问所需要的头文件。
使用了CocoaPods
或者MagicalRecord.framework
,用如下方式导入:
// Objective-C
#import <MagicalRecord/MagicalRecord.h>
// 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
- [NSManagedObjectContext MR_newContextWithStoreCoordinator:…]: 允许你具体化
默认上下文
当使用CoreData
,你将不断的和两个主要的对象打交道,NSManagedObject
和 NSManagedObjectContext.
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];
关于每一行的调用, NSFetchRequest
和 NSSortDescriptor
作为排序的标配。
自定有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,多总结。
养成有耐心的习惯。勿急躁。