使用NSPersistentCloudKitContainer时预填充核心数据存储的最佳方法是什么? [英] What's the best approach to prefill Core Data store when using NSPersistentCloudKitContainer?

查看:53
本文介绍了使用NSPersistentCloudKitContainer时预填充核心数据存储的最佳方法是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在以下情况下,我要从JSON文件解析对象并将其存储到我的Core Data存储中.现在,我正在使用 NSPersistentCloudKitContainer ,并且当我在其他设备上运行该应用程序时,它还会解析JSON文件并将对象添加到Core Data.这样会导致对象重复.

I'm having the following scenario where I'm parsing objects from a JSON file and store them into my Core Data store. Now I'm using NSPersistentCloudKitContainer and when I'm running the app on a different device, it also parses the JSON file and adds objects to Core Data. That results in duplicate objects.

现在我想知道是否有:

  • 如果我可以检查一个实体是否已经存在一个简单的方法?
  • 还有其他避免对象在CloudKit中保存两次的方法吗?
  • 从远程获取数据完成后会收到通知吗?

推荐答案

也许回答为时已晚,但我最近正在处理同一问题.经过数周的研究,我想把我所学到的东西留在这里,希望能帮助遇到同样问题的人.

Maybe it's too late to answer but I am working on the same issue recently. After weeks of research and I would like to leave here what I've learned, hope to help someone having the same problem.

如果我可以检查一个实体是否已经存在一个简单的方法?

An easy way if I can check that an entity already exists remotely?

还有其他避免对象在CloudKit中保存两次的方法吗?

Any other way to avoid objects being saved twice in CloudKit?

是的,我们可以检查实体是否已存在于iCloud上,但这不是决定是否解析JSON文件并将其保存到CoreDatapersistentStore的最佳方法.该应用可能未连接到Apple ID/iCloud,或者存在某些网络问题,因此无法可靠地检查该实体是否存在.

Yes, we can check if the entity already exists on iCloud, but that's not the best way to decide whether to parse the JSON file and save it to CoreData persistentStore or not. Chances are the app is not connected to an Apple ID / iCloud, or having some network issue that makes it not reliable to check if that entity exists remotely or not.

当前的解决方案是通过将一个UUID字段添加到从JSON文件添加的每个数据对象中来对数据进行重复数据删除,然后删除具有相同UUID的对象.大多数时候,我还会添加一个lastUpdate字段,以便我们可以保留最新的数据对象.

The current solution is to deduplicate the data ourselves, by adding a UUID field to every data object added from the JSON file, and remove the object with the same UUID. Most of the time I would also add a lastUpdate field, so we can keep the most latest data object.

从远程获取数据完成后会收到通知吗?

Getting notified when fetching data from remote has finished?

我们可以添加一个NSPersistentStoreRemoteChange的观察者,并在远程存储发生更改时获取通知.

We can add an observer of NSPersistentStoreRemoteChange, and get notifications whenever the remote store changes.

Apple提供了一个有关将CoreData与CloudKit一起使用的演示项目,并很好地解释了重复数据删除.

Apple provided a demo project on using CoreData with CloudKit, and explain the deduplication quite well.

将本地存储同步到云 https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud

WWDC2019会议202:将CoreData与CloudKit一起使用 https://developer.apple.com/videos/play/wwdc2019/202

整个想法是监听远程存储中的更改,跟踪更改历史记录,并在有新数据传入时对我们的数据进行重复数据删除.(当然,我们需要一些字段来确定数据是否重复).持久性存储提供了历史跟踪功能,当它们合并到本地存储中时,我们可以获取这些事务,并运行重复数据删除过程.假设我们将在应用启动时解析JSON并导入标签:

The whole idea is to listen to changes in remote store, keep track of the changes history, and deduplicate our data when there is any new data coming in. (And of course we need some field to determine whether the data is duplicated or not). The persistent store provides a history tracking feature, and we can fetch those transactions when they are merging to the local store, and run our deduplication process. Let's say we will parse JSON and import Tags when app launched:

