CloudKit + 核心数据:如何仅写入或读取 public 数据库

CloudKit + Core Data: How to write or read the public database only

我在 Public 数据库配置模式和私有数据库配置模式中都有一个记录类型。

当我使用 PersistentStore.shared.context 写入记录类型时,它会将记录写入私有数据库和 public 数据库。当我使用@FetchRequest 查询记录类型时,它 returns 来自 public 和私有数据库的记录。

如何写入或读取 public 或私人数据库?

我的 PersistentStore Stack 基本上是苹果 WWDC 代码的复制粘贴:

class PersistentStore: ObservableObject {
    var context: NSManagedObjectContext { persistentContainer.viewContext }
    
    // One line singleton
    static let shared = PersistentStore()
    
    private let persistentStoreName: String = "XXXX"
    let containerIdentifier: String = "iCloud.com.XXXX.XXXX"
    
    // MARK: - Core Data stack
    lazy var persistentContainer: NSPersistentContainer = {
        //let container = NSPersistentContainer(name: persistentStoreName)
        // OR - Include the following line for use with CloudKit - NSPersistentCloudKitContainer
        let container = NSPersistentCloudKitContainer(name: persistentStoreName)
        
        // Enable history tracking
        // (to facilitate previous NSPersistentCloudKitContainer's to load as NSPersistentContainer's)
        // (not required when only using NSPersistentCloudKitContainer)
        guard let persistentStoreDescriptions = container.persistentStoreDescriptions.first else {
            fatalError("\(#function): Failed to retrieve a persistent store description.")
        }
        let storesURL = persistentStoreDescriptions.url!.deletingLastPathComponent()
        
        //private database
        let privateStoreURL = storesURL.appendingPathComponent("\(persistentStoreName)-private.sqlite")
        let privateStoreDescription = NSPersistentStoreDescription(url: privateStoreURL)
        privateStoreDescription.configuration = "Private"
        privateStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
        privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        privateStoreDescription.cloudKitContainerOptions?.databaseScope = .private
        
        //public database
        let publicStoreURL = storesURL.appendingPathComponent("\(persistentStoreName)-public.sqlite")
        let publicStoreDescription = NSPersistentStoreDescription(url: publicStoreURL)
        publicStoreDescription.configuration = "Public"
        publicStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
        publicStoreDescription.cloudKitContainerOptions?.databaseScope = .public
        
        container.persistentStoreDescriptions = [publicStoreDescription, privateStoreDescription]
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error {
                // Replace this implementation with code to handle the error appropriately.
                fatalError("Unresolved error \(error)")
            }
        })
        
        // Include the following line for use with CloudKit - NSPersistentCloudKitContainer
        container.viewContext.automaticallyMergesChangesFromParent = true
        // Include the following line for use with CloudKit and to set your merge policy, for example...
        
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        return container
    }()
    
    // Mark the class private so that it is only accessible through the singleton `shared` static property
    private init() {}
    
    // MARK: - Core Data Saving and "other future" support (such as undo)
    
    func save() {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Customize this code block to include application-specific recovery steps.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

您可以使用 assign 来选择将新对象保存到哪个存储区

https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506436-assign

对于请求from/to某家商店你可以使用affectedStores

https://developer.apple.com/documentation/coredata/nsfetchrequest/1506518-affectedstores

请注意,您可能更幸运地在模型编辑器中为 public 和私有创建 2 个配置,并为每个配置分配不同的实体。然后自动从实体对应的store中保存和获取。

我有一个类似的要求:我的应用程序使用 CloudKit 和核心数据与私有和共享数据库,并且实体(ItemPlace)被分配给两者商店,但一次只能使用其中一个。
为了处理这个问题,我使用了一个 var currentlyUsedStores 设置为私有持久存储或共享持久存储。
新实体在 viewContextbackgroundContext 中初始化,但在保存上下文之前,上下文发送通知:

// Register for notifications that the view context will save.
NotificationCenter.default.addObserver(self, 
                                       selector: #selector(viewContextOrBackgroundContextWillSave), 
                                       name: .NSManagedObjectContextWillSave,
                                       object: viewContext)

// Register for notifications that the background context will save.
NotificationCenter.default.addObserver(self, 
                                       selector: #selector(viewContextOrBackgroundContextWillSave), 
                                       name: .NSManagedObjectContextWillSave,
                                       object: backgroundContext)

 @objc func viewContextOrBackgroundContextWillSave(_ notification: Notification) {
    guard let context = notification.object as? NSManagedObjectContext  else { return }
    let inserts = context.insertedObjects
    let itemsAndPlaces = inserts.filter({ [=10=] is Item || [=10=] is Place })
    itemsAndPlaces.forEach({ context.assign([=10=], to: currentlyUsedStores.first!) })
}    

由于 assign 可以在保存新插入的对象之前使用,所有新对象现在仅存储在当前使用的持久存储中。

要仅从当前使用的商店中获取,必须在获取请求中设置 affectedStores 属性,例如

let itemFetchRequest = NSFetchRequest<Item>(entityName: Item.entityName)
itemFetchRequest.affectedStores = currentlyUsedStores  

然后切换当前使用的商店(也可以保存,例如在用户默认中,只需更新var currentlyUsedStores

编辑:
NSBatchInsertRequest 也被 NSManagedContext 执行。这样,只要context中新插入的对象,都可以用同样的方式赋值给相关的持久化存储。