Search code examples
objective-ciosvalidationcore-dataduplicates

Preventing Core Data Duplicates with validateForInsert


My application has a race condition where it is possible that multiple API requests could return the exact same data and attempt to save them. I want to prevent this from happening by adding validateForInsert on my models. The premise of the validation would simply be check and see if the identifier key exists already like this

- (BOOL)validateForInsert:(NSError *__autoreleasing *)error
{
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([CWDeal class])];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"identifier == %@", self.identifier];
    NSError *validateError;
    int count = [[(CWAppDelegate *)[[UIApplication sharedApplication] delegate] privatewriterManagedObjectContext] countForFetchRequest:fetchRequest error:&validateError];
    if (count > 0) {
        return FALSE;
    }
    return [super validateForInsert:error];
}

The issue is that nothing is ever saved. I have a managedObjectContext(main thread), which has a parent of privateWriterManagedObjectContext(connect to PSC). When I import things, I'll create an importContext(some background thread) which has a parent of managedObjectContext. When I get new data and attempt to save the flow would be something like this.

(Remember validation is checking privateWriterMOC for the object)

Create Object on importContext -> Save -> Validate -> Okay.

(Data gets pushed up to importContext's parent, managedObjectContext).

Save managedObjectedContext -> Validate -> Okay.

(Data gets pushed up to managedObjectContext's parent, privateWriterMOC).

Save privateWriterMOC -> Validate -> Failure. privateWriterMOC recognizes that the objects are on its context and will not save them.

There doesn't seem to be a lot of documentation out there on using validateForInsert so I'm hoping someone has a suggestion on how to go about doing this?


Solution

  • Here is the code I went to production with:

    - (BOOL)validateForInsert:(NSError *__autoreleasing *)error {
        [[(AppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator] lock];
        NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Deal class])];
        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"identifier == %@", self.identifier];
        NSError *validateError;
        int count = [[(AppDelegate *)[[UIApplication sharedApplication] delegate] validationContext] countForFetchRequest:fetchRequest error:&validateError];
        if (count > 0) {
            [[(AppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator] unlock];
            return FALSE;
        }
        [[(CWAppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator] unlock];
        return [super validateForInsert:error];
    }
    

    Locking the PSC was the key to this working. I was getting a lot of deadlocks before when different contexts would try to reach out to the PSC at the same time. The only flaw that I've found with this approach is that if one object returns false the entire context is marked invalid and won't save. Say you have many good objects but a single bad object all on an unsaved context, that single bad object will not allow the good objects to save. I ran into the issue while testing, but in production I haven't seen any problems.


    I'm going to post an answer I came up with as I writing the question and just tested. I have no idea if this is the correct idea or not, but it seems to be preventing duplicates at this time.

    What I did was create another context in my appDelegate called validationContext and set it up exactly the same as the privateWriterMOC. Basically the idea is that it is only connected to the store and the only data it knows about has already been written. When validateForInsert is called I used validationContext to perform the fetch and that will inform me if that object has already been saved.

    I believe the race condition could still occur (assume the write and the fetch occur at the same time and the fetch returns first) but I'll look into it more (maybe the sqlite3 db is atomic?). The race condition is quite an edge case, but I just wanted to get it handled just in case.