主要参考objc.io关于CoreData的文章(https://www.objc.io/issues/4-core-data/ )和magicalRecord(https://github.com/magicalpanda/MagicalRecord )的设计;ef
1.CoreData的结构:
要理解coreData,首先要了解它的结构,即它包含了哪些部分、这些部分又是怎么关联起来的。先上个图,从objc.io里盗来的图:
coreData是一个数据库管理框架,那么肯定要和数据库打交道,图里的File System就代表数据库文件操作;谁来直接管理这些,SQLite,可以理解coreData是建立在SQLite之上的,它内部的数据操作仍然是SQLite.
SQLite本身是一个独立的框架,coreData想要使用它管理数据文件,肯定需要一个角色来负责,这就是NSPersistentStore。看一下NSPersistentStore的一般构建方法,类似:
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];
可以看得出,对于它来说,基本的需求是类型和地址,也就是决定在什么位置建立一个什么样的数据库。所以图的下半部分,或者说coreData的整体功能的一端,就是和本地的数据库文件交互。
然后,从上面看。首先是NSManagedObject,它的类代表了一种数据模型,而类的每个对象对应着一条数据。要写的舒服开心,肯定是要有数据模型的,这样存储的过程就不需要程序员自己动手了。模型具有什么属性,是什么类型,有这些,框架自身就能处理好数据以什么格式写入的问题,所以也就可以摆脱SQL语句拉,我们也就可以像使用其他类一样处理NSManagedObject了。
而NSManagedObjectContext,一般命名里带Context的,都像是一个工作场地,它们把一个操作的所需要的东西都保存进来,统一管理配置,就像绘图的CGContextRef。这里的NSManagedObjectContext就是管理NSManagedObject的,设想,你在内存里构建了一个对象,也就是一条数据,你修改它了,谁知道?删除它了,谁知道?都是NSManagedObjectContext在监控,只有把所有的增删改查都保存下来了,那么到时只要save一下,内存里改变的东西才会如实的反馈到数据库文件里。
最后,回到中间,NSPersistentStoreCoordinator;很明显,它是连接两端的纽带。所以整个流程就是:1、数据(NSManagedObject)被创建修改或删除等等,这些都被context看在眼里,然后你要保存了,context把修改的信息提交给,context根据数据信息找到正确的数据库文件,根据数据模型,正确的把数据写入到数据库文件里。2、查询时,context把查询条件提交给Coordinator,它去数据库文件里把数据查出来,给context,context再把这些数据和以在它管理内的结合。
2.构建一个CoreData stack:
上面图里的整个联系在一起的东西称为一个CoreData stack,那么怎么用代码把真个框架搭起来呢?
构建的入口在模型,模型决定了数据库表该建成啥样,context里的数据该是什么样。因为是从设计模块的角度来认识CoreData,所以还要考虑下类设计的问题:通过模型文件构建代码NSPersistentStoreCoordinator,由它再构建其它的,所以完全可以使用一个类方法,只提供一个字符串做参数,构建整个CoreData stack,那么以后使用CoreData的时候,一句话搞定。
所以我定义了一个DataManager的类,提供一个类方法:
+(BOOL)setupDataBaseWithModelName:(NSString*)modelName;
+(BOOL)setupDataBaseWithModelName:(NSString *)modelName{
if ([DataManager shareInstance].coordinator) {
return [DataManager shareInstance].coordinator;
}
//(1)构建Coordinator
NSString* filePath = [[NSBundle mainBundle]pathForResource:modelName ofType:@"momd"];
NSURL* modelURL = [NSURL URLWithString:filePath];
NSAssert(modelURL, @"数据模型文件缺失");
NSManagedObjectModel* model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSPersistentStoreCoordinator* coordinator = [[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:model];
//(2)构建数据库文件
NSError* error = nil;
NSURL* storeURL = [NSPersistentStore storeURLForModelName:modelName];
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];
if (error) {
NSLog(@"%@",[error localizedDescription]);
return NO;
}
[DataManager shareInstance].coordinator = coordinator;
//(3)构建context
[NSManagedObjectContext setDefaultContextWithCoordinator:coordinator];
return YES;
}
(案例代码,放在了https://github.com/FindCrt/coreDataExample_sinaWeibo, CoreData相关的代码都在coreDataModel文件夹里 )
注释标记了3个部分,第一部分构建Coordinator,有文件构建模型NSManagedObjectModel,由模型构建Coordinator;NSManagedObjectModel还有一个比较有意思的方法:
+ (nullable NSManagedObjectModel *)mergedModelFromBundles:(nullable NSArray<NSBundle *> *)bundles;
是把指定bundle里面的模型文件合成了一个模型,统一管理了,这样也好。否则得多个Coordinator,那查某个数据,还得分清是哪个Coordinator。
第二部分由Coordinator构建数据库,地址我是用一个NSPersistentStore的category方法来提供的,把文件存在沙盒的application support文件夹下,使用模型名区别。在magicalRecord里,大量的使用了category,好处是分工明确。比如一个数据库文件的地址,这个肯定是和NSPersistentStore一一对应的,也是属于它使用的东西,使用category,把这个任务交给它自身,多好。
第三部分,是构建context。为了在多线程的情况下能够使用Coredata,构建了一系列相关联的context。这个等下专门写。
基本的基础建设好,就要提供服务了:增删改查的操作。
3.增删改查服务:
//添加
+(NSManagedObject*)createEntityNamedWith:(NSString*)entityName;
+(NSManagedObject *)createEntityNamedWith:(NSString *)entityName{
NSManagedObject* entity = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:[NSManagedObjectContext contextForCurrentThread]];
return entity;
}
提供了两个参数,一个实体名、一个context,数据都要建在context来管理,肯定要指定context;然后context是关联到Coordinator的,而Coordinator有所有的模型数据,那么通过名字就可以知道这个实体的数据结构了,这样就没有问题了。
//删除
+(void)deleteEntity:(NSManagedObject*)entity;
+(void)deleteEntity:(NSManagedObject *)entity{
[entity.managedObjectContext deleteObject:entity];
}
很简单,context有提供删除方法,只要实体的context把它自身删除即可。这个执行过程暴露了一个问题,就是数据只由context来管,如果一个数据的context没了,它就没法做任何操作了。
//修改
+(BOOL)saveContextForCurrentThead;
+(BOOL)saveContextForCurrentThead{
return [[NSManagedObjectContext contextForCurrentThread]save];
}
对数据对象的修改,都会被记录,所以修改的关键不是修改,而是如何把内存里数据的修改同步到数据库文件里,也就是保存的问题。context本身提供了修改,考虑到多线程处理,自定义了save来做了一些处理。
//查询
+(NSArray*)fetchEntity:(NSString*)entityName withPredicate:(NSPredicate*)predicate;
+(NSArray *)fetchEntity:(NSString *)entityName withPredicate:(NSPredicate *)predicate{
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:entityName];
request.predicate = predicate;
NSError* error = nil;
NSArray* result = [[NSManagedObjectContext contextForCurrentThread]executeFetchRequest:request error:&error];
if (!result) {
NSLog(@"fetch %@ error :%@",entityName,[error localizedDescription]);
return nil;
}
return result;
}
因为数据对象都是context在管理,所以查询也是它来处理。CoreData专门用于查询的类NSFetchRequest,它有许多的属性,可以满足丰富的查询需求,但最基本的两个是: entityName和predicate。也就是指定查哪个数据,使用什么筛选条件。
关于查询,有一个概念是fault和fire fault。当你查询的时候,数据并没有加载到内存里,得到的只是每条数据的ID,在手机里的地址等,这时数据其实是空的,称为fault;当你访问了这个数据对象的一个属性的时候,才会把实际数据加载到内存里,也就是真的用到的时候才加载。这种机制也是经常出现的吧。
从这里可以看出,我们需要处理的是上面架构图里的上部分,而CoreData帮我们把处理反馈到下部分的数据库文件里。所以操作的方向是自上而下的,然后看了下各个类的引用,果然是:
NSManagedObject有属性NSManagedObjectContext *managedObjectContext,
而NSManagedObjectContext有属性NSPersistentStoreCoordinator *persistentStoreCoordinator;
而NSPersistentStoreCoordinator有NSArray<__kindof NSPersistentStore *> *persistentStores;