Search code examples
swiftcore-dataswift3ios10nsfetchrequest

Understanding Core Data When Deleting Objects that Depend on Each Other


This question is asking for the best practice in the following scenario:

Attached are images showing my work orders and services core data entities. Note that the Delete Rule is currently No Action for Work Order. (Note changing to Nullify will not fix my issue, just causes same issue). Also take note that on Service I have constraints on id. This won't allow duplicates. As such I aded a merge policy below:

context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

The merge policy will take the new data I send and overwrite what is in the database as the default. Without this my program will throw an error with how its written.

If I run my code with these settings, and I do a batch delete on workorders BUT NOT SERVICES (because I want to keep those) what happens is when I restart my program it crashes when I try to add **a reference to a Service with the same id.

My question is why would it crash and what is the best way to work around this? My current theory is that these entities might have another unique identifier and because I deleted the work order its reference was to a different contexted version of services... and when I create the new one using the same id as the old services it assumes the same internal id possibly. I am not sure if this is happening though or how to confirm that.

My code happens in viewDidLoad method of one of my controllers and looks like this.

override func viewDidLoad() {
        super.viewDidLoad()

        // Uncomment the following line to preserve selection between presentations
        // self.clearsSelectionOnViewWillAppear = false

        // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
        // self.navigationItem.rightBarButtonItem = self.editButtonItem()

        let context = gm_getContext()

        //Create default fetch request to get all workorders
        let fetchRequest: NSFetchRequest<Workorders> = Workorders.fetchRequest()

        do{
            //Run fetch request to get search results.
            let searchResults = try context.fetch(fetchRequest)

            //If no results were found and demo mode = true, lets create some default records.
            if(searchResults.count<=0 && g_demoMode==true){
                print("create default data")

                //Uncomment the following lines if you want to prove that the Merge Policy
                //Is working for Unique Constraints.
                let serviceFetchRequest: NSFetchRequest<Service> = Service.fetchRequest()
                let serviceSearchResults = try context.fetch(serviceFetchRequest)
                print("Services Count = \(serviceSearchResults.count)")

                //First we have to create a sample service
                let entity =  NSEntityDescription.entity(forEntityName: "Service", in: context)
                let service = NSManagedObject(entity: entity!, insertInto: context)

                service.setValue(1, forKey: "id")
                service.setValue("Tire Repair Service Sample", forKey: "name")
                service.setValue("<html>Test Service Field</html>",forKey:"templatedata")

                //add reference to the global
                g_services.append(service as! Service)

                //Proof that service is indeed a Service object and stored in global
                print("g_services[0].name = "+g_services[0].name!)

                //Save the service object (overwriting an old one with same id if needed)
                do {
                    try context.save()
                    print("Saved context with service")
                } catch let error as NSError  {
                    print("Could not save \(error), \(error.userInfo)")
                } catch {
                    print("Could not save, unknown error")
                }

                //Now create 3 sample work orders all using the same service template.
                let workorderEntity1 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
                let workorder1 = NSManagedObject(entity: workorderEntity1!, insertInto: context)

                print("created work order variable 1")

                workorder1.setValue(1, forKey: "id")
                workorder1.setValue("11402 Kensington Rd, Los Alamitos, CA, 90720", forKey: "address")
                workorder1.setValue("33.797472", forKey: "lat")
                workorder1.setValue("-118.084136", forKey: "lng")
                workorder1.setValue(15,forKey: "client_id")
                workorder1.setValue("Need to fix their tire fast", forKey: "desc")
                workorder1.setValue("(562)810-4384", forKey: "phone")
                workorder1.setValue(g_services[0], forKey: "service")

                print("Created first work order")

                let workorderEntity2 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
                let workorder2 = NSManagedObject(entity: workorderEntity2!, insertInto: context)

                workorder2.setValue(2, forKey: "id")
                workorder2.setValue("17078 Greenleaf Street, Fountain Valley, CA, 92708", forKey: "address")
                workorder2.setValue("33.714992", forKey: "lat")
                workorder2.setValue("-117.958874", forKey: "lng")
                workorder2.setValue(16,forKey: "client_id")
                workorder2.setValue("This guy does not know what he wants", forKey: "desc")
                workorder2.setValue("(562)777-3344", forKey: "phone")
                workorder2.setValue(g_services[0], forKey: "service")

                let workorderEntity3 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
                let workorder3 = NSManagedObject(entity: workorderEntity3!, insertInto: context)

                workorder3.setValue(3, forKey: "id")
                workorder3.setValue("17045 South Pacific Avenue", forKey: "address")
                workorder3.setValue("33.713565", forKey: "lat")
                workorder3.setValue("-118.067535", forKey: "lng")
                workorder3.setValue(17,forKey: "client_id")
                workorder3.setValue("Tire damaged by the beach", forKey: "desc")
                workorder3.setValue("(714)234-5678", forKey: "phone")
                workorder3.setValue(g_services[0], forKey: "service")

                //Don't need signature, pictures and videos because they just don't exist yet.

                //add reference to the global
                g_workOrders.append(workorder1 as! Workorders)
                g_workOrders.append(workorder2 as! Workorders)
                g_workOrders.append(workorder3 as! Workorders)

                print("Preparing to save to context for work orders")

                //Save the work order objects (overwriting any old ones with same id if needed)
                do {
                    try context.save()
                    print("Saved context with workorders")
                } catch let error as NSError  {
                    print("Could not save \(error), \(error.userInfo)")
                } catch {
                    print("Could not save, unknown error")
                }

            }else{
                print("WorkOrders Count = \(searchResults.count)")

                let workorderFetchRequest   = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
                //let workorderFetchRequest   = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
                let deleteWorkOrderRequest  = NSBatchDeleteRequest(fetchRequest: workorderFetchRequest) //Deletes ALL workorders

                //Perform Actual Deletion On Database Tables
                do{
                    try context.persistentStoreCoordinator!.execute(deleteWorkOrderRequest, with: context)
                }catch{
                    fatalError("Bad Things Happened \(error)")
                }

                print("deleted workorders")
            }

        } catch {
            print("Error with request: \(error)")

        }

        print("service table view controller loaded")
    }