// Use a custom queue to ensure only one process of history handling at the same time
private lazy var historyQueue: OperationQueue = {
    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = 1
    return queue
}()

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitDemo")

    ...
    // set the persistentStoreDescription to track history and generate notificaiton (NSPersistentHistoryTrackingKey, NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    // load the persistentStores
    // set the mergePolicy of the viewContext
    ...

    // Observe Core Data remote change notifications.
    NotificationCenter.default.addObserver(
        self, selector: #selector(type(of: self).storeRemoteChange(_:)),
        name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)

    return container
}()

@objc func storeRemoteChange(_ notification: Notification) {
    // Process persistent history to merge changes from other coordinators.
    historyQueue.addOperation {
        self.processPersistentHistory()
    }
}

// To fetch change since last update, deduplicate if any new insert data, and save the updated token
private func processPersistentHistory() {
    // run in a background context and not blocking the view context.
    // when background context is saved, it will merge to the view context based on the merge policy
    let taskContext = persistentContainer.newBackgroundContext()
    taskContext.performAndWait {
        // Fetch history received from outside the app since the last token
        let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
        let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
        request.fetchRequest = historyFetchRequest

        let result = (try? taskContext.execute(request)) as? NSPersistentHistoryResult
        guard let transactions = result?.result as? [NSPersistentHistoryTransaction],
              !transactions.isEmpty
            else { return }

        // Tags from remote store
        var newTagObjectIDs = [NSManagedObjectID]()
        let tagEntityName = Tag.entity().name

        // Append those .insert change in the trasactions that we want to deduplicate
        for transaction in transactions where transaction.changes != nil {
            for change in transaction.changes!
                where change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {
                    newTagObjectIDs.append(change.changedObjectID)
            }
        }

        if !newTagObjectIDs.isEmpty {
            deduplicateAndWait(tagObjectIDs: newTagObjectIDs)
        }
        
        // Update the history token using the last transaction.
        lastHistoryToken = transactions.last!.token
    }
}

在这里,我们保存添加的标签的ObjectID,以便我们可以在其他任何对象上下文中对它们进行重复数据删除,

Here we save the ObjectID of the added Tags so we can deduplicate them on any other object context,

private func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID]) {
    let taskContext = persistentContainer.backgroundContext()
    
    // Use performAndWait because each step relies on the sequence. Since historyQueue runs in the background, waiting won’t block the main queue.
    taskContext.performAndWait {
        tagObjectIDs.forEach { tagObjectID in
            self.deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
        }
        // Save the background context to trigger a notification and merge the result into the viewContext.
        taskContext.save(with: .deduplicate)
    }
}

private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
    // Get tag by the objectID
    guard let tag = performingContext.object(with: tagObjectID) as? Tag,
        let tagUUID = tag.uuid else {
        fatalError("###\(#function): Failed to retrieve a valid tag with ID: \(tagObjectID)")
    }

    // Fetch all tags with the same uuid
    let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
    // Sort by lastUpdate, keep the latest Tag
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastUpdate", ascending: false)]

    fetchRequest.predicate = NSPredicate(format: "uuid == %@", tagUUID)
    
    // Return if there are no duplicates.
    guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
        return
    }
    // Pick the first tag as the winner.
    guard let winner = duplicatedTags.first else {
        fatalError("###\(#function): Failed to retrieve the first duplicated tag")
    }
    duplicatedTags.removeFirst()
    remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
}

(我认为)最困难的部分是处理被删除的重复对象的那些关系,可以说我们的Tag对象与Category对象具有一对多关系(每个Tag可能具有多个Category)

And the most difficult part (in my opinion) is to handle those relationship of the duplicated object that got deleted, lets say our Tag object have a one-to-many relationship with a Category object (each Tag may have multiple Category)

private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
    duplicatedTags.forEach { tag in
        // delete the tag AFTER we handle the relationship
        // and be careful that the delete rule will also activate
        defer { performingContext.delete(tag) }
        
        if let categorys = tag.categorys as? Set<Category> {
            for category in categorys {
                // re-map those category to the winner Tag, or it will become nil when the duplicated Tag got delete
                category.ofTag = winner
            }
        }
    }
}

有趣的是,如果同时从远程存储中添加了Category对象,那么当我们处理这种关系时它们可能还不存在,但这是另一回事了.

One interesting thing is, if the Category objects are also added from the remote store, they may not yet exist when we handle the relationship, but that's another story.

这篇关于使用NSPersistentCloudKitContainer时预填充核心数据存储的最佳方法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