在本系列的前几篇文章中,我们遇到了一个烦人的问题,需要解决。 每当我们修改Core Data应用程序的数据模型时,持久存储都将与数据模型不兼容。 结果是启动时发生崩溃,使应用程序无法使用,如果这在App Store中的应用程序发生,将是一个严重的问题。
我们的应用程序崩溃是因为,如果无法将持久性存储添加到持久性存储协调器中,我们将调用abort
。 需要明确的是, abort
功能使应用程序立即终止。
do {
// Add Persistent Store to Persistent Store Coordinator
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: nil)
} catch {
// Populate Error
var userInfo = [String: AnyObject]()
userInfo[NSLocalizedDescriptionKey] = "There was an error creating or loading the application's saved data."
userInfo[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data."
userInfo[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "com.tutsplus.Done", code: 1001, userInfo: userInfo)
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
但是,无需终止我们的应用程序,更不用说将其崩溃了。 如果Core Data告诉我们数据模型和持久性存储不兼容,那么我们就应该解决这个问题。
在本文中,我们将讨论两种从这种情况中恢复的选项,即迁移持久性存储和创建与修改后的数据模型兼容的新持久性存储。
1.问题
首先让我澄清一下我们要解决的问题。 下载我们在上一篇文章中创建的示例项目 ,然后在模拟器中运行它。 该应用程序应该可以正常运行。
打开Done.xcdatamodeld并将类型为Date的属性updatedAt添加到Item实体。 再运行一次该应用程序,并注意该应用程序在启动后如何崩溃。 幸运的是,Core Data为我们提供了出了什么问题的线索。 看一下Xcode控制台中的输出。
Done[897:14527] CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:file:///Users/Bart/Library/Developer/CoreSimulator/Devices/A263775B-4D73-48C8-BD79-825E0BED5128/data/Containers/Data/Application/E46663CA-79AF-4645-AF78-0A17236943E1/Documents/Done.sqlite options:(null) ... returned error Error Domain=NSCocoaErrorDomain Code=134100 "(null)" UserInfo={metadata={
NSPersistenceFrameworkVersion = 640;
NSStoreModelVersionHashes = {
Item = <4c880226 3219fc66 283b28c5 54f026dc 7f95af5f c19fb76e 255a26a7 2a2a79f5>;
};
NSStoreModelVersionHashesVersion = 3;
NSStoreModelVersionIdentifiers = (
""
);
NSStoreType = SQLite;
NSStoreUUID = "F0F98261-4F60-451A-9606-91E1F60425B9";
"_NSAutoVacuumLevel" = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store} with userInfo dictionary {
metadata = {
NSPersistenceFrameworkVersion = 640;
NSStoreModelVersionHashes = {
Item = <4c880226 3219fc66 283b28c5 54f026dc 7f95af5f c19fb76e 255a26a7 2a2a79f5>;
};
NSStoreModelVersionHashesVersion = 3;
NSStoreModelVersionIdentifiers = (
""
);
NSStoreType = SQLite;
NSStoreUUID = "F0F98261-4F60-451A-9606-91E1F60425B9";
"_NSAutoVacuumLevel" = 2;
};
reason = "The model used to open the store is incompatible with the one used to create the store";
}
快要结束时,Core Data告诉我们,用于打开持久性存储的数据模型与用于创建持久性存储的数据模型不兼容。 等待。 什么?
当我们第一次启动该应用程序时,Core Data根据数据模型创建了一个SQLite数据库。 但是,由于我们通过在Item实体中添加了一个属性updatedAt来更改了数据模型, 因此 Core Data不再了解它应如何在SQLite数据库中存储Item记录。 换句话说,修改后的数据模型不再与之前创建的持久存储SQLite数据库兼容。
2.解决方案
对我们来说幸运的是,苹果公司的一些聪明的工程师创造了一种解决方案,可以安全地修改数据模型而不会遇到兼容性问题。 为了解决我们面临的问题,我们需要找到一种方法来告诉Core Data数据模型的一个版本与另一个版本之间的关系。 没错,对数据模型进行版本控制是解决方案的一部分。
有了这些信息,Core Data可以了解如何更新持久性存储以与修改后的数据模型(即数据模型的新版本)兼容。 换句话说,我们需要向核心数据提供必要的信息,以将持久性存储从数据模型的一个版本迁移到另一个版本。
3.迁移
有两种类型的迁移,轻量迁移和重迁移。 “ 轻量级”和“ 沉重 ”一词具有很强的描述性,但是了解Core Data如何处理每种类型的迁移非常重要。
轻量级迁移
轻量级迁移只需要开发人员的少量工作即可。 我强烈建议您尽可能选择轻量级迁移,而不是繁重的迁移。 轻量级迁移的成本大大低于重度迁移的成本。
当然,轻量级迁移的另一面是,其功能不如重迁移。 您可以对具有轻量级迁移的数据模型进行的更改受到限制。 例如,轻量级迁移使您可以添加或重命名属性和实体,但不能修改属性的类型或现有实体之间的关系。
轻量级迁移非常适合扩展数据模型,添加属性和实体。 如果您打算修改关系或更改属性类型,那么您将需要进行大量迁移。
大量迁移
繁重的迁移有些棘手。 我再改一下。 大量的迁徙是脖子上的痛苦,如果可能的话,应尽量避免。 繁重的迁移是强大的,但这种迁移需要付出一定的代价。 繁重的迁移需要大量的工作和测试,以确保迁移成功完成,更重要的是,不会丢失数据。
如果我们进行更改,而Core Data无法通过比较数据模型的版本自动为我们推断出这些更改,则我们将进入繁重的迁移领域。 然后,核心数据将需要一个映射模型来了解数据模型的版本之间如何相互关联。 由于繁重的迁移是一个复杂的主题,因此在本系列文章中我们将不会涉及。
4.版本控制
如果您使用Ruby on Rails或任何其他支持迁移的框架进行了工作,那么Core Data迁移对您将很有意义。 这个想法很简单,但功能强大。 核心数据使我们能够对数据模型进行版本控制,这使我们能够安全地修改数据模型。 核心数据检查版本化的数据模型,以了解持久性存储与数据模型之间的关系。 通过查看版本化的数据模型,它还知道持久存储是否需要迁移,然后才能与数据模型的当前版本一起使用。
版本控制和迁移是并行的。 如果您想了解迁移的工作方式,则首先需要了解如何对Core Data数据模型进行版本控制。 让我们重新回顾上一篇文章中创建的待办事项应用程序。 如前所述,向Item实体添加属性updateAt会导致持久性存储与修改后的数据模型不兼容。 我们现在了解原因。
首先,打开Done.xcdatamodeld并从Item实体中删除updatedAt属性,从头开始。 现在是时候创建数据模型的新版本了。
选择数据模型后,从“ 编辑器”菜单中选择“ 添加模型版本... ”。 Xcode将要求您命名新的数据模型版本,更重要的是,新版本应基于哪个版本。 为了确保Core Data可以为我们迁移持久性存储,请务必选择数据模型的先前版本。 在此示例中,我们只有一个选择。
该操作的结果是,我们现在可以在Project Navigator中看到三个数据模型文件。 有一个扩展名为.xcdatamodeld的顶级数据模型,以及两个扩展名为.xcdatamodel的子级 。
您可以将.xcdatamodeld文件视为数据模型版本的软件包,每个版本均由.xcdatamodel文件表示。 您可以通过右键单击.xcdatamodeld文件并选择Show in Finder来验证这一点。 这将带您进入Xcode项目中的数据模型。 您应该看到数据模型的两个版本, Done.xcdatamodel和Done 2 .xcdatamodel 。
您是否在项目浏览器中注意到其中一个版本带有绿色对勾? 此复选标记指示当前模型版本,在此示例中为Done.xcdatamodel 。 换句话说,即使我们创建了数据模型的新版本,我们的应用程序仍未使用它。 但是,在更改此设置之前,我们需要告诉Core Data它应该如何处理版本化数据模型。
我们需要告诉Core Data如何迁移数据模型的持久性存储。 当我们将持久性存储添加到AppDelegate.swift中的持久性存储协调器时,我们将执行此操作。 在persistentStoreCoordinator
属性的实现中,我们创建了持久性存储协调器,并通过调用addPersistentStoreWithType(_:configuration:URL:options:)
添加了持久性存储。 现在应该已经熟悉了。
此方法的第四个参数是选项字典,当前为nil
。 该选项词典包含有关核心数据的说明。 它使我们有机会告诉持久性存储协调器应如何迁移我们要添加到持久性存储中的持久性存储。
看看下面的代码片段,其中我们传递了带有两个键值对的选项字典。
// Declare Options
let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
// Add Persistent Store to Persistent Store Coordinator
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
第一个键NSMigratePersistentStoresAutomaticallyOption
告诉Core Data我们希望它尝试为我们迁移持久性存储。 第二个键NSInferMappingModelAutomaticallyOption
,指示Core Data推断迁移的映射模型。 这正是我们想要的。 只要我们要处理轻量级迁移,它就应该可以正常工作。
通过此更改,我们准备将数据模型迁移到我们不久前创建的新版本。 首先选择新版本Done 2.xcdatamodel ,然后将Date类型的新属性updatedAt添加到Item实体。
我们还需要将新的数据模型版本标记为Core Data要使用的版本。 选择 在Project Navigator中 建模Done.xcdatamodel ,然后打开右侧的File Inspector 。 在“ 模型版本 ”部分中,将“ 当前 ”设置为“完成2” 。
在Project Navigator中 , Done 2.xcdatamodel现在应该带有绿色的选中标记,而不是Done.xcdatamodel 。
通过此更改,您可以安全地构建和运行该应用程序。 如果执行了上述步骤,Core Data应该通过基于版本化数据模型推断映射模型来自动为您迁移持久性存储。
请注意,您应该注意一些警告。 如果遇到崩溃,则说明您做错了什么。 例如,如果您已将数据模型版本设置为Done 2.xcdatamodel ,运行该应用程序,然后对Done 2.xcdatamodel进行更改,则由于永久存储与以下版本不兼容,您很可能会崩溃数据模型。 轻量级迁移功能相对强大且易于实现,但这并不意味着您可以随时修改数据模型。
软件项目的数据层需要注意,注意和准备。 迁移很棒,但应谨慎使用。 崩溃在开发过程中没有问题,但是在生产中是灾难性的。 在下一部分中,我们将仔细研究这意味着什么以及如何防止由于有问题的迁移而导致崩溃。
5.避免崩溃
我从来没有遇到过保证在生产中调用abort
的情况,当我浏览一个使用Apple的默认实现来设置Core Data堆栈的项目时,我感到很痛苦。在该项目中,添加持久性存储失败时调用abort
。
避免abort
并不是那么困难,但是它需要几行代码,并在发生问题时通知用户出了什么问题。 开发人员只是人,我们都会犯错,但这并不意味着您应该全力以赴,并称之为abort
。
步骤1:摆脱abort
首先打开AppDelegate.swift并删除我们称为abort
。 这是让满意的用户迈出的第一步。
do {
// Declare Options
let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
// Add Persistent Store to Persistent Store Coordinator
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
} catch {
// Populate Error
var userInfo = [String: AnyObject]()
userInfo[NSLocalizedDescriptionKey] = "There was an error creating or loading the application's saved data."
userInfo[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data."
userInfo[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "com.tutsplus.Done", code: 1001, userInfo: userInfo)
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
}
步骤2:移动不相容的商店
如果Core Data检测到持久性存储与数据模型不兼容,我们首先将不兼容的存储移动到安全位置。 我们这样做是为了确保用户的数据不会丢失。 即使数据模型与持久性存储不兼容,您也可以从中恢复数据。 看一下AppDelegate.swift中 persistentStoreCoordinator
属性的更新实现。
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
// Initialize Persistent Store Coordinator
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
// URL Persistent Store
let URLPersistentStore = self.applicationStoresDirectory().URLByAppendingPathComponent("Done.sqlite")
do {
// Declare Options
let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
// Add Persistent Store to Persistent Store Coordinator
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
} catch {
let fm = NSFileManager.defaultManager()
if fm.fileExistsAtPath(URLPersistentStore.path!) {
let nameIncompatibleStore = self.nameForIncompatibleStore()
let URLCorruptPersistentStore = self.applicationIncompatibleStoresDirectory().URLByAppendingPathComponent(nameIncompatibleStore)
do {
// Move Incompatible Store
try fm.moveItemAtURL(URLPersistentStore, toURL: URLCorruptPersistentStore)
} catch {
let moveError = error as NSError
print("\(moveError), \(moveError.userInfo)")
}
}
}
return persistentStoreCoordinator
}()
请注意,我已经更改了URLPersistentStore
的值(持久存储的位置)。 它指向应用程序沙箱中Documents目录中的目录。 如下所示,helper方法applicationStoresDirectory()
的实现非常简单。 它肯定比Objective-C更为冗长。 还要注意,我强制解开URL
常量path()
的结果,因为我们可以安全地假定在iOS和OS X上,应用程序的沙箱中都有一个应用程序支持目录。
private func applicationStoresDirectory() -> NSURL {
let fm = NSFileManager.defaultManager()
// Fetch Application Support Directory
let URLs = fm.URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)
let applicationSupportDirectory = URLs[(URLs.count - 1)]
// Create Application Stores Directory
let URL = applicationSupportDirectory.URLByAppendingPathComponent("Stores")
if !fm.fileExistsAtPath(URL.path!) {
do {
// Create Directory for Stores
try fm.createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil)
} catch {
let createError = error as NSError
print("\(createError), \(createError.userInfo)")
}
}
return URL
}
如果持久性存储协调器无法在URLPersistentStore
处添加现有的持久性存储, URLPersistentStore
持久性存储移动到单独的目录中。 为此,我们使用了另外两个辅助方法, applicationIncompatibleStoresDirectory()
和nameForIncompatibleStore()
。 applicationIncompatibleStoresDirectory()
的实现类似于applicationStoresDirectory()
。
private func applicationIncompatibleStoresDirectory() -> NSURL {
let fm = NSFileManager.defaultManager()
// Create Application Incompatible Stores Directory
let URL = applicationStoresDirectory().URLByAppendingPathComponent("Incompatible")
if !fm.fileExistsAtPath(URL.path!) {
do {
// Create Directory for Stores
try fm.createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil)
} catch {
let createError = error as NSError
print("\(createError), \(createError.userInfo)")
}
}
return URL
}
在nameForIncompatibleStore()
,我们根据当前日期和时间为不兼容商店生成一个名称,以避免命名冲突。
private func nameForIncompatibleStore() -> String {
// Initialize Date Formatter
let dateFormatter = NSDateFormatter()
// Configure Date Formatter
dateFormatter.formatterBehavior = .Behavior10_4
dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss"
return "\(dateFormatter.stringFromDate(NSDate())).sqlite"
}
步骤3:创建一个新的持久性存储
是时候创建一个新的持久性存储来完成Core Data堆栈的设置了。 到目前为止,接下来的几行应该看起来非常熟悉。
do {
// Declare Options
let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ]
// Add Persistent Store to Persistent Store Coordinator
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
} catch {
let storeError = error as NSError
print("\(storeError), \(storeError.userInfo)")
}
如果Core Data无法创建新的持久性存储,则存在更严重的问题,这些问题与数据模型与持久性存储不兼容无关。 如果确实遇到此问题,请再次检查URLPersistentStore
的值。
步骤4:通知使用者
就创建用户友好的应用程序而言,这一步骤可能是最重要的一步。 丢失用户的数据是一回事,但假装什么也没有发生是不好的。 如果一家航空公司假装好像什么也没发生一样,丢了行李,您会感觉如何。
let storeError = error as NSError
print("\(storeError), \(storeError.userInfo)")
// Update User Defaults
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setBool(true, forKey: "didDetectIncompatibleStore")
如果Core Data无法使用数据模型迁移持久性存储,则可以通过在应用程序的用户默认数据库中设置键值对来记住这一点。 我们在ViewController
类的viewDidLoad()
方法中查找此键值对。
// MARK: -
// MARK: View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
let userDefaults = NSUserDefaults.standardUserDefaults()
let didDetectIncompatibleStore = userDefaults.boolForKey("didDetectIncompatibleStore")
if didDetectIncompatibleStore {
// Show Alert
let applicationName = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleDisplayName")
let message = "A serious application error occurred while \(applicationName) tried to read your data. Please contact support for help."
self.showAlertWithTitle("Warning", message: message, cancelButtonTitle: "OK")
}
}
showAlertWithTitle(_:message:cancelButtonTitle:)
的实现类似于在AddToDoViewController
看到的AddToDoViewController
。 请注意,当用户点击警报按钮时,我们会删除键值对。
// MARK: -
// MARK: Helper Methods
private func showAlertWithTitle(title: String, message: String, cancelButtonTitle: String) {
// Initialize Alert Controller
let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
// Configure Alert Controller
alertController.addAction(UIAlertAction(title: cancelButtonTitle, style: .Default, handler: { (_) -> Void in
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.removeObjectForKey("didDetectIncompatibleStore")
}))
// Present Alert Controller
presentViewController(alertController, animated: true, completion: nil)
}
我们向用户显示警报,但是最好进一步采取措施。 例如,您可以敦促他们与支持人员联系,甚至可以实施一项功能,使他们将损坏的存储发送给您。 后者对于调试问题非常有用。
步骤5:测试
持久数据是大多数应用程序的重要方面。 因此,正确测试我们在本文中实现的内容很重要。 要测试我们的恢复策略,请在模拟器中运行该应用程序,然后仔细检查持久存储是否已在“ 应用程序支持”目录中“ 商店”子目录中成功创建。
在Done 2.xcdatamodel中向Item实体添加一个新属性,然后再次运行该应用程序。 由于现有的持久性存储现在与数据模型不兼容,因此不兼容的持久性存储将移至“ 不兼容”子目录,并创建一个新的持久性存储。 您还应该看到警报,通知用户有关问题。
结论
如果您打算广泛使用Core Data,则迁移是一个重要的主题。 迁移使您可以安全地修改应用程序的数据模型,并且在进行轻量级迁移的情况下,无需太多开销。
在下一篇文章中,我们着重于子类化NSManagedObject
。 如果核心数据项目具有任何复杂性,那么NSManagedObject
是NSManagedObject
的方法。
翻译自: https://code.tutsplus.com/tutorials/core-data-and-swift-migrations--cms-25084