My context and global variables to track the coreData values are defined globally in a globals.swift file like this.

 var g_workOrders = [Workorders]()
 var g_services = [Service]()

//Shortcut method to get the viewcontext easily from anywhere.
func gm_getContext () -> NSManagedObjectContext {
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    let context = appDelegate.persistentContainer.viewContext

    //For unique constraints it will overwrite the data.
    context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

    return context
}

Core Data Model References: enter image description here

enter image description here

Other Notes & Things I've Tried:

I know it crashes at this line (workorder1.setValue(g_services[0], forKey: "service")), which is how I know its related to service, and changing the rule to cascade delete for workorders fixes the crash however it deletes the Services that were attached to it! ... which makes sense but not what I wanted.


Solution

  • I have recently found the answer to my question, and the problem is related to multiple things.

    First my core data stack was set incorrectly. I've now changed it to this (courtesy my friendly developer friend who pointed this out).

    import UIKit
    import CoreData
    
    class DataController: NSObject {
    
        var managedObjectContext: NSManagedObjectContext
        static var dataController: DataController!
    
        override init() {
            // This resource is the same name as your xcdatamodeld contained in your project.
            guard let modelURL = Bundle.main.url(forResource: "WorkOrders", withExtension: "momd") else {
                fatalError("Error loading model from bundle")
            }
    
            // The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
            guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
                fatalError("Error initializing mom from: \(modelURL)")
            }
    
            let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
    
            managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
            managedObjectContext.persistentStoreCoordinator = psc
    
            let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
            let docURL = urls[urls.endIndex-1]
            /* The directory the application uses to store the Core Data store file.
             This code uses a file named "DataModel.sqlite" in the application's documents directory.
             */
            let storeURL = docURL.appendingPathComponent("WorkOrders.sqlite")
            do {
                let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]]
                try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
            } catch {
                fatalError("Error migrating store: \(error)")
            }
    
        }
    
        class func sharedInstance() -> DataController {
    
            if (dataController != nil) {
                return dataController
            }
    
            dataController = DataController()
    
            return dataController
        }
    }
    

    Whenever I need to access coreData I should be doing it this way now...

    let context = DataController.sharedInstance().managedObjectContext
    

    Another thing to note is the concurrency setting in the Datacontroller is set to work on the main thread. This was also part of the problem since I was running my code in a thread.

    Its set to the main thread on this line in DataController

    managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    

    So everytime you are going to access or save data to coreData always wrap it in a call to the main thread like below...

    DispatchQueue.main.async {
       AppDelegate.appDelegate.saveContext()
    }
    

    Finally, the last problem I had was I was doing a batch delete with the following command below.

    let workorderFetchRequest   = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
                let deleteWorkOrderRequest  = NSBatchDeleteRequest(fetchRequest: workorderFetchRequest) //Deletes ALL workorders
    let context = DataController.sharedInstance().managedObjectContext
    
    //Save the work order objects (overwriting any old ones with same id if needed)
                do {
                    try context.execute(deleteWorkOrderRequest)
                    context.reset()
                    print(">>> cleared old data!")
                } catch let error as NSError  {
                    print("Could not save \(error), \(error.userInfo)")
                } catch {
                    print("Could not save, unknown error")
                }
    

    The key here is understanding that batch commands currently work directly on the database and ignore the managed context, this means my managed context and database were getting out of sync after I ran this command. The easy fix is to always make sure after doing batch commands to run...

    context.reset()
    

    This will forcefully load back the data from the database into the managed context so everything is in sync. After I made these changes everything worked fine. Hope this helps someone.