返回

修复Core Data迁移崩溃:`table already exists`错误全解

IOS

解决 Core Data 迁移崩溃:“table ZEKYCINFO already exists” 错误分析与处理

碰到了个头疼的问题:老版本的 iOS App (1.4.3) 用户升级到新版 (1.5.0) 后,App 一启动就崩。瞅了眼崩溃日志,罪魁祸首直指 Core Data 迁移。

具体来说,旧版 App 用的是数据模型版本 2(.xcdatamodeld v2),新版切换到了版本 3(v3)。我在 AppDelegate 里设置了 NSPersistentContainer,让它自动处理迁移:

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "MSN_App")
    let fileName = "MSN_App.sql" // 简化了原始代码中的文件名拼接

    // 尝试获取 Application Support 目录 URL
    guard let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last?.appendingPathComponent(fileName) else {
         // 如果无法获取 URL,记录错误并提前返回一个空的或默认配置的容器可能更安全,而不是直接 fatalError
         Log.b("COREDATA: Failed to construct URL for persistent store.")
         // 这里可以根据你的应用逻辑返回一个无法使用的容器或触发更安全的错误处理
         fatalError("COREDATA: Could not create URL for persistent store.") // 或者采取其他措施
    }

    // 检查文件是否存在(虽然检查存在性不影响迁移逻辑,但保留了原始日志)
    if FileManager.default.fileExists(atPath: url.path) {
        Log.b("COREDATA: Sqlite File ALREADY present at \(url.path)")
    } else {
        Log.b("COREDATA: Sqlite File NOT present yet at \(url.path)")
    }

    let description = NSPersistentStoreDescription(url: url)
    // 开启自动轻量级迁移 和 自动推断映射模型
    description.shouldMigrateStoreAutomatically = true
    description.shouldInferMappingModelAutomatically = true
    container.persistentStoreDescriptions = [description]

    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            // 注意:线上版本应避免 fatalError,应记录错误并尝试恢复或通知用户
            Log.e("COREDATA: Unresolved error loading persistent store: \(error), \(error.userInfo)")
            // 对于迁移错误,可以尝试更精细的处理
            if error.domain == NSCocoaErrorDomain && (error.code == NSPersistentStoreIncompatibleVersionHashError || error.code == NSMigrationError || error.code == NSMigrationConstraintViolationError || error.code == 134110 /* 从日志中推断 */) {
                 Log.e("COREDATA: Migration failed. Error details: \(error.userInfo)")
                 // 这里可以添加迁移失败的处理逻辑,比如删除旧数据、提示用户等
            }
            // 原始代码中的 Fatal Error,调试时可用,生产环境应替换
            fatalError("Unresolved error \(error), \(error.userInfo)")
        } else {
            Log.b("COREDATA: Persistent store loaded successfully at \(storeDescription.url?.path ?? "unknown URL")")
        }
    })
    return container
}()

结果,用户从 v1.4.3 (模型 v2) 升级到 v1.5.0 (模型 v3) 时,App 就炸了,日志里躺着这么一条信息:

