3.5 通过迁移管理器来迁移数据
除了通过NSPersistentStoreCoordinator来迁移存储区之外,还可以采用迁移管理器来做。迁移管理器可以使开发者全权掌控迁移过程中创建的文件,从而令他们能够按自己的方式来灵活处理迁移中的各种问题。使用迁移管理器的一个好处就是可以向用户报告迁移进度,使用户知道应用程序哪次会启动得比较慢一些,所以需要耐心等待。虽说迁移过程理应执行得非常快才对,但当数据库比较大、变动比较复杂时,迁移过程就需要耗费一定的时间了。为了使用户界面保持流畅,迁移过程必须在后台线程里执行。只有这样做,用户界面才能反应灵敏,并能把最新动态提供给用户。实现数据迁移的难点在于如何防止用户在迁移过程中操作应用程序。由于此时数据尚未准备好,所以我们必须做这个限制,否则用户就会对着黑屏不知所措。
请按下列步骤修改Grocery Dude,以便配置Migration 视图控制器(View Controller):
1. 选定Main.storyboard。
2. 向故事板(storyboard)中拖放一个新的View Controller,将它摆在现有的Navigation 控制器上方。
3. 向这个新的视图控制器中拖放Label与Progress View控件。
4. 把Progress View放在视图控制器正中,并将Label放在Progress View上方。
5. 按图3-10拓宽Label与Progress View控件,使其宽度与视图控制器相符。
6. 把Label的文本改为Migration Progress 0%,并将其居中(Centered),如图3-10左侧所示。
7. 把Progress View的进度(progress)设为0。
8. 选中视图控制器,然后在Identity Inspector界面(可以按“Option++3”组合键调出该界面)中把视图控制器的Storyboard ID设为migration。
9. 点击Editor>Resolve Auto Layout Issues>Reset to Suggested Constraints in View Controller菜单项。最终结果如图3-10所示。
由于Migration 视图控制器里的UILabel控件及UIProgressView控件需要在迁移过程中更新,所以我们需要一种能在代码中使用这两个控件的方式。为此,我们新建UIViewController的子类,并将其命名为MigrationVC。
请按下列步骤修改Grocery Dude,以便将MigrationVC类添加到新的组中:
1. 在现有的Grocery Dude 组上面右击鼠标,选择New Group。
2. 把新组的名字改为Grocery Dude View Controllers。
3. 选中Grocery Dude View Controllers 组。
4. 点击File>New>File...菜单项。
5. 新建iOS>Cocoa Touch>Objective-C class,并点击Next按钮。
6. 把Subclass of设为UIViewController,并把Class名称设为MigrationVC,然后点击Next按钮。
7. 确保Targets中的“Grocery Dude”处于勾选状态,然后点击Create,在Grocery Dude项目的文件夹中创建这个类。
8. 选中Main.storyboard。
9. 将Migration 视图控制器选中,在Identity Inspector界面(可按“Option++3”组合键调出该界面)里把Custom Class设为MigrationVC。该选项和刚才设置的Storyboard ID都位于同一界面中。
10. 点击View>Assistant Editor>Show Assistant Editor菜单项(或按“Option++Return”组合键),把Assistant Editor界面显示出来。
11. 此时Assistant Editor界面中应该就会自动显示出MigrationVC.h文件了。图3-11右上角是该界面的样貌。如果显示的不是这个文件,那可以先把migration 视图控制器选中,然后点击界面上方的Manual或Automatic,再选择MigrationVC.h。
12. 点击Control键并按住鼠标左键不放,从显示迁移进度的Label控件开始沿直线拖到MigrationVC.h代码的@end上方。松开鼠标左键之后,会弹出一个对话框,该对话框中列出了类型为UILabel的新特性,我们把该特性的Name设为label,并确保Storage是Strong,然后点击Connect。配置好的各选项如图3-11所示。
13. 按第12步所讲的操作方式,把Progress View(进度视图)同UIProgressView类型的特性链接起来,并将该特性命名为progressView。
为了向用户报告迁移进度,我们需要在CoreDataHelper.h文件中声明指向migration 视图控制器的指针。
请按下列步骤修改Grocery Dude,以添加新特性:
1. 点击View>Standard Editor>Show Standard Editor菜单项或按“+Return”组合键,把Standard Editor界面显示出来。
2. 把#import "MigrationVC.h"添加到CoreDataHelper.h文件顶部。
3. 在CoreDataHelper.h文件的现有特性下方添加@property(nonatomic, retain)MigrationVC*migrationVC;。
如果采用手动方式迁移数据,那就得在每次启动应用程序时判断数据是否需要迁移。为了做出该判断,我们需要知道存储区的URL,以便检查系统里是不是有这个存储区。如果有的话,那还要把存储区里的“模型元数据”(model metadata)与新的模型相比较,并根据比较的结果来判断新模型是否与现有的存储区相兼容。假如不兼容,那就要迁移数据了。把刚才说的这段逻辑写成代码,就得到了程序清单3-5中的isMigrationNecessary-ForStore方法。
请按下列步骤修改Grocery Dude,以便实现新的MIGRATION MANAGER 部分:
1. 将程序清单3-5中的代码添加到CoreDataHelper.m文件底部,并将其放在@end语句之前。
假如已经确定要迁移数据,那么接下来就该执行迁移了。此过程分为三步,程序清单3-6中的注释写明了这三个步骤。
STEP 1(第一步) 用于收集执行数据迁移所需的信息,这些信息分别是:
源模型,也就是通过NSPersistentStoreCoordinator的metadataFor-PersistentStoreOfType方法从持久化存储区里获取到的元数据。
目标模型,也就是_model实例变量。
映射模型,该模型由系统自动决定,开发者只需把nil当做mappingModelFrom-Bundles:forSourceModel:destinationModel:方法的第一个参数,并把源模型和目标模型也一并传过去即可。
STEP 2(第二步) 就是实际的迁移过程。我们先用源模型与目标模型创建NSMigra-tionManager实例,然后在调用migrateStoreFromURL之前,还需把目标存储区准备好。该目标存储区只是个为迁移而设的临时存储区。
STEP 3(第三步) 只有在顺利完成迁移时才会触发。replaceStore方法用于在迁移完成后清理旧的存储区。执行完第二步之后,目标位置上就会出现一份新的存储区了,但是,我们还必须把这个迁移过来的新存储区放回到原来的位置上,并且要把它的文件名起得和旧存储区一样,唯有如此,Core Data才能使用这个新存储区。为了使用新迁移好的存储区,我们需要把旧存储区删掉,并将新存储区放到旧存储区的位置上。当开发自己的项目时,也可以在删除旧存储区之前先把它备份到某处。是否需要备份,由你自己决定,如果真要备份,那可能得稍微修改一下replaceStore方法。备份旧存储区会导致应用程序在迁移过程中对存储量的需求翻倍。
当迁移进度有变化时,系统会调用observeValueForKeyPath方法,而我们可以通过该方法把目前的迁移进度告知用户。migrationManager的migrationProgress特性一旦改变,我们就可通过该方法来更新migrationVC。
程序清单3-7列出了observeValueForKeyPath及replaceStore方法的代码。
请按下列步骤修改Grocery Dude,以继续实现MIGRATION MANAGER 部分:
1. 把程序清单3-7里的代码添加到CoreDataHelper.m文件MIGRATION MANAGER部分的底部,并放在@end上方,然后按同样方式把程序清单3-6里的代码也加进去。
为了在后台通过migrationManager来迁移数据,我们需要使用程序清单3-8中的方法。
performBackgroundManagedMigrationForStore方法用故事板标识符来实例化视图控制器,并把它展示给用户。用户的操作由这个视图来接受,而我们则可以开始迁移数据了。migrateStore方法会在后台线程中执行。等迁移完数据,我们就可像平常那样通过_coordinator来添加存储区,并把显示迁移进度所用的view关闭,然后,应用程序就可以照常往下运行了。
请按下列步骤修改Grocery Dude,以继续实现MIGRATION MANAGER部分:
1. 把程序清单3-8里的代码添加到CoreDataHelper.m文件MIGRATION MANAGER 部分的底部,并放在@end语句上方。
检测是否需要执行数据迁移的最佳时机应该是在把存储区添加到_coordinator前的那一刻。为了安排好这项检测,我们需要修改CoreDataHelper.m文件中的loadStore方法。如果真的要迁移,那么迁移操作就会在此刻触发。相关代码如程序清单3-9所示。
请按下列步骤修改Grocery Dude,以完成本节范例:
1. 修改CoreDataHelper.m文件中的loadStore方法,用程序清单3-9里的代码把原有代码替换掉。
2. 基于Model 3,添加名为Model 4的模型版本。
3. 选定Model 4.xcdatamodel。
4. 删除Amount实体。
5. 新增名为Unit的实体,并添加名为name的字符串类型属性。
6. 根据Unit实体来创建NSManagedObject子类。在保存类文件的这一步里,别忘了勾选Targets中的“Grocery Dude”。
7. 将Model 4设为当前模型。
8. 以Model 3为源模型,以Model 4为目标模型,新建映射模型。在保存映射模型文件的这一步里,别忘了勾选Targets中的“Grocery Dude”,然后,把这个模型存为Model3toModel4。
9. 选定Model3toModel4.xcmappingmodel。
10. 选定ENTITIES MAPPINGS中的Unit。
11. 将Unit实体的Source设为Amount,并给名为name的Destination 属性设定Value Expression,将这个Value Expression写成$source.xyz。此时ENTITIES MAPPINGS中的Unit实体应该会自动改名为AmountToUnit,如图3-12所示。
现在基本上已经可以开始执行迁移了,不过demo方法里的获取请求仍然在使用旧的Amount实体。
请按下列步骤修改Grocery Dude,使demo方法不再获取Amount实体,而是改为获取Unit实体:
1. 把AppDelegate.m文件顶部的#import "Amount.h"替换成#import "Unit.h"。
2.修改AppDelegate.m文件的demo方法,用程序清单3-10中的代码替换原有代码。新的代码只从持久化存储区里获取50个Unit对象。
迁移管理器终于实现好了!运行应用程序,仔细观察设备屏幕!你眼前会出现Migration Progress界面,它会显示数据的迁移进度。同时,这个进度也会出现在控制台里。
请用第2章讲过的办法来查看Grocery-Dude.sqlite文件中ZUNIT表的内容。正常的结果如图3-14所示。假如你已把默认的日志记录模式关闭,但Stores目录里却还有-wal文件,那么请点击Product>Clean菜单项并重新运行应用程序,然后再次查看sqlite文件。
操作结果要是和图3-14一样的话,那你真的太棒了,因为你已把三种模型迁移方式全部实现出来了!本书接下来的内容要使用轻量级迁移方式,所以现在必须重新启用它。
请按下列步骤修改Grocery Dude,以重新启用轻量级迁移方式:
1. 修改CoreDataHelper.m文件的loadStore方法,把NSInferMappingModel-AutomaticallyOption选项改为@YES。
2. 修改CoreDataHelper.m文件的loadStore方法,把useMigrationManager设为NO。
3. 删除AppDelegate.m文件demo方法中的所有代码。
旧的映射模型以及根据旧实体所创建出来的NSManagedObject子类现在都已经没有用处了。虽说也可以把它们删掉,但为了便于日后查阅,我们还是将其留在项目之中吧。