swiftcore-data

Executing NSBatchInsertRequest creates duplicates


When I fetch the entities after a batch insert in the main queue's managed object context to update my SwiftUI list, I notice there are always duplicates (to be precise, literal doubling of each entity).

I perform a batch insert of the files the user imports through the "import files" method in the Manager class. Then, I fetch them into the managed object context of the main queue to update my SwiftUI list.

class DataController {
    static let shared = DataController()

    func createNewBackgroundContext() -> NSManagedObjectContext {
        let taskContext = container.newBackgroundContext()
        taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        return taskContext
    }
}

class Manager {
    func importFiles(from urls: [URL]) async {
        let importContext = DataController.shared.createNewBackgroundContext()
        
        var objects: [[String: Any]] = []
        
        for url in urls {
            if url.startAccessingSecurityScopedResource() {
                let metadata = getMetadata(url: url)
                
                let audioFileName = url.deletingPathExtension().lastPathComponent                  
                let fileExtension = url.pathExtension
                let artist = metadata?.value(forKey: "artist") as? String ?? "Unknown Artist"
                let album = metadata?.value(forKey: "album") as? String ?? "Unknown Album"
                let genre = metadata?.value(forKey: "genre") as? String ?? "Unknown Genre"
                let composer = metadata?.value(forKey: "composer") as? String ?? "Unknown Composer"
                let year = metadata?.value(forKey: "year") as? String ?? "Unknown Release Year"
                let importDate = Date()
                let artwork = audioFileName + ".\(fileExtension)"
                let trackNumber = metadata?.value(forKey: "trackNumber") as? Int ?? -1
                
                let properties: [String: Any] = [
                    "name": audioFileName,
                    "artist": artist,
                    "album": album,
                    "genre": genre,
                    "year": year,
                    "composer": composer,
                    "importDate": importDate,
                    "trackNumber": trackNumber,
                    "artwork": artwork,
                    "fileExtension": fileExtension,
                    "favourite": false
                ]
                objects.append(properties)

                //Save the file to disk
                save(url: url, with: audioFileName + ".\(fileExtension)")
            }
            
            url.stopAccessingSecurityScopedResource()
        }
             
//objects array is confirmed to have the right number of items to be inserted

await importContext.perform { [weak self] in
        guard let self else { return }
                    
    let batchInsertRequest = NSBatchInsertRequest(entity: AudioFile.entity(), objects: objects)
    batchInsertRequest.resultType = .objectIDs
    
    let result = try? importContext.execute(batchInsertRequest) as? NSBatchInsertResult
    let objs = result?.result as? [NSManagedObjectID] ?? []

//The number of ids is always double here for some reason...

    let changes: [AnyHashable: Any] = [NSInsertedObjectIDsKey: objs]
    // Merge changes
    NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [moc])
}

        await MainActor.run {
             SongsListViewModel.shared.fetchAudioFiles()
        }
    }
}

class SongsListViewModel: ObservableObject {
    static let shared = AllSongsListViewModel()
    
    @Published var audioFiles = [AudioFile]()
    private let moc = DataController.shared.container.viewContext
    
    func fetchAudioFiles() {
        let fetchRequest = AudioFile.fetchRequest()
        if let audioFiles = try? moc.fetch(fetchRequest) {
            self.audioFiles = audioFiles
        }
    }
}

Solution

  • I have found the source of the duplicate entries. If anyone cares to explain why "shouldMigrateStoreAutomatically" should be set to true to prevent a NSBatchInsertRequest from inserting a copy for each entity, that would be great.

    let description = NSPersistentStoreDescription()
    //I have set this property of the description to false all along. Apparently this must be set to true to avoid duplicates when performing a NSBatchInsertRequest
    //description.shouldMigrateStoreAutomatically = false
    description.shouldMigrateStoreAutomatically = true
    container.persistentStoreDescriptions = [description]
    

    There were zero information regarding this on stack overflow or any mention regarding this in the Apple Documentation.