MSN_App_Release/AppDelegate.swift:518: Fatal error: Unresolved error Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={sourceURL=..., reason=Cannot migrate store in-place: I/O error for database at .... SQLite error code:1, 'table ZEKYCINFO already exists', destinationURL=..., NSUnderlyingError=...{Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={NSSQLiteErrorDomain=1, NSFilePath=..., NSUnderlyingException=constraint failed}}

核心错误信息是 SQLite error code:1, 'table ZEKYCINFO already exists'

为啥会崩?原因分析

明明设置了 shouldMigrateStoreAutomatically = trueshouldInferMappingModelAutomatically = true,按理说 Core Data 应该能自动处理简单的模型变更(轻量级迁移)。那为啥还会报“表已存在”的错呢?

问题出在 Core Data 尝试进行的“自动迁移”上。

  1. 轻量级迁移的局限: shouldMigrateStoreAutomaticallyshouldInferMappingModelAutomatically 主要适用于“轻量级迁移”。这类迁移能处理的变更很有限,比如:

    • 添加、移除实体(Entity)。
    • 添加、移除属性(Attribute)。
    • 将可选属性(Optional)变为非可选(Non-Optional),前提是提供了默认值。
    • 将非可选属性变可选。
    • 重命名实体或属性(需要在 Xcode 的 Data Model Inspector 中设置 Renaming ID)。
  2. "table ZEKYCINFO already exists" 的异常信号: 这个 SQLite 底层错误非常具体。Core Data 在管理 SQLite 数据库时,通常会给实体名加上 Z 前缀(例如实体 EKYCINFO 对应表 ZEKYCINFO)。在迁移过程中,它尝试创建一个名为 ZEKYCINFO 的表,但发现这个表在目标数据库里(也就是迁移过程中的临时或最终数据库)已经存在了。

    这通常意味着以下几种情况之一:

    • 变更超出了轻量级迁移的范围: 你在 v2到v3 之间做的模型修改,可能包含了 Core Data 无法自动推断如何处理的复杂变更。比如,改变了属性类型但需要数据转换、改变了关系类型(一对一变一对多等)、或者更复杂的实体结构调整。虽然只是猜测,但很可能与 EKYCINFO 这个实体相关的变更不被轻量级迁移支持。
    • 推断映射模型(Inferred Mapping Model)出错: 即使变更理论上兼容轻量级迁移,Core Data 自动推断出的映射步骤也可能出错。它可能错误地判断需要创建一个新表,而不是修改现有表。这种情况比较少见,但可能发生在某些边缘的模型结构或变更组合下。
    • 不一致的重命名标识(Renaming ID): 如果你重命名了实体 EKYCINFO 或其属性,但没有正确设置 Renaming ID,或者跨版本修改了 Renaming ID,Core Data 可能无法识别出这是同一个对象的演变,从而尝试创建一个新表。
    • 迁移中断或状态不一致? 极少数情况下,如果之前的迁移尝试被异常中断,可能导致数据库处于一个中间状态,包含了一些本不该存在的结构。

从错误信息看,最可能的原因是模型 v2 到 v3 的变更不完全兼容轻量级迁移 ,或者自动推断的映射逻辑对 EKYCINFO 实体的处理有误 ,导致它试图重复创建表。

怎么办?解决方案来了

既然自动挡失灵了,咱们就得考虑手动介入或者换个策略。下面是几种解决这个问题的思路:

方案一:仔细检查模型变更,确认是否兼容轻量级迁移

这是最先要做的事。你需要精确对比 .xcdatamodeld 的 v2 和 v3 版本,看看 EKYCINFO 实体以及与之相关的实体、属性、关系到底改了些啥。

  • 原理与作用:
    搞清楚具体的模型变更,判断它们是否真的满足轻量级迁移的条件。如果发现存在复杂变更,那就证实了自动迁移失败的原因,需要采用更复杂的迁移策略。

  • 操作步骤:

    1. 在 Xcode 中,并排打开你的 .xcdatamodeld 文件的 v2 和 v3 版本。
    2. 逐个检查实体(Entities),特别是 EKYCINFO
    3. 对比 v2 和 v3 中 EKYCINFO 的属性(Attributes):有没有新增、删除、重命名?类型变了吗?可选性(Optional)变了吗?默认值变了吗?
    4. 对比 EKYCINFO 的关系(Relationships):有没有新增、删除、重命名?目标实体变了吗?基数(To-One, To-Many)变了吗?删除规则(Delete Rule)变了吗?
    5. 检查是否使用了重命名标识符(Renaming ID):如果在 v3 中重命名了实体或属性,确保在 Xcode 的 Data Model Inspector 的 Versioning 部分正确设置了 Renaming ID,并且这个 ID 与 v2 中该元素的名字或之前的 Renaming ID 对应。
    6. 特别留意:
      • 属性类型变更(如 String -> DataInt16 -> Int64)通常 支持轻量级迁移,除非是某些特定的安全转换(如 Int16 -> Int32)。
      • 关系基数的重大改变(如 To-One -> To-Many 支持轻量级迁移。
      • 添加或修改复杂的数据验证规则可能也需要手动处理。
  • 安全建议:
    在本地开发环境中,先备份好你的 .sqlite 文件,再进行各种迁移测试。可以用模拟器或测试设备上的旧版本 App 生成一份数据,然后升级到新版代码,复现迁移过程。

方案二:创建显式映射模型 (XCMappingModel)

如果确认变更复杂,或者怀疑自动推断出错,最标准、最可靠的办法就是创建一个显式映射模型(Mapping Model)

  • 原理与作用:
    映射模型是一个 .xcmappingmodel 文件,它精确定义了数据如何从源模型版本(v2)迁移到目标模型版本(v3)。你可以在这个文件里指定每个实体、每个属性、每个关系如何映射。Core Data 检测到这个文件后,会优先使用它来进行迁移,而不是依赖自动推断。这让你对迁移过程有完全的控制权。

  • 操作步骤:

    1. 在 Xcode 中,选中你的项目导航器。
    2. 右键点击 -> "New File..."。
    3. 选择 "iOS" -> "Core Data" -> "Mapping Model"。
    4. 点击 "Next"。
    5. Xcode 会提示你选择 "Source Data Model"(选择你的 .xcdatamodeldv2 版本)和 "Target Data Model"(选择 .xcdatamodeldv3 版本)。
    6. 点击 "Next",然后 "Create"。Xcode 会生成一个 .xcmappingmodel 文件。
    7. 打开这个新创建的映射模型文件。你会看到一个编辑器,左侧是源实体,右侧是目标实体。Xcode 会尝试自动填充一些映射,你需要仔细检查和调整。
    8. 重点关注 EKYCINFO 实体映射:
      • 确保源 EKYCINFO (v2) 正确映射到了目标 EKYCINFO (v3)。
      • 检查 EKYCINFO属性映射 (Attribute Mappings) 。对于每个属性,默认可能是直接复制 ($source.attributeName)。如果类型变了或需要转换,你可以在 "Value Expression" 里编写转换逻辑(比如,FUNCTION($source.oldAttribute, "transformMyData:"))。对于 "table already exists" 问题,要确保这里的映射逻辑是更新 现有表结构,而不是隐含地创建新表。通常,正确的实体和属性映射就能避免这个问题。
      • 检查关系映射 (Relationship Mappings)
    9. 保存映射模型文件。
  • 代码示例:
    好消息是,如果你把 .xcmappingmodel 文件放在项目里,并且 shouldMigrateStoreAutomatically 仍然是 true,Core Data 通常会自动找到并使用这个映射模型。你的 AppDelegate 代码可能不需要修改

    // AppDelegate 中的代码通常保持不变
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "MSN_App")
        // ... 其他设置保持不变 ...
        let description = NSPersistentStoreDescription(url: url)
        description.shouldMigrateStoreAutomatically = true // 保持 true
        description.shouldInferMappingModelAutomatically = true // 这个设为 true 或 false 影响不大,因为显式模型优先
        container.persistentStoreDescriptions = [description]
        // ... loadPersistentStores ...
        return container
    }()
    

    Core Data 会按以下顺序查找迁移方法:

    1. 寻找 v2 -> v3 的显式映射模型 (.xcmappingmodel)。如果找到,就用它。
    2. 如果没找到显式模型,且 shouldInferMappingModelAutomaticallytrue,尝试推断轻量级迁移。
    3. 如果推断也失败,或者不被允许,迁移失败。
  • 安全建议:
    用包含各种边界情况(空值、特殊值、大量数据)的测试数据,彻底测试使用映射模型的迁移过程。确保所有数据都按预期迁移到了新模型。

  • 进阶技巧:
    在映射模型的 "Value Expression" 中,你可以使用一些内置函数或自定义函数(通过 NSEntityMigrationPolicy,见方案四)来进行更复杂的数据转换。比如,合并两个旧属性到一个新属性,或者根据旧属性的值计算新属性的值。

