使用 CloudKit 的 CoreData:模型迁移后,所有内容都被复制

Pro*_*rth 6 core-data core-data-migration ios swift cloudkit

我尝试迁移我的 CoreData-Model(使用 CloudKit),它复制了我存储的所有对象。在 CloudKit 中使用 CoreData 时如何正确迁移?

概括

我在 CloudKit 中使用 CoreData。几天前,我对模型进行了一些更改,因此需要进行迁移。事情是这样的(详情见下文):

  1. 我只是在我的模型 ( Model.xcdatamodel) 中进行了更改,没有更改模型的版本并将其安装在我的 iPhone 上进行测试 -> 崩溃并显示消息“无法迁移就地存储:尝试迁移期间违反约束”。

  2. 我创建了模型的新版本 ( Model 2.xcdatamodel) 并在那里进行了更改。然后我创建了一个.xcmappingmodel来管理迁移。没有崩溃并且它起作用了,但是......

  3. 我的应用程序中的所有条目现在都重复了,这当然不是预期的。

我在模型中改变了什么:

我的原始(源)模型有两个实体 A 和 B。A 和 B 之间存在多对多映射。我做了以下更改。

  • 添加两个新实体 C 和 D,具有一个数据字段(“名称”)
  • 在两个新实体 C、D 和我现有实体之一 (A) 之间创建一对多映射

我只是创建了.xcmappingmodel-file 而没有更改其中的任何内容。对于现有实体 A 和 B,它具有接管先前数据的条目,如下所示:

destination attribute: name
value expression: $source.name
Run Code Online (Sandbox Code Playgroud)

对于现有的映射 AB(实体 B 称为“标签”),它具有: FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:" , "TagToTag", $source.tags) 与逆关系类似。

我如何构建我的 CoreData 堆栈

我遵循了 Apple 的文档。我的代码看起来像这样(我做了一个CoreDataManager-class):

[...]
lazy var persistentContainer: NSPersistentContainer = {
    let container: NSPersistentContainer
    container = NSPersistentCloudKitContainer(name: containerName)
    let storeDescription = container.persistentStoreDescriptions.first
    storeDescription?.type = NSSQLiteStoreType

    container.loadPersistentStores { (_, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error when loading CoreData persistent stores: \(error), \(error.userInfo)")
        }
    }
    return container
}()

lazy var mainContext: NSManagedObjectContext = {
    let context = self.persistentContainer.viewContext
    context.automaticallyMergesChangesFromParent = true

    return context
}()
[...]
Run Code Online (Sandbox Code Playgroud)

我真的不知道我做错了什么,或者我该如何解决这个问题。如果有人能指出我正确的方向,我将不胜感激。

Cli*_*udo 1

我们有同样的问题,似乎每次我们使用映射模型进行重量级模型迁移时都会发生这种情况。经过大量研究,看来您/我们没有做错任何事情。

\n

Apple CloudKit 文档似乎表明数据复制是设计使然,以便访问核心数据的旧版本应用程序将能够使用旧数据。这可能是 CloudKit 中不允许使用唯一约束的原因(我们不确定)。请参阅此Apple CloudKit 文档。请注意标题为“更新生产架构”的部分以及向核心数据实体添加版本号的建议。

\n
\n
    \n
  • 向现有记录类型增量添加新字段。如果您采用这种方法,旧版本的应用程序可以访问用户创建的每条记录,但不能访问每个字段。
  • \n
\n
\n
\n
    \n
  • 通过从一开始就包含版本属性来对实体进行版本控制,并使用提取请求仅选择与应用程序当前版本兼容的记录。如果您采用这种方法,旧版本的应用程序将不会\xe2\x80\x99 获取用户使用较新版本创建的记录,从而有效地将它们隐藏在该设备上。
  • \n
\n
\n

因此,我们得出的结论是,唯一的解决方案是执行以下一项或两项操作:

\n
    \n
  1. 对我们的核心数据实体进行版本控制,并让较新版本的软件在获取请求期间过滤掉旧模型实体。
  2. \n
  3. 编写代码来手动删除旧记录,但这可能会给在由于操作系统不兼容而无法更新的设备上运行旧版本软件的用户带来问题。
  4. \n
\n

有一些解决方案可供使用,NSPersistentHistoryTrackingKey但这些解决方案似乎相当复杂,我们不清楚额外的复杂性与简单地使用我们模型实体中确实拥有的 UUID 编写一些简单的重复数据删除代码相比有什么优势。

\n

每次应用程序启动并初始化核心数据时都可以简单地调用该代码。我们每次都会这样做,因为我们的软件在 MacOS 和 iOS 设备上运行,并在所有版本之间共享数据,并且无法知道其他设备何时会升级到较新的数据模型并复制数据。去重的代码很简单:

\n
func deDuplicateAfterMigrationFrom14to15()\n{\n    print("**** CoreDataUtil DeDupe UUIDs without new attribute or too many with new")\n    let moc = pc.viewContext\n    let chartsFetch = NSFetchRequest<NSFetchRequestResult>(entityName:"Charts")     // Fetch all charts\n    do {\n        let fetchedCharts = try moc.fetch(chartsFetch) as! [Chart]\n        for chart in fetchedCharts\n        {\n            // Find and Remove duplicate UUID\n            chartsFetch.predicate = NSPredicate(format:"uuid == %@", chart.uuid)\n            do {\n                let fetchedChartsWithUUID = try moc.fetch(chartsFetch) as! [Chart]\n                if(fetchedChartsWithUUID.count > 1) {\n                    for(index, chartWithUUID) in fetchedChartsWithUUID.enumerated() {\n                        // Find old Entity without new attribute\n                        let nameFirstChar = chartWithUUID.nameFirstChar ?? ""\n                        if(nameFirstChar.isEmpty) {\n                            print("***** DeDupe OLD Chart UUID-> " + chartWithUUID.uuid + " NAME[\\(index)]-> " + chartWithUUID.name)\n                            self.pc.viewContext.delete(chartWithUUID)\n                        }\n                        // Find extra copies of updated Entity created by other devices.\n                        else if(!nameFirstChar.isEmpty && index > 1) {\n                            print("***** DeDupe NEW Extra Chart UUID-> " + chartWithUUID.uuid + " NAME[\\(index)]-> " + chartWithUUID.name)\n                            self.pc.viewContext.delete(chartWithUUID)\n                        }\n                    }\n                    try moc.save()\n                }\n            }\n            catch {\n                print("****** De-Dupe UUID Failed for UUID?: \\(error)")\n            }\n        }\n        try moc.save()\n    }\n    catch {\n        print("****** De-Dupe initial Fetch failed: \\(error)")\n    }\n    print("**** CoreDataUtil De-Dupe DONE")\n}\n
Run Code Online (Sandbox Code Playgroud)\n