Core Data, caching NSManagedObjects in NSMutableDictionary, Problems - caching

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 Words 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 Words 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:(NSDictionary *) 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)")
}
}

Related

Having Trouble Sorting CoreData backed NSArrayController

Im trying to retrieve an array of sorted objects from my Core Data backed NSArrayController. Once the ManagedObjectContext is ready I fire a Notification which the object owning the NSArrayController listens for and then sets up the fetch request with sortDescriptors. However the data is NEVER sorted. I've been pounding my head on this for several hours now.
I get the expected data in the array but it's not sorted at all. Anyone have any ideas where I am going wrong?
// Elsewhere in code
...
_charts = [[NSArrayController alloc] init];
_charts.entityName = #"Charts";
...
// When Cored Data and Managed Object Context is ready
-(void)mangagedObjectContextReady
{
NSManagedObjectContext *moc = ((AppDelegate *)[NSApp delegate]).managedObjectContext;
_charts.managedObjectContext = moc;
_charts.usesLazyFetching = NO;
_charts.automaticallyPreparesContent = YES;
_charts.automaticallyRearrangesObjects = YES;
// Create fetch request for data
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = [NSEntityDescription entityForName:#"Charts" inManagedObjectContext:moc];
// Create the sort descriptors for the Charts entity
// Name and chartType are properties on the entity "Charts"
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES selector:#selector(caseInsensitiveCompare:)];
NSSortDescriptor *chartTypeDescriptor = [[NSSortDescriptor alloc] initWithKey:#"chartType" ascending:YES selector:#selector(caseInsensitiveCompare:)];
fetchRequest.sortDescriptors = #[nameDescriptor, chartTypeDescriptor];
NSError *error = nil;
BOOL success = [_charts fetchWithRequest:fetchRequest merge:NO error:&error];
if(!success)
NSLog(#"Error %#:", [error localizedDescription]);
else {
[_charts rearrangeObjects];
[_tv reloadData];
}
I use this method to get an array or sorted objects.. but data is not sorted as per above sort descriptors?
[_charts.arrangedObjects objectAtIndex:row]
_charts.sortDescriptors = #[nameDescriptor, chartTypeDescriptor];

Nested NSCollection View

I am trying to create a nested collection view. First I did for one level.
Created a data model class with String header. In app delegate created an array sectionTitle. Now in the nib, I added collection view & array controller and did all the bindings following this guide. Next in awakeFromNib I populated some random data
- (void)awakeFromNib {
int idx = 0;
NSMutableArray *sectionTitle = [[NSMutableArray alloc] init];
while (idx < 1) {
HeaderModel *header = [[HeaderModel alloc] init];
[header setHeader:[NSString stringWithFormat:#"Section %d", idx]];
[sectionTitle addObject:header];
idx++;
}
[self setHeaderData:sectionTitle];
}
Running it will give me 4 sections. I want to achieve similar layout as this. Section title, under it another collection of items. The answer given there only hints at using Nested collection view.
So I added another collection view in the first view prototype. Then I followed the same approach what I did for the first view(with different data model and array).
- (void)awakeFromNib {
int idx = 0;
NSMutableArray *sectionTitle = [[NSMutableArray alloc] init];
NSMutableArray *groupData = [[NSMutableArray alloc] init];
while (idx < 1) {
HeaderModel *header = [[HeaderModel alloc] init];
DataModel *name = [[DataModel alloc] init];
[header setHeader:[NSString stringWithFormat:#"Section %d", idx]];
[name setName:[NSString stringWithFormat:#"Name %d", idx]];
[sectionTitle addObject:header];
[groupData addObject:name];
idx++;
}
[self setHeaderData:sectionTitle];
[self setData:groupData]; //NSCollectionView item prototype must not be nil.
}
But now I get the error NSCollectionView item prototype must not be nil.
How do I resolve this ?
I have just answered a similar question here
But somehow by inserting the second NSCollectionView with I.B, you get a corrupted prototype for your inner NSCollectionViewItem. Simply try to extract each associated NSView into its own .xib

Array of NSManagedObjects has null values in it after passing it to another method

In my app I'm using MagicalRecord for CoreData stack and I'm having a weird behavior of NSManagedObject. I have this method in my DB class that fetches all the entries of the Stuff entity from DB. I'm calling this method from another class, and inside of this getStuff method I'm getting correct values that getting print out in the log statement below.
-(NSArray*) getStuff
{
NSManagedObjectContext * managedObjectContext = [NSManagedObjectContext MR_contextWithStoreCoordinator:self.psc];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Stuff"];
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"idNumber" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObjects:sort, nil]];
NSError *error = nil;
NSArray *arr = [managedObjectContext executeFetchRequest:request error:&error];
for (Stuff *stuff in arr) {
NSLog(#"stuff fN: %#",stuff.idNumber);
}
if (arr)
return arr;
return [NSArray array];
}
However, the array that gets returned by this method to another class contains all these objects with null value in them.
// -- some othe class
// call DB to check if we have stuff there
NSArray* stuff = [database getStuff];
for (Stuff *stuff in stuff) {
NSLog(#"stuff fN: %#",stuff.idNumber);
}
Does anyone know why is this happening?! Any kind of help is highly appreciated!
Your getStuff method creates a new NSManagedObjectContext, which is (assuming that you compile with ARC) deallocated at the end of the method. Managed objects "live" in the context that they were created in. Accessing their properties after the context has been deallocated does not work.
Assuming you setup your CoreData stack with MagicalRecord you can use [NSManagedObjectContext MR_contextForCurrentThread] instead of [NSManagedObjectContext MR_contextWithStoreCoordinator:self.psc]

NSFetchRequest fetches zero matches of entity from database created using UIManagedDocument

I am using Justin Driscoll's article on Core Data with UIManagedDocument in singleton pattern to set it up for UITabViewController. I am running the app on Simulator. Its working fine for the first time. The database is created successfully and I can see data in the tableview controller for each tab. But when I restart my application, the tableviews are empty because NSFetchRequest fetches 0 matches for the entity. The same fetch request fetches correct result during the first run.
I think its something to do with asynchronous nature of loading data and data not autosaving before I stop the app in simulator. So data is not available in second run of app.
The way I am doing my data loading as seen in the code. The fetchDataIntoDocument method does the initial loading of data.
// Document Handler Singleton Class
-(void) performWithDocument:(OnDocumentReady)onDocumentReady {
void (^OnDocumentDidLoad)(BOOL) = ^(BOOL Success) {
onDocumentReady(self.document);
};
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
**[self fetchDataIntoDocument:self.document];**
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateClosed) {
[self.document openWithCompletionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateNormal) {
OnDocumentDidLoad(YES);
}
}
-(void)fetchDataIntoDocument:(UIManagedDocument *)document {
MyEntityDataController *dc= [[MyEntityDataController alloc] init];
NSDictionary *entityInfo =[dc getEntityInfo];
[document.managedObjectContext performBlock:^{
[Entity createEntityWithInfo:entityInfo inManagedObjectContext:document.managedObjectContext];
}];
}
My TableViewController class
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (!self.databaseDocument) {
[[LTDatabaseDocumentHandler sharedDatabaseDocumentHandler] performWithDocument:^ (UIManagedDocument *document) {
self.databaseDocument = document;
[self populateTableViewArrayFromDocument:self.databaseDocument];
}];
}
}
Within populateTableViewArrayFromDocument I am executing my fetch request
-(void)populateTableViewArrayFromDocument:(UIManagedDocument *)document
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Entity2"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
NSError *error = nil;
NSArray *matches = [self.databaseDocument.managedObjectContext executeFetchRequest:request error:&error];
NSLog(#" matches count for Entity2 %d", matches.count);
for (Entity2 *entity2 in matches) {
//do stuff with data and add it to tableview array
}
}
I think I have found why you have this problem. I have just run into this issue and it took some research to figure it out. Basically, you are right. The problem is indeed in the asynchronous nature of UIManagedDocument. You need to wait until the document loads into memory and then do your fetching.
This is the code I use to make sure the document is ready:
if ([[NSFileManager defaultManager] fileExistsAtPath:[_URLDocument path]]) {
[_managedDocument openWithCompletionHandler:^(BOOL success){
[self ready]
if (!success) {
// Handle the error.
}
}];
}
Hope this helps, cheers!

Cocoa Core Data: Setting default entity property values?

I know I can set default values either in the datamodel, or in the -awakeFromInsert method of the entity class. For example, to make a "date" property default to the current date:
- (void) awakeFromInsert
{
NSDate *now = [NSDate date];
self.date = now;
}
How though can I make an "idNumber" property default to one greater than the previous object's idNumber?
Thanks, Oli
EDIT: Relevant code for my attempt (now corrected)
- (void) awakeFromInsert
{
self.idNumber = [NSNumber numberWithInt:[self maxIdNumber] + 1];
}
-(int)maxIdNumber{
NSManagedObjectContext *moc = [self managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Flight" inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
// Set example predicate and sort orderings...
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"idNumber > %#", [NSNumber numberWithInt:0]];
[request setPredicate:predicate];
[request setFetchLimit:1];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"idNumber" ascending:NO];
[request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
[sortDescriptor release];
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array == nil | array.count == 0)
{
return 0;
}
return [[[array objectAtIndex:0] valueForKey:#"idNumber"] intValue];
}
If the maxIdNumber method is called, the new object is added to the table twice!? (but with the correct idNumber). The two entries in the table are linked - editing / removing one also edits / removes the other. For this reason I believe it has something to do with the managed object context. For what its worth, the outcome (two copies) is the same no matter how many times the maxIdNumber method is called in the awakFromNib; even if self.idNumber is just set to [NSNumber numberWithInt:5] and the maxIdNumber method is just called for a throwaway variable.
Any clues??
SOLVED IT!
Ok, the problem of double entry occurs when a fetch request is performed from within the awakeFromInsert method. Quoting from the docs:
You are typically discouraged from performing fetches within an implementation of awakeFromInsert. Although it is allowed, execution of the fetch request can trigger the sending of internal Core Data notifications which may have unwanted side-effects. For example, on Mac OS X, an instance of NSArrayController may end up inserting a new object into its content array twice.
A way to get around it is to use the perfromSelector:withObject:afterDelay method as outlined here (I am only allowed to post one hyperlink :( ):http://www.cocoabuilder.com/archive/cocoa/232606-auto-incrementing-integer-attribute-in-awakefrominsert.html.
My working code is now as follows: (note, I have put the bulk of the fetching code used above into a category to tidy it up a little, this allows me to use the method fetchObjectsForEntityName:withPredicate:withFetchLimit:withSortDescriptors:)
- (void) awakeFromInsert
{
[self performSelector:#selector(setIdNumber) withObject:nil afterDelay:0];
self.date = [NSDate date];
}
-(void)setIdNumber
{
int num = 0;
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"idNumber" ascending:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"idNumber > %#", [NSNumber numberWithInt:0]];
NSArray *array = [[self managedObjectContext] fetchObjectsForEntityName:#"Flight"
withPredicate:predicate
withFetchLimit:0
withSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
[sortDescriptor release];
if (array != nil & array.count != 0)
{
num = [[[array objectAtIndex:0] valueForKey:#"idNumber"] intValue];
}
num ++;
[self setIdNumber:[NSNumber numberWithInt:num]];
}
Let me know what you think!
One Approach: Create a fetch request of all instances of your entity with a limit of 1, sorted by idNumber to get the highest number.
Another Approach: Keep the highest idNumber in your store's metadata and keep incrementing it.
There are plenty of arguments for and against either. Ultimately, those are the two most common and the choice is yours.
An easier way to do that is to override the newObject method of NSArrayController:
- (id) newObject
{
id result=[super newObject];
[result setValue: [NSDate date] forKey: #"date"];
return result;
}

Resources