方案三:分步迁移 (Progressive Migration)

如果你的 App 经历过多个模型版本(比如 v1 -> v2 -> v3),并且用户可能跳过中间版本直接升级(比如从 v1 直接到 v3),或者某个版本间的迁移特别复杂,你可能需要实现分步迁移

  • 原理与作用:
    不是一次性从 v2 迁移到 v3,而是像爬楼梯一样,一步步来。比如,先从 v2 迁移到 v2.5 (如果存在),再从 v2.5 迁移到 v3。这需要你有所有中间的模型版本和对应的映射模型(或者确保每一步都兼容轻量级迁移)。

  • 操作步骤:
    这通常需要自定义迁移逻辑,不能完全依赖 shouldMigrateStoreAutomatically。你需要:

    1. 检测当前数据库文件的模型版本。
    2. 获取 App 支持的最新模型版本。
    3. 循环执行:
      a. 找到能处理当前版本到下一个兼容版本(可能是轻量级迁移或有映射模型)的迁移路径。
      b. 手动执行这一步迁移(使用 NSMigrationManager)。
      c. 重复此过程,直到数据库模型更新到最新版本。
  • 代码示例(概念性):
    这部分逻辑通常放在 loadPersistentStores 之前或其错误处理回调中。你需要关闭自动迁移,并手动管理。

    import CoreData
    
    func migrateStoreIfNeeded(storeURL: URL, targetModel: NSManagedObjectModel) {
        // 1. 获取当前存储的模型版本信息
        guard let sourceMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: storeURL, options: nil),
              let sourceModel = NSManagedObjectModel.mergedModel(from: [Bundle.main], forStoreMetadata: sourceMetadata) else {
            Log.e("COREDATA: Failed to get metadata or source model for migration.")
            return
        }
    
        // 2. 如果当前模型不是目标模型,开始逐步迁移
        var currentSourceModel = sourceModel
        while !currentSourceModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: sourceMetadata) {
             // 查找兼容当前 metadata 的下一个模型版本
             guard let nextModelVersion = findNextModelVersion(after: currentSourceModel),
                   let mappingModel = findMappingModel(from: currentSourceModel, to: nextModelVersion) else {
                 Log.e("COREDATA: Could not find next model version or mapping model.")
                 // 处理无法找到迁移路径的错误
                 handleMigrationError("Cannot find migration path")
                 return
             }
    
             // 3. 执行单步迁移
             let migrationManager = NSMigrationManager(sourceModel: currentSourceModel, destinationModel: nextModelVersion)
             let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) // 临时目标 URL
    
             do {
                 try migrationManager.migrateStore(from: storeURL,
                                                   sourceType: NSSQLiteStoreType,
                                                   options: nil,
                                                   with: mappingModel,
                                                   toDestinationURL: tempURL,
                                                   destinationType: NSSQLiteStoreType,
                                                   destinationOptions: nil)
    
                 // 迁移成功,用新文件替换旧文件
                 let backupURL = storeURL.appendingPathExtension("backup") // 备份旧文件
                 try FileManager.default.moveItem(at: storeURL, to: backupURL)
                 try FileManager.default.moveItem(at: tempURL, to: storeURL)
                 try FileManager.default.removeItem(at: backupURL) // 删除备份
    
                 // 更新当前 metadata 和 model,为下一步迁移做准备
                 guard let newMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: storeURL, options: nil) else { /*...*/ }
                 sourceMetadata = newMetadata // 这行可能有误,metadata是字典,应重新读取
                 currentSourceModel = nextModelVersion
                 Log.b("COREDATA: Successfully migrated step to version associated with model: \(nextModelVersion.versionIdentifiers)")
    
    
             } catch {
                 Log.e("COREDATA: Migration step failed: \(error)")
                 // 删除临时的目标文件
                 try? FileManager.default.removeItem(at: tempURL)
                 // 处理迁移失败(可能需要回滚或删除)
                 handleMigrationError("Step migration failed: \(error.localizedDescription)")
                 return
             }
        }
        Log.b("COREDATA: Store is up to date with target model.")
    }
    
    // --- 需要实现的辅助函数 ---
    func findNextModelVersion(after sourceModel: NSManagedObjectModel) -> NSManagedObjectModel? {
        // 实现查找模型版本顺序的逻辑,返回紧跟 sourceModel 的下一个版本
        // ...
    }
    
    func findMappingModel(from sourceModel: NSManagedObjectModel, to destinationModel: NSManagedObjectModel) -> NSMappingModel? {
        // 尝试查找显式映射模型,如果找不到,尝试推断轻量级迁移映射
        var mapping: NSMappingModel?
        if let explicitMapping = NSMappingModel(from: [Bundle.main], forSourceModel: sourceModel, destinationModel: destinationModel) {
            mapping = explicitMapping
        } else {
            // 尝试推断 (仅当这步兼容轻量级迁移)
             do {
                 mapping = try NSMappingModel.inferredMappingModel(forSourceModel: sourceModel, destinationModel: destinationModel)
             } catch {
                 Log.e("COREDATA: Cannot infer mapping model from \(sourceModel.versionIdentifiers) to \(destinationModel.versionIdentifiers): \(error)")
             }
        }
        return mapping
    }
    
    func handleMigrationError(_ message: String) {
         // 实现错误处理逻辑,比如删除数据、报告错误等
         Log.e("COREDATA: MIGRATION ERROR - \(message)")
         // 可能需要删除损坏的数据库: try? FileManager.default.removeItem(at: storeURL)
    }
    
    // --- 在加载持久化存储前调用 ---
    // let storeURL = ... a你的数据库文件URL ...
    // let container = NSPersistentContainer(name: "MSN_App")
    // let latestModel = container.managedObjectModel // 获取最新模型
    // migrateStoreIfNeeded(storeURL: storeURL, targetModel: latestModel)
    //
    // // 配置 description 时关闭自动迁移
    // let description = NSPersistentStoreDescription(url: storeURL)
    // description.shouldMigrateStoreAutomatically = false // 关闭自动迁移
    // description.shouldInferMappingModelAutomatically = false
    // container.persistentStoreDescriptions = [description]
    // container.loadPersistentStores { ... }
    
    
  • 安全建议:
    分步迁移逻辑复杂,出错几率高。务必编写健壮的错误处理和回滚逻辑(如果需要)。充分测试所有可能的升级路径(v1->v3, v2->v3 等)。

