《CoreData》系列(二)

CoreData数据迁移以及版本升级

1 概述

为什么要有数据迁移?
由于CoreData可视化的特殊性,那么当数据模型发生变化时,相应的sqlite数据库的表由于不知道model发生了变化,表结构必须相应的做出调整,否则会导致程序Crash,CoreData的解决方案是通过创建新的sqlite表,然后将旧的数据迁移到新表上得方案来处理。下面分别介绍三种数据迁移的方式,并详细说明三种迁移方式的应用场景和注意事项。

1.轻量级的数据迁移方式
2.默认的迁移方式
3.使用迁移管理器

1.1 轻量级的数据迁移方式

轻量级的数据迁移,也就是说,并不需要程序员做很多事情就可以完成数据的迁移,是由系统默认进行的数据迁移。
那么如何进行轻量级的数据迁移呢,当model的表字段发生变化,且应用程序已经发布过版本时,此时千万不能单单修改原model来达到修改model的目的,如果这样做的话,程序会crash。正确的做法是,

1.新建一个model,并将model命名为model2,并将model2设置为当前model。
2.修改NSPersistentStoreCoordinator加载缓存区的配置。具体如下

1
2
3
4
5
6
7
8
9
NSDictionary *option = @{NSMigratePersistentStoresAutomaticallyOption:@(YES),  
NSInferMappingModelAutomaticallyOption:@(YES),
};

_store = [_coordinate addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeUrl]
options:option
error:&error];

tips:使用iCloud开发程序的app,只能使用这种迁移方式。

1.2 默认的迁移方式

正常情况下,使用轻量级的数据迁移已经足够了,但是如果由于开发需要,需要将某个Entity下面的某个Attribute迁移到新的Entity下的某个Attribute,那么轻量级的迁移方式就不能够满足需求,这个时候就需要使用默认的迁移方式来进行数据迁移。这里以一个例子代码来详细阐述如何进行默认的迁移

现在要将Model2里面的Measurement下面的name迁移到Account里面的下面的xyz属性下。
1.根据model2来创建一个新model,并命名为model3,然后将model3设置为currentmodel。
2.添加新的entity,并命名为Account,添加attribute xyz。
3.删除model2里面的Measurement,根据model3创建NSManagerObect的子类Account。
4.以model2为soureModel,model3为destinationModel添加一个MappingModel
5.按照下图所示设置映射model即可
6.最后记得将NSInferMappingModelAutomaticallyOption设置为Yes(coredata会优先读取映射model,如果没有就会自己推断),至此,默认的迁移方式就算是搞定了。


1.3 迁移管理器

简单概述下何为迁移管理器,迁移管理器,就是不再使用系统的NSPersistentCoordinator进行数据迁移,而是使用NSMigrationManager进行数据缓存区的迁移。并配合一个数据迁移视图控制器提供优雅的迁移等待界面。等待界面如下,是不是感觉很丑呢,哈哈。那么使用迁移管理器的好处又是什么呢?可以实现更加精细化的数据操作,此外还能向用户报告迁移进度。有这俩点,还不够我们去研究下它么?Let’s go!

准备工作
何时启用迁移管理器,即迁移的时机?
迁移工作如何进行?
迁移完成如何善后?

下面对上面的问题一一来做解答
迁移的时机,迁移工作需要在载入数据库的时候进行,即上节所讲的 loadStore:的时候进行,但是呢?还需要做一些判断工作。具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (void)loadStore  
{
if (debug) {
NSLog(@"Running %@ ,'%@'",[self class], NSStringFromSelector(_cmd));
}

if (_store) {
return;
}

BOOL useMigrateManager = MigrationMode;

if (useMigrateManager && [self isMigrationNecessaryForStore:[self storeUrl]]) {
[self performBackgroundManagedMigrationForStore:[self storeUrl]];
}else{
NSError *error;

//NSMigratePersistentStoresAutomaticallyOption coreData尝试将低版本的数据模型向高版本进行迁移
//NSInferMappingModelAutomaticallyOption coredata会自动创建迁移模型,会去自动尝试
NSDictionary *option = @{NSMigratePersistentStoresAutomaticallyOption:@(YES),
NSInferMappingModelAutomaticallyOption:@(YES),
NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}};

_store = [_coordinate addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeUrl]
options:option
error:&error];
if (!_store) {
if (debug) {
NSLog(@"failed load store,error = %@",error);
abort();
}
}
else/**/{
NSLog(@"successfully add store : %@",_store);
}
}
}

