Search code examples
swiftcore-datacore-data-migration

Migration to change the configuration of CoreData


I started a macOS project using Default configuration of CoreData. Application was released and some users started to use it. Now, I need some data to be synced with iCloud and some data to be only stored locally. If I understand correctly, the only way I can achieve this is to create two different configurations (in CoreData data model), add the needed entities in each configuration, and configure the NSPersistentContainer accordingly. However the above method might lead to some data loss since I wont be using the Default configuration anymore.

Is there any way I can "migrate" the data saved under the Default configuration to another configuration?


Solution

  • After some trial and error I found a solution that seems to do the work (however, it seems dirty). First, when instantiating the container, I make sure I add my 3 storeDescriptors to persistentStoreDescriptions (each representing an scheme)

    let defaultDirectoryURL = NSPersistentContainer.defaultDirectoryURL()
    var persistentStoreDescriptions: [NSPersistentStoreDescription] = []
    
    let localStoreLocation = defaultDirectoryURL.appendingPathComponent("Local.sqlite")
    let localStoreDescription = NSPersistentStoreDescription(url: localStoreLocation)
    localStoreDescription.cloudKitContainerOptions = nil
    localStoreDescription.configuration = "Local"
    persistentStoreDescriptions.append(localStoreDescription)
    
    let cloudStoreLocation = defaultDirectoryURL.appendingPathComponent("Cloud.sqlite")
    let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreLocation)
    cloudStoreDescription.configuration = "iCloud"
    cloudStoreDescription.cloudKitContainerOptions = "iCloud.com.xxx.yyy"
    
    persistentStoreDescriptions.append(cloudStoreDescription)
    
    let defaultStoreLocation = defaultDirectoryURL.appendingPathComponent("Default.sqlite")
    let defaultStoreDescription = NSPersistentStoreDescription(url: defaultStoreLocation)
    defaultStoreDescription.cloudKitContainerOptions = nil
    defaultStoreDescription.configuration = "Default"
    persistentStoreDescriptions.append(defaultStoreDescription)
    
    container.persistentStoreDescriptions = persistentStoreDescriptions       
    

    Note: One important thing is to make sure that NSPersistentStoreDescription with the Default configuration is added last.

    Secondly, I am for-eaching thought all data saved in core data checking if managedObject.objectID.persistentStore?.configurationName is "Default" (or any string containing Default. With my empiric implementation I got to the conclusion that configuration name might be different from case to case). If the above condition is true, create a new managedObject, I copy all properties from the old one to new one, delete the old managed object, and save the context.

    for oldManagedObject in managedObjectRepository.getAll() {
        guard let configurationName = oldManagedObject.objectID.persistentStore?.configurationName else {
            continue
        }
        
        if (configurationName == "Default") {
            let newManagedObject = managedObjectRepository.newManagedObject()
            
            newManagedObject.uuid = oldManagedObject.uuid
            newManagedObject.createDate = oldManagedObject.createDate
            ......
    
            managedObjectRepository.delete(item: oldManagedObject)        
            managedObjectRepository.saveContext()
        }
    }
    

    With this implementation, old data that was previously saved in Default.sqlite is now saved in Local.sqlite or 'Cloud.sqlite' (depending on which configuration contains which entity).