方案四:自定义迁移策略 (NSEntityMigrationPolicy)

如果映射模型中的简单值表达式不足以处理复杂的转换逻辑(例如,需要基于多个源属性、执行网络请求或复杂计算来生成目标数据),你可以使用自定义实体迁移策略

  • 原理与作用:
    创建一个 NSEntityMigrationPolicy 的子类,并在其中实现自定义的数据转换代码。然后,在你的 .xcmappingmodel 文件中,将这个自定义策略类关联到需要特殊处理的实体映射上。

  • 操作步骤:

    1. 创建一个新的 Swift 文件,继承自 NSEntityMigrationPolicy
    2. 覆盖必要的方法,最常用的是 createDestinationInstances(forSource:in:manager:)。在这个方法里,你可以访问源实例 (sInstance)、映射管理器 (manager),并负责创建和填充目标实例。
    3. .xcmappingmodel 文件中,选中 EKYCINFO(或其他需要自定义逻辑的)实体映射。
    4. 在 Xcode 的 Data Model Inspector 中,找到 "Entity Mapping" 部分。
    5. 在 "Custom Policy" 字段中,输入你的自定义策略类的名字(例如 YourApp.EKYCINFOmigrationPolicy)。
  • 代码示例:

    import CoreData
    
    class EKYCINFOmigrationPolicy: NSEntityMigrationPolicy {
    
        // 这个方法负责创建目标实例
        override func createDestinationInstances(forSource sInstance: NSManagedObject,
                                               in mapping: NSEntityMapping,
                                               manager: NSMigrationManager) throws {
    
            // 1. 获取目标实体的
            guard let destinationEntityName = mapping.destinationEntityName,
                  let destinationEntity = NSEntityDescription.entity(forEntityName: destinationEntityName, in: manager.destinationContext) else {
                throw NSError(domain: "MigrationError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not find destination entity"])
            }
    
            // 2. 创建目标实例
            let destInstance = NSManagedObject(entity: destinationEntity, insertInto: manager.destinationContext)
    
            // 3. 遍历映射的属性,执行自定义转换逻辑
            for propertyMapping in mapping.attributeMappings ?? [] {
                guard let propName = propertyMapping.name else { continue }
    
                var value: Any?
    
                if let valueExpression = propertyMapping.valueExpression {
                    // 对于复杂的自定义逻辑,你可能在这里判断属性名,并手动处理
                    if propName == "someComplexNewAttribute" {
                        // 示例:根据源实例的多个属性计算新值
                        let sourceVal1 = sInstance.value(forKey: "oldAttribute1") as? String ?? ""
                        let sourceVal2 = sInstance.value(forKey: "oldAttribute2") as? Int ?? 0
                        value = "\(sourceVal1)-\(sourceVal2)" // 你的复杂逻辑
                    } else {
                        // 对于其他属性,可能还使用默认的 value expression
                        value = valueExpression.expressionValue(with: sInstance, context: nil) // 注意: context 可能是 nil
                    }
                } else {
                    // 如果没有 value expression,可能需要从源实例直接获取?根据你的需要
                    // value = sInstance.value(forKey: propName) // 这可能不对,应是源属性名
                }
    
                if value != nil {
                    destInstance.setValue(value, forKey: propName)
                }
            }
    
            // 4. 关联新创建的目标实例与源实例(重要!)
            manager.associate(sourceInstance: sInstance, withDestinationInstance: destInstance, for: mapping)
        }
    
        // 你可能还需要覆盖其他方法,如处理关系映射的方法等,具体看需求
    }
    
  • 安全建议:
    这是最高级的迁移技术,代码复杂度高。一定要在隔离的环境中详细测试所有自定义逻辑,确保数据准确无误且性能可接受。