其中有开关,用来控制是否使用迁移管理器,以及系统是否需要进行迁移的判断。系统是否需要迁移的判断代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (BOOL)isMigrationNecessaryForStore:(NSURL *)storeUrl  
{
if (debug) {
NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
}

//文件是否存在,如果不存在认为是用户设备上并没有持久化存储区,自然不需要迁移
if (![[NSFileManager defaultManager]fileExistsAtPath:[self storeUrl].path isDirectory:nil]) {
if (debug) {
NSLog(@"Skipped Migration, source database missing");
}
return NO;
}

NSError *error = nil;
NSDictionary *sourceMetaData = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
URL:storeUrl
error:&error];
NSManagedObjectModel *destinationModel = _coordinate.managedObjectModel;

//比较当前对象模型是否与用户之前安装的应用持久化存储区是否兼容。如果兼容,不需要迁移
if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetaData]) {
if (debug) {
NSLog(@"Skipped Migration, source database is already compatible");
return NO;
}
}

//所有情况都尝试了,发现还是需要进行数据迁移
return YES;
}

迁移工作如何进行,众所周知,迁移工作是一项比较耗时间的工作,尤其是在数据库比较大的情况下,那么肯定不能放在前台进行,必须放在后台进行,前台展示加载进度,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- (void)performBackgroundManagedMigrationForStore:(NSURL *)store  
{
if (debug) {
NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
}

UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
self.migrationVC = [sb instantiateViewControllerWithIdentifier:@"migration"];

UIApplication *app = [UIApplication sharedApplication];
UINavigationController *navigationCtl = (UINavigationController *)[app keyWindow].rootViewController;

[navigationCtl presentViewController:self.migrationVC
animated:YES
completion:nil];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{

BOOL done = [self migrateStore:[self storeUrl]];
if (done) {
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = nil;

NSDictionary *configuration = @{NSMigratePersistentStoresAutomaticallyOption:@(YES),
NSInferMappingModelAutomaticallyOption:@(YES),
NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}};

_store = [_coordinate addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeUrl]
options:configuration
error:&error];
if (_store) {
if (debug) {
NSLog(@"success create store");
}
}else {
if (debug) {
NSLog(@"failed, error = %@",error);
}
abort();
}

[self.migrationVC dismissViewControllerAnimated:YES
completion:nil];

self.migrationVC = nil;
});
}

});
}

接下来是,真正的迁移过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
- (BOOL)migrateStore:(NSURL *)store  
{
if (debug) {
NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
}


NSDictionary *sourceMeta = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
URL:store
error:nil];

NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil
forStoreMetadata:sourceMeta];

NSManagedObjectModel *destinationModel = _model;
NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:nil
forSourceModel:sourceModel
destinationModel:destinationModel];
if (mappingModel) {
NSError *error = nil;

NSMigrationManager *migrationManager = [[NSMigrationManager alloc]initWithSourceModel:sourceModel
destinationModel:destinationModel];

[migrationManager addObserver:self
forKeyPath:@"migrationProgress"
options:NSKeyValueObservingOptionNew
context:nil];

NSURL *destinationStore = [[self applicationStoreDirectory]URLByAppendingPathComponent:@"temp.sqlite"];
BOOL success = NO;
success = [migrationManager migrateStoreFromURL:store
type:NSSQLiteStoreType
options:nil
withMappingModel:mappingModel
toDestinationURL:destinationStore
destinationType:NSSQLiteStoreType
destinationOptions:nil
error:&error];
if (success) {
if (debug) {
NSLog(@"Migration Successfully!");
}
if ([self replaceStore:store withStore:destinationStore]) {
[migrationManager removeObserver:self forKeyPath:@"migrationProgress" context:NULL];
[[NSNotificationCenter defaultCenter]postNotificationName:someThingChangedNotification object:nil];
}
}else{
if (debug) {
NSLog(@"Migration Failed");
}
}
}else{
if (debug) {
NSLog(@"Mapping model is NULL");
}
}
return YES;
}

