0

I use the following code to load data from a web service and store it in a Core Data DB.

    dispatch_queue_t fetchQ = dispatch_queue_create("fetcher", NULL);
dispatch_async(fetchQ, ^{
    NSArray *list = [WebService loadData];
    dispatch_sync(dispatch_get_main_queue(), ^{
        if (list != nil) {
            for (NSDictionary *data in list) {

                NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Object" inManagedObjectContext:appDelegate.coreDataDatabase.managedObjectContext];
                NSFetchRequest *request = [[NSFetchRequest alloc] init];
                [request setEntity:entityDescription];

                NSPredicate *predicate = [NSPredicate predicateWithFormat:@"id = %i", [[data objectForKey:@"id"] integerValue]];
                [request setPredicate:predicate];

                NSError *error = nil;
                NSArray *data = [appDelegate.coreDataDatabase.managedObjectContext executeFetchRequest:request error:&error];

                Object *object = [data objectAtIndex: 0];
                if (object == nil) {
                    object = [NSEntityDescription insertNewObjectForEntityForName:@"Object" inManagedObjectContext:appDelegate.coreDataDatabase.managedObjectContext];
                    object.id = [NSNumber numberWithInteger:[[data objectForKey:@"id"] integerValue]];
                }

                object.name = [data objectForKey:@"name"];
            }

            [appDelegate.coreDataDatabase saveToURL:appDelegate.coreDataDatabase.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
        }
    });
});
dispatch_release(fetchQ);

The code is simplified, but it should represent the sequence. The access to the web service runs in the background. After that, the store is called in the main thread. Should actually OK, right?

Unfortunately, the whole works not on all devices. On all my devices and most of the other, there is no problem. On some but apparently so, because unfortunately there are some negative Bewertugnen in the store and a couple of questions. The problem is that he finds an already existing record in the DB and not constantly new investing

Thanks for your help, Stefan

Urkman
  • 1,298
  • 1
  • 16
  • 32

2 Answers2

1

I am having a hard time understanding your actual question. I am not exactly sure what you mean by this...

On some but apparently so, because unfortunately there are some negative Bewertugnen in the store and a couple of questions. The problem is that he finds an already existing record in the DB and not constantly new investing

so I will take my best guess.

From the code, I assume you are using UIManagedDocument.

You should not save the UIManagedDocument directly (saveToUrl). Instead, you should let it handle the autosaving, as it will do so appropriately.

If you have an NSUndoManager active on the UIManagedDocument, you don't need to do anything. All actions will automatically be saved. If you do not have an undo manager, you need to poke it to get it to know that it has dirty data. You do that with:

[managedDocument updateChangeCount:UIDocumentChangeDone];

However, note that UIManagedDocument uses nested contexts, and there are a few bugs related to inserting new objects. Namely, persistent IDs are not properly pushed back to child contexts. My assumption about your problem is that you are getting duplicate data entries because of this.

This SO question addresses this issue in some depth.

Core Data could not fullfil fault for object after obtainPermanantIDs

Basically, you have several choices, considering you want to continue with UIManagedDocument. First, obtain the permanent IDs. Replace this line:

[appDelegate.coreDataDatabase saveToURL:appDelegate.coreDataDatabase.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];

with this (of course -- handle the error -- this just passes 0):

NSManagedObjectContext *moc = appDelegate.coreDataDatabase.managedObjectContext;
[moc obtainPermanentIDsForObjects:moc.insertedObjects.allObjects error:0];
[appDelegate.coreDataDatabase updateChangeCount:UIDocumentChangeDone];

There are still some issues with this, but they are quite rare. Your code does not appear to have a very complex object graph, so this should suffice.

Another option is to import into a separate MOC (a child of the UIManagedDocument parent context), and just have your main context refetch when the import is complete.

Using your code example, an easy way to do this would be something like...

dispatch_queue_t fetchQ = dispatch_queue_create("fetcher", NULL);
dispatch_async(fetchQ, ^{
    NSArray *list = [WebService loadData];
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
    moc.parentContext = appDelegate.coreDataDatabase.managedObjectContext.parentContext;
    for (NSDictionary *data in list) {
        NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Object" inManagedObjectContext:moc];
        NSFetchRequest *request = [[NSFetchRequest alloc] init];
        [request setEntity:entityDescription];

        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"id = %i", [[data objectForKey:@"id"] integerValue]];
        [request setPredicate:predicate];

        NSError *error = nil;
        NSArray *data = [moc executeFetchRequest:request error:&error];
        Object *object = [data objectAtIndex: 0];
        if (object == nil) {
            object = [NSEntityDescription insertNewObjectForEntityForName:@"Object" inManagedObjectContext:moc];
            object.id = [NSNumber numberWithInteger:[[data objectForKey:@"id"] integerValue]];
        }

        object.name = [data objectForKey:@"name"];
    }

    // Save the MOC you just loaded everything into
    [moc save:&error]; // handle error...
    // Save the parent MOC, which is also the parent of your main MOC
    moc = moc.parentContext;
    [moc performBlock:^{
        [moc save:&error]; // handle error
        // When all the saves are done, run on the main queue and just refetch
        // all the entities that were inserted.
        dispatch_async(dispatch_get_main_queue(), ^{
            // Here, use appDelegate.coreDataDatabase.managedObjectContext
            // to refetch the objects that were altered.  If you are using a
            // table view or FRC just issue a refetch of the data and the
            // main MOC will get everything that was modified.
        });
    }];
});
dispatch_release(fetchQ);
Community
  • 1
  • 1
Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • This seems to work :) My first test are working on a device which was not working before... – Urkman Aug 26 '12 at 18:47
  • In the article you linked, you are talking about a known bug. Where can I find more about this bug? Thanks :) – Urkman Aug 27 '12 at 06:47
  • Unfortunately, bugs reported to Apple are not made public. A google search, however, should provide you with plenty of related hits. In this case, however, I think that post explains it pretty thoroughly. – Jody Hagins Aug 27 '12 at 13:11
0

If you created your context on the main thread, then every access to it also must be on the mainthread (assuming pre-ios 5 configuration - Core Data supports 3 modes post iOS 5).

You can create a serial dispatch queue, create the moc there, and funnel all accesses to it through that queue too. The dispatch queue serializes the accesses so you never have concurrency.

I myself just converted to using the private queue capability in iOS5, in which case every access is done by '[moc perform block...]' - it took me less than 1 hour to convert, and the code actually got cleaner (I had been using a serial queue as mentioned earlier).

David H
  • 40,852
  • 12
  • 92
  • 138