方案五:改进错误处理与用户体验

无论采用哪种迁移方案,生产环境的 App 绝对不能 在迁移失败时直接 fatalError

  • 原理与作用:
    优雅地处理迁移失败,避免崩溃。可以尝试自动恢复(如下策:删除旧数据),或者至少给用户明确的提示和可能的解决方案。

  • 操作步骤:
    修改 loadPersistentStores 的完成回调,捕获特定于迁移的错误代码。

  • 代码示例:

    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            Log.e("COREDATA: Unresolved error loading persistent store: \(error), \(error.userInfo)")
    
            // 检查是否是迁移相关的错误
            let migrationErrors = [
                NSMigratePersistentStoreError, // 通用迁移错误
                NSPersistentStoreIncompatibleVersionHashError, // 模型版本不兼容
                NSMigrationError, // 映射或策略执行错误 (这个可能是你遇到的134110的更通用版本)
                NSMigrationConstraintViolationError, // 迁移后数据违反约束
                NSMigrationCancelledError,
                NSMigrationMissingSourceModelError,
                NSMigrationMissingMappingModelError
                // 也可以直接检查 domain 和 code
                // error.domain == NSCocoaErrorDomain && error.code == 134110
            ]
    
            if migrationErrors.contains(error.code) || (error.domain == NSCocoaErrorDomain && error.code == 134110) {
                Log.e("COREDATA: MIGRATION FAILED! Error: \(error)")
    
                // 在这里实现恢复逻辑
                // 选项 1: (危险!)尝试删除旧的数据库文件,让 App 重新开始
                 let storeURL = storeDescription.url! // 确保 URL 存在
                 Log.w("COREDATA: Attempting to recover by deleting the store at \(storeURL.path)")
                 do {
                     try container.persistentStoreCoordinator.destroyPersistentStore(at: storeURL, ofType: NSSQLiteStoreType, options: nil)
                     // 可以尝试再次加载 (或者提示用户重启App)
                     // container.loadPersistentStores { ... } // 注意避免无限循环!
                     Log.i("COREDATA: Old store destroyed. App might restart or need data sync.")
                     // 这里应该通知用户数据已被重置,可能需要他们重新登录或同步
    
                 } catch let deletionError as NSError {
                     Log.e("COREDATA: Failed to delete persistent store for recovery: \(deletionError), \(deletionError.userInfo)")
                     // 删除都失败了,只能提示用户卸载重装或联系客服
                      showAlertToUserAboutDataLossAndReinstall()
                 }
    
                // 选项 2: 提示用户
                // showAlertToUserAboutMigrationFailure()
    
                // 选项 3: 将错误上传到你的崩溃报告系统或日志服务器
    
            } else {
                // 非迁移错误,但也应该处理而不是崩溃
                Log.e("COREDATA: Non-migration error loading persistent store: \(error)")
                 showAlertToUserAboutGenericDataError()
            }
    
            // 无论如何,避免 fatalError in production
            // fatalError("Unresolved error \(error), \(error.userInfo)")
        } else {
            Log.b("COREDATA: Persistent store loaded successfully.")
        }
    })
    
  • 安全建议:
    删除用户数据是非常敏感的操作。如果选择了这种恢复方式,务必:

    • 只在万不得已时执行。
    • 清晰地告知用户发生了什么以及数据丢失的后果。
    • 如果 App 依赖云同步,确保删除本地数据后能正确触发重新同步。
    • 考虑在删除前提供备份选项(如果技术上可行)。

解决 "table ZEKYCINFO already exists" 的关键通常在于识别出不兼容轻量级迁移的变更,并提供一个正确的显式映射模型 (.xcmappingmodel)。同时,健壮的错误处理是保证用户体验的基础。