Core Data, caching NSManagedObjects in NSMutableDictionary, Problems
I am writing a dictionary application, and i am trying to import raw data from strings, one string per word. Amongst other things, the raw input strings contain the names of the parts of speech the corresponding word belongs to. In my datamodel I have a separate entity for Word
s and PartOfSpeech
, and i want to create one entity of the type PartOfSpeech
for each unique part of speech there may be in the input strings, and establish the relationships from the Word
s to the relevant pars of speech. The PartOfSpeech
entity has just one Atribute, name
, and one-to-many relationship to the Word
:
My first implementation of getting unique PartOfSpeech
entities involved caching them in a mutable array and filtering it each time with a predicate. It worked, but it was slow. I decided to speed it up a bit by caching the PartsOfSpeech in an NSDictionary, and now when i try and save the datastore after the import, i get the error "Cannot save objects with references outside of their own stores.". It looks like the problem is in the dictionary, but how can i solve it?
Here is the code that worked:
(in both sniplets managedObjectContext
is an ivar, and processStringsInBackground:
method runs on a background thread using performSelectorInBackground:withObject:
method)
- (void) processStringsInBackground:(NSFetchRequest *)wordStringsReq {
NSError *err = NULL;
NSFetchRequest *req = [[NSFetchRequest alloc] init];
[req setEntity:[NSEntityDescription entityForName:@"PartOfSpeech" inManagedObjectContext:managedObjectContext]];
err = NULL;
NSMutableArray *selectedPartsOfSpeech = [[managedObjectContext executeFetchRequest:req error:&err] mutableCopy];
NSPredicate *p = [NSPredicate predicateWithFormat:@"name like[c] $name"];
// NSPredicate *formNamePredicate = [NSPredicate predicateWithFormat:<#(NSString *)predicateFormat#>]
...
for (int i = 0; i < count; i++){
...
currentPos = [self uniqueEntityWithName:@"PartOfSpeech" usingMutableArray:selectedPartsOfSpeech predicate:p andDictionary:[NSDictionary dictionaryWithObject:partOfSpeech forKey:@"name"]];
...
}
}
- (NSManagedObject *) uniqueEntityWithName:(NSString *) entityName usingMutableArray:(NSMutableArray *)objects predicate:(NSPredicate *)aPredicate andDictionary:(NSDictio开发者_如何学编程nary *) params {
NSPredicate *p = [aPredicate predicateWithSubstitutionVariables:params];
NSArray *filteredArray = [objects filteredArrayUsingPredicate:p];
if ([filteredArray count] > 0) {
return [filteredArray objectAtIndex:0];
}
NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:managedObjectContext];
NSArray *dicKeys = [params allKeys];
for (NSString *key in dicKeys) {
[newObject willChangeValueForKey:key];
[newObject setPrimitiveValue:[params valueForKey:key] forKey:key];
[newObject didChangeValueForKey:key];
}
[objects addObject:newObject];
return newObject;
}
And here is the same, but with caching using NSMutableDictionary, which fails to save afterwards:
- (void) processStringsInBackground:(NSFetchRequest *)wordStringsReq {
NSError *err = NULL;
[req setEntity:[NSEntityDescription entityForName:@"PartOfSpeech" inManagedObjectContext:managedObjectContext]];
NSArray *selectedPartsOfSpeech = [managedObjectContext executeFetchRequest:req error:&err];
NSMutableDictionary *partsOfSpeechChache = [[NSMutableDictionary alloc] init];
for (PartOfSpeech *pos in selectedPartsOfSpeech) {
[partsOfSpeechChache setObject:pos forKey:pos.name];
}
...
for (int i = 0; i < count; i++){
...
currentPos = [self uniqueEntity:@"PartOfSpeech" withName:partOfSpeech usingDictionary:partsOfSpeechChache];
...
}
}
- (NSManagedObject *)uniqueEntity:(NSString *) entityName withName:(NSString *) name usingDictionary:(NSMutableDictionary *) dic {
NSManagedObject *pos = [dic objectForKey:name];
if (pos != nil) {
return pos;
}
NSManagedObject *newPos = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:managedObjectContext];
[newPos willChangeValueForKey:@"name"];
[newPos setPrimitiveValue:name forKey:@"name"];
[newPos didChangeValueForKey:@"name"];
[dic setObject:newPos forKey:name];
return newPos;
}
Could you help me to find the problem?
Best regards, Timofey.
The error is caused by forming a relationship between managedObjects that don't share the same persistent store. You can do that by:
- Creating a managed object with initialization without inserting it into a context.
- Deleting a managed object from a context while retaining it in another object e.g. array, and then forming a relationship with it.
- Accidentally creating two Core Data stacks so that you have two context and two stores.
- Confusing configurations in a multi-store context.
I don't see any part of the code you provided that would trigger the problem.
It turns out, that it is wrong to pass NSManagedContext to a thread different from the one it was created in. Instead, one should pass the NSPersistenceStroreCoordinator to another thread, and create a new managed object context there. In order to merge the changes into the "main" context, one should save the other thread's context, receive the notification on the completion of the save on the main thread and merge the changes (see apple docs regarding Core Data and concurrency, can't give you the link, because i read it in Xcode). So here are the changes i made to my code to make it work (only posting the changed lines):
— (void) processStringsInBackground:(NSDictionary *) params {
NSFetchRequest *wordStringsReq = [params objectForKey:@"wordStringsReq"];
NSPersistentStoreCoordinator *coordinator = [params objectForKey:@"coordinator"];
NSManagedObjectContext *localContext = [[NSManagedObjectContext alloc] init];
[localContext setPersistentStoreCoordinator:coordinator];
(all the references to the managedObjectContext
were replaced by localContext
And on the main thread, i call this method thusly:
.......
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:req, @"wordStringsReq", persistentStoreCoordinator, @"coordinator", nil]; //the params i pass to the background method
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"NSManagingContextDidSaveChangesNotification" object:nil]; //register to receive the notification from the save
[self performSelectorInBackground:@selector(processStringsInBackground:) withObject:dict];
} - (void) handleNotification:(NSNotification *) notific { NSLog(@"got notification, %@", [notific name]); [managedObjectContext mergeChangesFromContextDidSaveNotification:notific]; }
Good luck!
Good answers, though a bit dated. The fine documentation notes that the main NSManagedObjectContext should never be used in worker threads. Instead, create a separate NSManagedObjectContext private to the worker using the "main" MOC as a parent, and then that instead. Here's the relevant "Concurrency" page from the Core Data Programming Guide:
https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Conceptual/CoreData/Concurrency.html
Snippet (Swift)
let jsonArray = … //JSON data to be imported into Core Data
let moc = … //Our primary context on the main queue
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc
privateMOC.performBlock {
for jsonObject in jsonArray {
let mo = … //Managed object that matches the incoming JSON structure
//update MO with data from the dictionary
}
do {
try privateMOC.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
精彩评论