最后附上俩个辅助方法,用来观察迁移过程和替换数据库的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (BOOL)replaceStore:(NSURL *)old withStore:(NSURL *)new  
{
BOOL success = NO;
NSError *error = nil;
if ([[NSFileManager defaultManager]removeItemAtURL:old error:&error]) {
error = nil;
if ([[NSFileManager defaultManager]moveItemAtURL:new toURL:old error:&error]) {
success = YES;
}else {
if (debug) {
NSLog(@"failed move new store to old");
}
}
}else{
if (debug) {
NSLog(@"failed remove old store");
}
}
return success;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(voidvoid *)context  
{
if ([keyPath isEqualToString:@"migrationProgress"]) {
dispatch_async(dispatch_get_main_queue(), ^{
float progress = [[change objectForKey:NSKeyValueChangeNewKey]floatValue];
self.migrationVC.progressView.progress = progress;

int percenttage = progress * 100;
NSString *string = [NSString stringWithFormat:@"Migration Progress %i%%",percenttage];
self.migrationVC.progressLabel.text = string;
});
}
}

至此,三种数据迁移的方式,都已叙述完毕。
[2 小结](#1)
三种迁移方式,各有各的好处,轻量级的迁移可以配套icloud实现云端存储,默认的数据迁移,支持将属性级别的数据进行任意迁移。迁移管理器,可以管理文件存储路径,并能够报告迁移进度,我们在开发过程中,应该按照自己的需求合理选择迁移方式,下一小节结合NSFetchedResultController进行数据的实际应用。

《CoreData》系列(一)

《题外篇》

学习这个东西贵在日积月累,而且事情往往说起来容易,做起来难,我是一个资深dota玩家,从dota1到dota2,从大学到工作,从2008年到2015年。一直看2009的视频,经常吐槽09视频更新速度慢,但是细细想想,09能保持优酷更新401(最近查看)期视频。又有多少人能做的到。
故而最近下了一个决定,每周五务必更新一篇技术博客,就看看自己能坚持多久。

《正文》

1 概述
本系列研究讨论的是iOS开发中的一种数据持久化技术-coredata。coredata、sqlite、fmdb的优缺点不是我要讨论的重点

这个系列的blog主要会研究讨论以下几点
1.快速搭建coredata环境,主要是连接数据库、创建数据库托管对象模型(NSManagerObject)、如何保存数据、查询数据?
2.coredata升级以及数据迁移的三种方式。
3.coredata与viewcontroller的结合,通过NSFetchedResultController使用coredata数据。
4.导入默认数据和前后台context。
5.关系

2 环境搭建

2.1 导入《CoreData》的framework
默认读者知道如何创建一个空白的项目,建立好空白项目之后,搜索coredata按图2-1操作,点击添加完成framework的引入。
图 2-1

2.2 创建Model
完成上述第一步,意味着我们已经可以使用CoreData提供的接口API了,接下来就是如何使用的事儿了。创建一个CoreData文件夹,专门用来放CoreData引擎,创建好文件夹后,右键点击选择newfile,然后按照图2-2所示创建数据库模型文件,并将其命名为Model,然后点击Model,添加Entity(表\Class),添加Attribute(字段\属性),到这一部,基本上就把Model,创建出来了。并且里面有了数据模型结构,接下来的问题就是,连接数据库,根据模型创建托管对象了
图 2-2

2.3 代码连接数据库
好啦,前面只是开胃菜,真正的大餐马上就要来了,在吃大餐前,有一些名称需要说明下

1.NSManagedObjectContext - 托管对象上下文,用来干嘛的呢?望文生意,用来管理托管对象的,负责从数据库中获取对象、保存对象、删除对象等等操作。
2.NSManagedObjectModel - 对象模型, 根据我们上面创建的数据模型,创建出托管对象模型,(类似于加工厂的概念,能够用来生产对象的模子)
3.NSPersistentStoreCoordinator - 持久化存储协调器,包含数据库的名称、存储数据类型(Sqlite、Xml、内存)、位置等信息
4.NSPersistentStore - 持久化存储区

另外再附一张图来说明这几者的依赖关系

由于CoreData管理数据的过程较为通用,个人觉的还是封装成一个管理对象较好,方便以后代码复用,这里创建一个CoreDataHelper的类,专门用来管理数据对象,该类的头文件如下

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>  
#import <CoreData/CoreData.h>
@interface CoreDataHelper : NSObject
@property (nonatomic, strong) NSManagedObjectContext *context;//托管对象上下文
@property (nonatomic, strong) NSManagedObjectModel *model;//托管对象模型
@property (nonatomic, strong) NSPersistentStoreCoordinator *coordinate;//持久化存储协调器
@property (nonatomic, strong) NSPersistentStore *store;//持久化存储区
- (id)init; //初始化
- (void)loadStore; //加载cordite
- (void)setupCoreData; //设置cordite相关信息
- (void)saveContext; //保存context

CoreDataHelp的实现文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#import "CoreDataHelper.h"  
static NSString *storeFileName = @"demo.sqlite"; //测试数据库

@implementation CoreDataHelper

#pragma mark - PATHS

- (NSString *)applicationDocumentDirectory
{
if (debug) {
NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
}

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
return [paths lastObject];
}

- (NSURL *)applicationStoreDirectory
{
if (debug) {
NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
}

NSURL *url = [[NSURL fileURLWithPath:[self applicationDocumentDirectory]] URLByAppendingPathComponent:@"stores"];
if (![[NSFileManager defaultManager]fileExistsAtPath:[url path]]) {
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager]createDirectoryAtURL:url
withIntermediateDirectories:YES
attributes:nil
error:&error];
if (success) {
if (debug) {
NSLog(@"success create directory!");
}
}else{
NSLog(@"failed create directory!");
}
}

return url;
}

