Core Data custom migration policy with multiple targets
Asked Answered
I

3

7

If I want to use a custom migration policy for a given entity, I believe I have to prefix the class name by the product module name, as shown on the following image:

Mapping Model Inspector

How can I manage to handle multiple targets?

I tried using the following entry: $(PRODUCT_MODULE_NAME).VisitToVisitPolicy but this does not seem to work. I still have the possibility to duplicate the mapping model, one for each target, but that does not feel right.

Irrevocable answered 16/1, 2018 at 15:14 Comment(0)
W
3

Had the same problem trying to share model files between app and test targets. Almost gave up and thought I'll have to use your duplicate hack, but thankfully found a sane way:

// Get mapping model
let mappingModel = NSMappingModel(from: [.main], 
                        forSourceModel: sourceModel, 
                      destinationModel: destinationModel)!

// Get migration policy class name that also includes the module name
let fullClassName = NSStringFromClass(NSEntityMigrationPolicySubclass.self)

// Set policy here (I have one policy per migration, so this works)
mappingModel.entityMappings.forEach { 
    $0.entityMigrationPolicyClassName = fullClassName 
}

// Migrate
let manager = NSMigrationManager(sourceModel: sourceModel, 
                            destinationModel: destinationModel)

try! manager.migrateStore(from: sourceURL, 
                    sourceType: NSSQLiteStoreType, 
                       options: nil, 
                          with: mappingModel, 
              toDestinationURL: destinationURL, 
               destinationType: NSSQLiteStoreType, 
            destinationOptions: nil)
Worthless answered 19/10, 2018 at 8:32 Comment(2)
can you please share where you put the above code?Borchardt
Can you provide more context, such as where and how you use this code?Argueta
M
0

I had the same problem. My solution is similar to Alexander's and it should work with multiple migration policies (one per entity). You need to set Custom Policy to class name without any namespace and after obtaining mapping model I do this:

    mapping.entityMappings.forEach {
        if let entityMigrationPolicyClassName = $0.entityMigrationPolicyClassName,
            let namespace = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String {
            $0.entityMigrationPolicyClassName = "\(namespace).\(entityMigrationPolicyClassName)"
        }
    }
Matisse answered 25/9, 2019 at 10:10 Comment(1)
can you please share where you put the above code?Borchardt
C
0

Based on the snippets from the previous answers, here's a more complete example implementation. In order to do this it seems like you need to do a fully manual migration.

Note: I haven't gotten this to work with the @FetchRequest decorator, but it is working with a basic MVVM setup. The part of the migration where I force update to get around WAL issues was causing issues with entities being loaded twice. Running a fetch request directly on a managed object context seems to work fine.

import CoreData

struct DataLayer {
    static let shared = DataLayer()
    let container: NSPersistentContainer
    
    // The base directory for you app's documents
    private var documentsUrl: URL {
        let fileManager = FileManager.default
        
        if let url = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "your.group.here") {
            return url
        } else {
            // Falling back to the regular core data location.
            let urls = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
            return urls.first!
        }
    }

    // The SQLite data store
    private var dataStoreUrl: URL {
        return documentsUrl.appendingPathComponent("YourDataStoreName")
    }

    init() {
        container = NSPersistentContainer(name: "YourDataStoreName")
        
        migrateIfNeeded()
        
        // Prevent Core Data from trying to automatically migrate
        container.persistentStoreDescriptions.first!.url = dataStoreUrl
        container.persistentStoreDescriptions.first!.shouldMigrateStoreAutomatically = false
        container.persistentStoreDescriptions.first!.shouldInferMappingModelAutomatically = false

        // Load the new store just like you normally would
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print("Core Data failed to load: \(error.localizedDescription)")
            }
        })
        
        container.viewContext.automaticallyMergesChangesFromParent = true
    }

    func getItems() -> [Item] {
        let fetchRequest = NSFetchRequest<Item>(entityName: "Item")
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
        
        do {
            return try container.viewContext.fetch(fetchRequest)
        } catch {
            let nsError = error as NSError
            print("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        
        return []
    }

    /// Checks if the current data store is up to date and migrates to the newest version if needed
    func migrateIfNeeded() {
        // The managed object model we might need to migrate to
        let finalManagedObjectModel = NSManagedObjectModel(contentsOf: Bundle.main.url(forResource: "Incremental", withExtension: "momd")!)!
        
        // If the app hasn't ever been launched there might not be a data store at all
        if !FileManager.default.fileExists(atPath: dataStoreUrl.path) {
            print("No store to check")
            return
        }

        // Get metadata from the source data store
        guard let sourceMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, at: dataStoreUrl) else {
            fatalError("Could not find metadata for current data store")
        }
        
        // If the current data store is compatable with the desired object model, no need to do anything
        let compatible = finalManagedObjectModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: sourceMetadata)
        if compatible {
            print("compatible - skipping migration")
            return
        }
        
        // Get the object model of the current data store
        let sourceModel = NSManagedObjectModel.mergedModel(from: [Bundle.main], forStoreMetadata: sourceMetadata)!
        
        // Because iOS by default uses WAL to write new data to a SQLite database there's a chance not all data has been written to the
        // main SQLite file. The following will force iOS to write all lingering data to the main file before migrating.
        do {
            var persistentStoreCoordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: sourceModel)

            let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]]
            let store = try persistentStoreCoordinator!.addPersistentStore(type: .sqlite, at: dataStoreUrl, options: options)
            try persistentStoreCoordinator!.remove(store)
            persistentStoreCoordinator = nil
        } catch let error {
            fatalError("\(error)")
        }
        
        // Search for a mapping model from the current store version to the target version
        // You could also attempt to infer a mapping model here
        guard let mappingModel = NSMappingModel(from: [Bundle.main], forSourceModel: sourceModel, destinationModel: finalManagedObjectModel) else {
            fatalError("Could not find mapping model")
        }
        
        // Fix the migration policies for the current target
        mappingModel.entityMappings.forEach {
            if let entityMigrationPolicyClassName = $0.entityMigrationPolicyClassName,
                let namespace = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String {
                $0.entityMigrationPolicyClassName = "\(namespace).\(entityMigrationPolicyClassName)"
            }
        }
        
        // Set up the migration manager and temporary data store
        let migrationManager = NSMigrationManager(sourceModel: sourceModel, destinationModel: finalManagedObjectModel)
        let tempDataStoreUrl = documentsUrl.appendingPathComponent("TemporaryIncremental.sqlite")
        
        do {
            // Migrate the old data store into the temporary one
            try migrationManager.migrateStore(from: dataStoreUrl, type: .sqlite, options: nil, mapping: mappingModel, to: tempDataStoreUrl, type: .sqlite, options: nil)
            
            // Delete the old data store and move the temporary into the original spot
            try FileManager.default.removeItem(at: dataStoreUrl)
            try FileManager.default.moveItem(at: tempDataStoreUrl, to: dataStoreUrl)
        } catch let error {
            fatalError("\(error)")
        }
    }
}
Chitter answered 27/11, 2023 at 6:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.