JohnEstropia / CoreStore

Unleashing the real power of Core Data with the elegance and safety of Swift
MIT License
3.97k stars 255 forks source link

[CoreStore: Error] NSManagedObjectContext+Transaction.swift:203 saveAsynchronouslyWithCompletion #250

Open tosbaha opened 6 years ago

tosbaha commented 6 years ago

I am using ImportUniqueObjects extension to import data from the server. If the Store is empty import works but if there is a previous data, following error comes

⚠️ [CoreStore: Error] NSManagedObjectContext+Transaction.swift:203 saveAsynchronouslyWithCompletion
  ↪︎ Failed to save 'NSManagedObjectContext'.
    (CoreStore.CoreStoreError) .internalError (
    .errorDomain = "com.corestore.error";
    .errorCode = 4;
    .NSError = (
        .domain = "NSCocoaErrorDomain";
        .code = 1560;
        .userInfo = 1 key-value(s) [
 "Error Domain=NSCocoaErrorDomain Code=1570 \"The operation couldn\U2019t be completed. (Cocoa error 1570.)\" UserInfo={NSValidationErrorObject=<KargoDetailCD: 0x600000289fb0> 
(entity: KargoDetailCD; id: 0xd000000004040002 <x-coredata://8E3ADB60-D46E-466A-91C6-840A45557571/KargoDetailCD/p257> ; 
data: {\n    date = \"2015-10-13 06:50:00 +0000\";\n    
lat = nil;\n    location = \"\";\n    
long = nil;\n    
message = \"Message\";\n    
parent = nil;\n    
status = 0;\n}), 
NSValidationErrorKey=parent, NSLocalizedDescription=The operation couldn\U2019t be completed. (Cocoa error 1570.)}",

I know code 1560 is validation error but why it works in the first time but not the second time puzzles me. Error code also shows that parent is nil.

Here is the relevant models



Storage.stack.perform(asynchronous: { transaction in
    try! transaction.importUniqueObjects(Into(KargoCD.self), sourceArray: kargos)
}, success: { done in
    print("DoneStore :\(done)")
}, failure: { error in
    print("MyError CoreStore: \(error)")
})

class KargoCD: NSManagedObject {
    @NSManaged var expected: Date?
    @NSManaged var extra: String?
    @NSManaged var finishDate: Date?
    @NSManaged var isDelivered: NSNumber
    @NSManaged var name: String?
    @NSManaged var receiver: String?
    @NSManaged var sender: String?
    @NSManaged var slug: String
    @NSManaged var startDate: Date?
    @NSManaged var trackingID: String
    @NSManaged var trackingNumber: String
    @NSManaged var updateDate: Date?
    @NSManaged var detail: NSOrderedSet
}

class KargoDetailCD: NSManagedObject {

    @NSManaged var message: String?
    @NSManaged var date: Date?
    @NSManaged var location: String?
    @NSManaged var status: NSNumber
    @NSManaged var lat: NSNumber?
    @NSManaged var long: NSNumber?
    @NSManaged var parent: KargoCD

}

extension KargoDetailCD:ImportableObject {
    typealias ImportSource = KargoDetail

    func didInsert(from source: KargoDetail, in transaction: BaseDataTransaction) throws {
        self.message = source.message
        self.date = source.date
        self.location = source.location
        self.status = NSNumber(value: source.status.rawValue)
    }
}

extension KargoCD:ImportableUniqueObject {
    typealias ImportSource = Kargo
    // MARK: ImportableUniqueObject
    typealias UniqueIDType = String
    static var uniqueIDKeyPath: String {
        return #keyPath(KargoCD.trackingID)
    }

    static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool {
        return true
    }

    static func shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool {
        return true
    }

    static func uniqueID(from source: Kargo, in transaction: BaseDataTransaction) throws -> String? {
        return source.trackingID
    }

    func update(from source: Kargo, in transaction: BaseDataTransaction) throws {
        self.name = source.name
        self.trackingNumber = source.trackingNumber
        self.trackingID = source.trackingID
        self.slug = source.slug
        self.startDate = source.startDate
        self.updateDate = source.updateDate
        self.finishDate = source.finishDate
        self.expected = source.expectedDate
        self.isDelivered = source.isDelivered as NSNumber
        self.sender = source.sender
        self.receiver = source.receiver
        self.extra = source.extra
        let details =  try transaction.importObjects(Into(KargoDetailCD.self), sourceArray: source.details)
        self.detail = NSOrderedSet(array: details)
    }
}
JohnEstropia commented 6 years ago

@tosbaha If I'm interpreting your error message correctly,

NSValidationErrorObject=<KargoDetailCD: 0x600000289fb0> ... NSValidationErrorKey=parent

I think the issue is that the moment this line is executed,

self.detail = NSOrderedSet(array: details)

the old KargoDetailCD instances get their KargoDetailCD.parent set to nil. You probably have a "required" constraint for the KargoDetailCD.parent relation.

Either you remove this requirement, or you delete the old details before you assign new ones. I would recommend the latter so your sqlite file don't get polluted with orphaned records.

tosbaha commented 6 years ago

Thanks a lot! I really appreciate your dedication to this project. I added below code and error is gone.

if let deletable = self.detail.array as? [KargoDetailCD] {
    transaction.delete(deletable)
 }

PS: It is not related to this question directly, but is there a way to have a Uniqueness by using multiple fields. Because for this user case, I don't have any ID to differentiate. Something like Hashable protocol maybe?