- (NSURL *)storeUrl
{
NSURL *storeUrl = [[self applicationStoreDirectory]URLByAppendingPathComponent:storeFileName];
NSLog(@"storeurl = %@",storeUrl);
return storeUrl;
}

#pragma mark - SETUP
- (id)init
{
if (self) {
_model = [NSManagedObjectModel mergedModelFromBundles:nil];
_coordinate = [[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:_model];
_context = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSMainQueueConcurrencyType];
[_context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[_context setPersistentStoreCoordinator:_coordinate];

_importContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];

[_importContext performBlock:^{
[_importContext setPersistentStoreCoordinator:_coordinate];
[_importContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[_importContext setUndoManager:nil];
}];
}
return self;
}

- (void)loadStore
{
if (debug) {
NSLog(@"Running %@ ,'%@'",[self class], NSStringFromSelector(_cmd));
}

if (_store) {
return;
}

NSError *error;

//NSMigratePersistentStoresAutomaticallyOption coreData尝试将低版本的数据模型向高版本进行迁移
//NSInferMappingModelAutomaticallyOption coredata会自动创建迁移模型,会去自动尝试
NSDictionary *option = @{NSMigratePersistentStoresAutomaticallyOption:@(YES),
NSInferMappingModelAutomaticallyOption:@(YES),
NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}};

_store = [_coordinate addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeUrl]
options:option
error:&error];
if (!_store) {
if (debug) {
NSLog(@"failed load store,error = %@",error);
abort();
}
}
else/**/{
NSLog(@"successfully add store : %@",_store);
}
}

- (void)setupCoreData
{
[self loadStore];
}

- (void)saveContext
{
if ([_context hasChanges]) {
NSError *error = nil;
if ([_context save:&error]) {
NSLog(@"context save successfully");
}else{
NSLog(@"failed save %@",error);
}
}else{
NSLog(@"skipped context save , there is no changes");
}
}

@end

2.4 最后附上查询和保存数据库的代码
在AppDelegate.m文件里写一个方法,用来初始化CoreData数据库

1
2
3
4
5
6
7
8
9
10
11
12
- (CoreDataHelper *)cdh  
{
if (!_cdh) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_cdh = [CoreDataHelper new];

});
[_cdh setupCoreData];
}
return _cdh;
}

下面是插入数据,查询、保存数据的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)demo  
{
Item *bananer = [NSEntityDescription insertNewObjectForEntityForName:@"Item" inManagedObjectContext:[[self cdh] context]];
bananer.unit = kg;
bananer.name = @"bananer";

Item *oranger = [NSEntityDescription insertNewObjectForEntityForName:@"Item" inManagedObjectContext:[[self cdh] context]];
oranger.unit = kg;
oranger.name = @"Oranger";

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Item"];
NSArray *result = [[[self cdh]context] executeFetchRequest:request error:nil];
for (Item *item in result) {
if (debug) {
NSLog(@"item.name = %@",item.name);
}
}
[[self cdh]saveContext];
}

2.5 小结
经过这么一番下来,终于将CoreData技术应用到我们的项目中了,我们现在能做到,把数据插入到数据库、也能从数据库中读取出数据来,也能保存数据。但是要注意这才是刚刚开始,接下来还有更多的coredata问题等着我们,比方说下节要介绍的数据迁移问题。