Optimizing this Core Data request - iphone

I have an entity in Core Data named MusicInterest. I have to add 5000 or so of these at a time and my current process is to query to see if the MusicInterest exists already, if not create a new one.
It seems this requires 5000 trips to the store to see if each title exists. There are also, of course, insert trips, but the 5000 queries is what's slowing me down.
Each FacebookFriend will have multiple music interests, and I enumerate through each one using an array of string titles, calling the following code.
Any ideas how to optimize this?
+ (MusicInterest*) musicInterestForFacebookFriend:(FacebookFriend*)facebookFriend WithTitle:(NSString*)musicTitle UsingManagedObjectContext:(NSManagedObjectContext*)moc
{
// query to see if there
NSArray *matches = [self queryForMusicTitle:musicTitle moc:moc];
if (([matches count] >= 1)) {
// NSLog(#"Music already in database");
MusicInterest *existingMusic = [matches lastObject];
[existingMusic addLikedByObject:facebookFriend];
return [matches lastObject];
} else {
// create new Music Interest
MusicInterest *newMusic = [NSEntityDescription insertNewObjectForEntityForName:#"MusicInterest" inManagedObjectContext:moc];
newMusic.title = musicTitle;
[newMusic addLikedByObject:facebookFriend];
return newMusic;
}
}
+ (NSArray *)queryForMusicTitle:(NSString *)MusicTitle moc:(NSManagedObjectContext *)moc
{
// query to see if there
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MusicInterest"];
request.predicate = [NSPredicate predicateWithFormat:#"title == %#", [NSString stringWithFormat:#"%#", MusicTitle]];
NSError *error = nil;
NSArray *matches = [moc executeFetchRequest:request error:&error];
if (error) {
NSLog(#"Error querying title in Music interest. Error = %#", error);
}
return matches;
}
UPDATE:
I employed the design suggested in the Core Data programming guide and it reduced my time from 12 seconds to 4 seconds (still needs some optimization in other areas :)
The guide only includes half the sample code - I thought I would share my complete implementation:
musicArray = [[music componentsSeparatedByString:#", "] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if (obj1 > obj2)
return NSOrderedDescending;
else if (obj1 < obj2)
return NSOrderedAscending;
return NSOrderedSame;
}];
if (musicArray) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MusicInterest"];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"title IN %#", musicArray]];
[fetchRequest setSortDescriptors:
#[[[NSSortDescriptor alloc] initWithKey: #"title" ascending:YES]]];
NSError *fetchError = nil;
NSArray *musicInterestMatchingTitles = [backgroundContext executeFetchRequest:fetchRequest error:&fetchError];
if ([musicArray count] > 0) {
// walk musicArray and musicInterestsMatchingTitles in parallel
for (int i = 0; i < [musicArray count]; i++) {
NSString *title = musicArray[i];
if (i < [musicInterestMatchingTitles count]) {
MusicInterest *comparingMusicInterest = musicInterestMatchingTitles[i];
// compare each title
if (![title isEqualToString:comparingMusicInterest.title]) {
// if it doesn't exist as a ManagedObject (a MusicInterest), create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
} else {
// otherwise, just establish the relationship
[comparingMusicInterest addLikedByObject:friend];
}
} else {
// if there are no existing matching managedObjects, create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
}
}
}
}
}];
[self saveBackgroundContext:backgroundContext];

Implementing Find-or-Create Efficiently in the "Core Data Programming Guide" describes a pattern that might be useful here. The basic idea is:
Sort your list of items that you want to insert/update by some unique id that is also stored in
the database.
Perform a single fetch request that fetches all objects from the database that have an id from your list, sorted by the same id.
Now traverse your list and the array of fetched items in parallel, to find which items have to be inserted and which items already exist and can be updated.

Related

Predicate and expression to fetch complex request core data

I have to make a complex core data fetch request but I don't know if it can be made.
This is my scenario: just one entity (Expense) with these attributes:
Cost (NSDecimalNumber)
Deposit (NSDecimalNumber)
Category (NSString)
Paid (Boolean Value)
The request should return the 3 most expensive categories but these are the rules that must be respected:
If Paid == YES, Expense cost should be added to Expense category total
If Paid == NO && Deposit > 0, Expense deposit should be added to Expense category total
If Paid == NO, nothing should be added to Expense category total
Using NSExpression, I'm able to calculate every total per category but it also includes cost of Expenses not paid.
Is there a way to accomplish this?
Thank you so much!
You could, for example, use a NSFetchRequest:
// Build the fetch request
NSString *entityName = NSStringFromClass([Expense class]);
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = entity;
which filters only relevant expenses:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(paid == YES) OR ((paid == NO) AND (deposit > 0))"];
request.predicate = predicate;
and sums up the cost and depost attributes:
NSExpressionDescription *(^makeExpressionDescription)(NSString *, NSString *) = ^(NSString *keyPath, NSString *name)
{
// Create an expression for the key path.
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:keyPath];
// Create an expression to represent the function you want to apply
NSExpression *totalExpression = [NSExpression expressionForFunction: #"sum:" arguments: #[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
// The name is the key that will be used in the dictionary for the return value
expressionDescription.name = name;
expressionDescription.expression = totalExpression;
expressionDescription.expressionResultType = NSDecimalAttributeType;
return expressionDescription;
};
NSExpressionDescription *totalCostDescription = makeExpressionDescription(#"cost", #"totalCost");
NSExpressionDescription *totalDepositDescription = makeExpressionDescription(#"deposit", #"totalDeposit");
// Specify that the request should return dictionaries.
request.resultType = NSDictionaryResultType;
request.propertiesToFetch = #[categoryDescription,
paidDescription,
totalCostDescription,
totalDepositDescription];
and group the results by category and paid status:
// Get 'category' and 'paid' attribute descriptions
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName
inManagedObjectContext:context];
NSDictionary *attributes = [entity attributesByName];
NSAttributeDescription *categoryDescription = attributes[#"category"];
NSAttributeDescription *paidDescription = attributes[#"paid"];
// Group by 'category' and 'paid' attributes
request.propertiesToGroupBy = #[categoryDescription, paidDescription];
You'll get paid and unpaid expenses summed up
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
all you need to do is combine (and sort) then:
if (results) {
NSMutableDictionary *combined = [NSMutableDictionary dictionary];
for (NSDictionary *result in results) {
NSString *category = result[#"category"];
BOOL paid = [result[#"paid"] boolValue];
NSDecimalNumber *total = result[paid ? #"totalCost" : #"totalDeposit"];
NSDecimalNumber *sum = combined[category];
if (sum) {
total = [total decimalNumberByAdding:sum];
}
combined[category] = total;
}
NSArray *sortedCategories = [combined keysSortedByValueUsingSelector:#selector(compare:)];
[sortedCategories enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"Category %#: %#", obj, combined[obj]);
}];
}
else {
NSLog(#"Error: %#", error);
}

Using a file (CSV) instead of using CoreData

An app I'm building contains a catalogue of thousands of items, which need to be stored on the phone. Currently I am achieving this through CoreData, as logically it seemed like the best place to put it. I'm using GCD to run the CoreData insertion processes in the background and showing a progress bar / current percentage complete. This works as expected, however for only 5000 items, it's taking 8 minutes to complete on an iPhone 4. This application will be used on the 3GS and up, and will more likely contain 30/40 thousand items once it launches. Therefore this processing time is going to be horrifically long.
Is there any way I can use a CSV file or something to search through instead of storing each item in CoreData? I'm assuming there are some efficiency downfalls with an approach like this, but it would alleviate the excessive wait times. Unless there is another solution that would help with this problem.
Thanks.
EDIT:
I'm not sure how I'd go about saving the context at the end of the entire operation, as it uses a separate context within the loop. Any suggestions for this would be very much appreciated. I've got no idea how to progress with this.
Insertion Code Being Used
- (void) processUpdatesGCD {
NSArray *jsonArray=[NSJSONSerialization JSONObjectWithData:_responseData options:0 error:nil];
NSArray *products = [jsonArray valueForKey:#"products"];
NSArray *deletions;
if ([jsonArray valueForKey:#"deletions"] == (id)[NSNull null]){
self.totalCount = [products count];
} else {
deletions = [jsonArray valueForKey:#"deletions"];
self.totalCount = [products count] + [deletions count];
}
self.productDBCount = 0;
_delegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = _delegate.managedObjectContext;
self.persistentStoreCoordinator = [managedObjectContext persistentStoreCoordinator];
_managedObjectContext = managedObjectContext;
// Create a new background queue for GCD
dispatch_queue_t backgroundDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
for (id p in products) {
// id product = p;
// Dispatch the following code on our background queue
dispatch_async(backgroundDispatchQueue,
^{
id product = p;
// Because at this point we are running in another thread we need to create a
// new NSManagedContext using the app's persistance store coordinator
NSManagedObjectContext *backgroundThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
NSFetchRequest *BGRequest = [[NSFetchRequest alloc] init];
NSLog(#"Running.. (%#)", product);
[BGRequest setEntity:[NSEntityDescription entityForName:#"Products" inManagedObjectContext:backgroundThreadContext]];
[BGRequest setIncludesSubentities:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"codes == %#", [product valueForKey:#"product_codes"]];
[BGRequest setPredicate:predicate];
NSError *err;
NSArray *results = [backgroundThreadContext executeFetchRequest:BGRequest error:&err];
if (results.count == 0){
// Product doesn't exist with code, make a new product
NSLog(#"Product not found for add/update (%#)", [product valueForKey:#"product_name"]);
NSManagedObject* newProduct;
newProduct = [NSEntityDescription insertNewObjectForEntityForName:#"Products" inManagedObjectContext:backgroundThreadContext];
[newProduct setValue:[product valueForKey:#"product_name"] forKey:#"name"];
[newProduct setValue:[product valueForKey:#"product_codes"] forKey:#"codes"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[newProduct setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[newProduct setValue:information forKey:#"information"];
}
} else {
NSLog(#"Product found for add/update (%#)", [product valueForKey:#"product_name"]);
// Product exists, update existing product
for (NSManagedObject *r in results) {
[r setValue:[product valueForKey:#"product_name"] forKey:#"name"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[r setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[r setValue:information forKey:#"information"];
}
}
}
// Is very important that you save the context before moving to the Main Thread,
// because we need that the new object is writted on the database before continuing
NSError *error;
if(![backgroundThreadContext save:&error])
{
NSLog(#"There was a problem saving the context (add/update). With error: %#, and user info: %#",
[error localizedDescription],
[error userInfo]);
}
// Now let's move to the main thread
dispatch_async(dispatch_get_main_queue(), ^
{
// If you have a main thread context you can use it, this time i will create a
// new one
// NSManagedObjectContext *mainThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
// [mainThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
self.productDBCount = self.productDBCount + 1;
float progress = ((float)self.productDBCount / (float)self.totalCount);
int percent = progress * 100.0f;
// NSNumber *progress = [NSNumber numberWithFloat:((float)self.productDBCount / (float)self.totalCount)];
self.downloadUpdateProgress.progress = progress;
self.percentageComplete.text = [NSString stringWithFormat:#"%i", percent];
NSLog(#"Added / updated product %f // ProductDBCount: %i // Percentage progress: %i // Total Count: %i", progress, self.productDBCount, percent, self.totalCount);
if (self.productDBCount == self.totalCount){
[self updatesCompleted:[jsonArray valueForKey:#"last_updated"]];
}
});
});
}
if ([deletions count] > 0){
for (id d in deletions){
dispatch_async(backgroundDispatchQueue,
^{
id deleted = d;
// Because at this point we are running in another thread we need to create a
// new NSManagedContext using the app's persistance store coordinator
NSManagedObjectContext *backgroundThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
NSFetchRequest *BGRequest = [[NSFetchRequest alloc] init];
// NSLog(#"Running.. (%#)", deleted);
[BGRequest setEntity:[NSEntityDescription entityForName:#"Products" inManagedObjectContext:backgroundThreadContext]];
[BGRequest setIncludesSubentities:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"codes == %#", [deleted valueForKey:#"product_codes"]];
[BGRequest setPredicate:predicate];
NSError *err;
NSArray *results = [backgroundThreadContext executeFetchRequest:BGRequest error:&err];
if (results.count == 0){
// Product doesn't exist with code, make a new product
NSLog(#"Product not found, can't delete.. %#", [deleted valueForKey:#"product_name"]);
} else {
NSLog(#"Product found, deleting: %#", [deleted valueForKey:#"product_name"]);
// Product exists, update existing product
for (NSManagedObject *r in results) {
[backgroundThreadContext deleteObject:r];
}
}
// Is very important that you save the context before moving to the Main Thread,
// because we need that the new object is writted on the database before continuing
NSError *error;
if(![backgroundThreadContext save:&error])
{
NSLog(#"There was a problem saving the context (delete). With error: %#, and user info: %#",
[error localizedDescription],
[error userInfo]);
}
// Now let's move to the main thread
dispatch_async(dispatch_get_main_queue(), ^
{
self.productDBCount = self.productDBCount + 1;
float progress = ((float)self.productDBCount / (float)self.totalCount);
int percent = progress * 100.0f;
// NSNumber *progress = [NSNumber numberWithFloat:((float)self.productDBCount / (float)self.totalCount)];
self.downloadUpdateProgress.progress = progress;
self.percentageComplete.text = [NSString stringWithFormat:#"%i", percent];
NSLog(#"Deleted product %f // ProductDBCount: %i // Percentage progress: %i // Total Count: %i", progress, self.productDBCount, percent, self.totalCount);
if (self.productDBCount == self.totalCount){
[self updatesCompleted:[jsonArray valueForKey:#"last_updated"]];
}
/*
*
* Change the completion changes to a method. Check to see if the total number of products == total count. If it does, run the completion method.
*
*/
});
});
}
}
}
Put the IF inside the dispatch, run one save at the end
// Create a new background queue for GCD
dispatch_queue_t backgroundDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
// id product = p;
// Dispatch the following code on our background queue
dispatch_async(backgroundDispatchQueue,
^{
NSManagedObjectContext *backgroundThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
for (id p in products) {
id product = p;
// Because at this point we are running in another thread we need to create a
// new NSManagedContext using the app's persistance store coordinator
NSFetchRequest *BGRequest = [[NSFetchRequest alloc] init];
NSLog(#"Running.. (%#)", product);
[BGRequest setEntity:[NSEntityDescription entityForName:#"Products" inManagedObjectContext:backgroundThreadContext]];
[BGRequest setIncludesSubentities:NO];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"codes == %#", [product valueForKey:#"product_codes"]];
[BGRequest setPredicate:predicate];
NSError *err;
NSArray *results = [backgroundThreadContext executeFetchRequest:BGRequest error:&err];
if (results.count == 0){
// Product doesn't exist with code, make a new product
NSLog(#"Product not found for add/update (%#)", [product valueForKey:#"product_name"]);
NSManagedObject* newProduct;
newProduct = [NSEntityDescription insertNewObjectForEntityForName:#"Products" inManagedObjectContext:backgroundThreadContext];
[newProduct setValue:[product valueForKey:#"product_name"] forKey:#"name"];
[newProduct setValue:[product valueForKey:#"product_codes"] forKey:#"codes"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[newProduct setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[newProduct setValue:information forKey:#"information"];
}
} else {
NSLog(#"Product found for add/update (%#)", [product valueForKey:#"product_name"]);
// Product exists, update existing product
for (NSManagedObject *r in results) {
[r setValue:[product valueForKey:#"product_name"] forKey:#"name"];
if ([product valueForKey:#"information"] == (id)[NSNull null]){
// No information, NULL
[r setValue:#"" forKey:#"information"];
} else {
NSString *information = [product valueForKey:#"information"];
[r setValue:information forKey:#"information"];
}
}
}
// Is very important that you save the context before moving to the Main Thread,
// because we need that the new object is writted on the database before continuing
// Now let's move to the main thread
dispatch_async(dispatch_get_main_queue(), ^
{
// If you have a main thread context you can use it, this time i will create a
// new one
// NSManagedObjectContext *mainThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
// [mainThreadContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
self.productDBCount = self.productDBCount + 1;
float progress = ((float)self.productDBCount / (float)self.totalCount);
int percent = progress * 100.0f;
// NSNumber *progress = [NSNumber numberWithFloat:((float)self.productDBCount / (float)self.totalCount)];
self.downloadUpdateProgress.progress = progress;
self.percentageComplete.text = [NSString stringWithFormat:#"%i", percent];
NSLog(#"Added / updated product %f // ProductDBCount: %i // Percentage progress: %i // Total Count: %i", progress, self.productDBCount, percent, self.totalCount);
NSDate *currentProcessedDate = [NSDate date];
NSTimeInterval timeSinceStarted = [currentProcessedDate timeIntervalSinceDate:self.startProcessing];
NSInteger remainingProcesses = self.totalCount - self.productDBCount;
float timePerProcess = timeSinceStarted / (float)self.productDBCount;
float remainingTime = timePerProcess * (float)remainingProcesses;
self.timeRemaining.text = [NSString stringWithFormat:#"ETA: %0.0f minutes", fmodf(remainingTime, 60.0f)];
if (self.productDBCount == self.totalCount){
[self updatesCompleted:[jsonArray valueForKey:#"last_updated"]];
}
/*
*
* Change the completion changes to a method. Check to see if the total number of products == total count. If it does, run the completion method.
*
*/
});
}
NSError *error;
if(![backgroundThreadContext save:&error])
{
NSLog(#"There was a problem saving the context (add/update). With error: %#, and user info: %#",
[error localizedDescription],
[error userInfo]);
}
});
Ok, here's your problem.
Every time you insert a record, you do a save operation to the context.
Now, don't do it, that's what takes alot of time.
Do the save operation once, in the end of the loop, not every time you insert a record.
In your case I would check what is really time consuming?
Is it downloading data, is it importing data to CoreData?
Where do you get data from? Do you download it or you have it in Application Bundle?
CoreData is faster then CSV file. So it wont make your app faster.
Some tricks:
While importing data just save context at the end of the process. Do not save context in a loop.
If you do not need to download data and can put in the bundle, you can create coredata file in the simulator, put in the bundle and copy the file on first launch. It is really much more faster then importing data.

Core Data: does a fetch have to make a trip to persistent store?

Say I do this:
NSManagedObjectContext *context = #a managed object context";
NSString *entityName = #an entity name#;
NSFetchRequest *requestForAll = [NSFetchRequest requestWithEntityName:entityName];
NSArray *allObj = [context executeFetchRequest:requestForAll];
for (NSString *name in allNamesArray){
NSFetchRequest *requestForOne = [NSFetchRequest requestWithEntityName:entityName];
requestForOne.predicate = [NSPredicate predicateWithFormat:#"name == %#",name];
NSArray *ObjsWithName = [context executeFetchRequest:requestForOne];
#do some work with the obj#
}
Does the fetch in the loop incur a trip to the persistent store every time? Or those fetches will only be performed in coredata's row cache?
EDIT
I've written a fragment of testing code :
You need to create a core data entity named "Person" and it should have an attribute named "name", which is of type string.
use this code to populate some data:
self.array = #[#"alkjsdfkllaksjdf",#"asldjflkajdklsfjlk;aj",#"aflakjsdl;kfjalksdjfklajkldhkl;aj",#"aljdfkljalksdjfl;j" ,#"flajdl;kfjaklsdjflk;j",#"akldsjfklajdslkf",#"alkdjfkljaklsdjflkaj",#"alsdjflkajsdflj",#"adlkfjlkajsdfkljkla",#"alkdjfklajslkdfj"];
NSString *firstRunKey = #"oh its first run!";
NSString *firstRun = [[NSUserDefaults standardUserDefaults] objectForKey:firstRunKey];
if (!firstRun) {
for (NSString *name in self.array) {
Person *p = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.managedObjectContext];
p.name = name;
}
}
[self.managedObjectContext save];
[[NSUserDefaults standardUserDefaults] setObject:firstRunKey forKey:firstRunKey];
[[NSUserDefaults standardUserDefaults] synchronize];
profile this two methods and you'll find usingCoreData costs much more time than usingFilterArray!
static int caseCount = 1000;
-(void)usingCoreData
{
NSLog(#"core data");
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Person"];
NSArray *allPersons = [self.managedObjectContext executeFetchRequest:request error:nil];
for (int i = 0; i < caseCount; i++){
for (NSString *name in self.array) {
request.predicate = [NSPredicate predicateWithFormat:#"name == %#",name];
NSArray *result = [self.managedObjectContext executeFetchRequest:request error:nil];
}
}
}
-(void)usingFilterArray
{
NSLog(#"filter array");
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Person"];
NSArray *allPersons = [self.managedObjectContext executeFetchRequest:request error:nil];
for (int i = 0; i < caseCount; i++){
for (NSString *name in self.array) {
NSArray *array = [allPersons filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"name == %#",name]];
}
}
}
Guess I need to answer my question myself.
I tested it and found, every time a fetch executed, core data will translate your NSFetchRequest into SQL command and invoke a data base query,the query result is firstly NSManagedObjectIDs, caching is applied to get the NSManagedObject from a NSManagedObjectID.
In conclusion, it caches object, but doesn't cache query result.
That means you execute the same NSFetchRequest for 10 times, it will query your persistent store for 10 times, event though you will get 10 times the same result. So in such situation, filtering array in memory will perform better than fetching.
The fetch will come from the specified cache when available.
EDIT:
Here's a link to a great tutorial that shows how to set up a NSFetchedResultsController that uses a cache.
http://www.raywenderlich.com/?p=999

Error: NSArray was mutated while being enumerated thrown when accessing globals and Core Data

I have this piece of code which I am using to update some values in Core Data when another object is added:
//Create new receipt
Receipt *receipt = [[Receipt alloc] init];
receipt.project = self.projectLabel.text;
receipt.amount = self.amountTextField.text;
receipt.descriptionNote = self.descriptionTextField.text;
receipt.business = self.businessNameTextField.text;
receipt.date = self.dateLabel.text;
receipt.category = self.categoryLabel.text;
receipt.paidBy = self.paidByLabel.text;
receipt.receiptImage1 = self.receiptImage1;
//Need to set this to 2
receipt.receiptImage2 = self.receiptImage1;
receipt.receiptNumber = #"99";
int count = 0;
int catCount = 0;
for (Project *p in appDelegate.projects)
{
if ([p.projectName isEqualToString:receipt.project]){
double tempValue = [p.totalValue doubleValue];
tempValue += [receipt.amount doubleValue];
NSString *newTotalValue = [NSString stringWithFormat:#"%.02f", tempValue];
NSString *newProjectName = p.projectName;
//remove entity from Core Data
NSFetchRequest * allProjects = [[NSFetchRequest alloc] init];
[allProjects setEntity:[NSEntityDescription entityForName:#"Project" inManagedObjectContext:appDelegate.managedObjectContext]];
[allProjects setIncludesPropertyValues:NO]; //only fetch the managedObjectID
NSError * error = nil;
NSArray * projectsArray = [appDelegate.managedObjectContext executeFetchRequest:allProjects error:&error];
//Delete product from Core Data
[appDelegate.managedObjectContext deleteObject:[projectsArray objectAtIndex:count]];
NSError *saveError = nil;
[appDelegate.managedObjectContext save:&saveError];
[appDelegate.projects removeObjectAtIndex:count];
NSLog(#"Removed project from Core Data");
//Insert a new object of type ProductInfo into Core Data
NSManagedObject *projectInfo = [NSEntityDescription
insertNewObjectForEntityForName:#"Project"
inManagedObjectContext:appDelegate.managedObjectContext];
//Set receipt entities values
[projectInfo setValue:newProjectName forKey:#"name"];
[projectInfo setValue:newTotalValue forKey:#"totalValue"];
if (![appDelegate.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Added Project to Core Data");
Project *tempProject = [[Project alloc] init];
tempProject.projectName = [projectInfo valueForKey:#"name"];
tempProject.totalValue = [projectInfo valueForKey:#"totalValue"];
[appDelegate.projects addObject:tempProject];
}
count++;
}
for (Category *c in appDelegate.categories){
if ([c.categoryName isEqualToString:receipt.category]){
double tempValue = [c.totalValue doubleValue];
tempValue += [receipt.amount doubleValue];
NSString *newTotalValue = [NSString stringWithFormat:#"%.02f", tempValue];
NSString *newCategoryName = c.categoryName;
//remove entity from Core Data
NSFetchRequest * allCategories = [[NSFetchRequest alloc] init];
[allCategories setEntity:[NSEntityDescription entityForName:#"Category" inManagedObjectContext:appDelegate.managedObjectContext]];
[allCategories setIncludesPropertyValues:NO]; //only fetch the managedObjectID
NSError * categoriesError = nil;
NSArray * categoriesArray = [appDelegate.managedObjectContext executeFetchRequest:allCategories error:&categoriesError];
//Delete product from Core Data
[appDelegate.managedObjectContext deleteObject:[categoriesArray objectAtIndex:catCount]];
NSError *categorySaveError = nil;
[appDelegate.managedObjectContext save:&categorySaveError];
[appDelegate.categories removeObjectAtIndex:catCount];
NSLog(#"Removed category from Core Data");
NSError * error = nil;
//Insert a new object of type ProductInfo into Core Data
NSManagedObject *categoryInfo = [NSEntityDescription
insertNewObjectForEntityForName:#"Category"
inManagedObjectContext:appDelegate.managedObjectContext];
//Set receipt entities values
[categoryInfo setValue:newCategoryName forKey:#"name"];
[categoryInfo setValue:newTotalValue forKey:#"totalValue"];
if (![appDelegate.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Added Category to Core Data");
Category *tempCategory = [[Category alloc] init];
tempCategory.categoryName = [categoryInfo valueForKey:#"name"];
tempCategory.totalValue = [categoryInfo valueForKey:#"totalValue"];
[appDelegate.categories addObject:tempCategory];
}
catCount++;
}
This code gives the error:
'...was mutated while being enumerated'.
Can anyone explain why? Also, is there a better approach to doing what I'm trying to achieve?
The error you're seeing is accurate. The problem is that you're mutating (changing) a collection while iterating over it. Basically, you're doing something of the form:
for (Project *p in appDelegate.projects) {
...
[p addObject: X]
}
This isn't allowed.
One simple solution is to make a new collection of the objects you want to add, and then add them to the original container outside of your loop. Something like:
NSMutableArray *array = [NSMutableArray array];
for (Project *p in appDelegate.projects) {
...
[array addObject:X];
}
[p addObjects:array];
By the way, did you google for the error text "was mutated while being enumerated"? I'd be surprised if you didn't find the answer for this common problem just by googling.
Also, when posting an error message, it's helpful to post the full line, not just part of it.
You are adding and removing items from appDelegate.projects while iterating over it in a for-each loop here:
[appDelegate.projects removeObjectAtIndex:count];
// ...
[appDelegate.projects addObject:tempProject];
And the same for appDelegate.categories:
[appDelegate.categories removeObjectAtIndex:count];
// ...
[appDelegate.categories addObject:tempProject];
AFAIK, in such case you should use a simple for loop, and access the array with index.

Indexing results from an NSFetchedResultsController

I'm having some issues with a mixed, indexed, searchable results set from an NSFetchedResults controller. I have it set up to store an indexed A-Z first initial for an entity, and then want it to display numeric first initials (i.e. # as the UILocalizedIndexCollation would do).
I have already written the code that saves a "firstInitial" attribute of an Artist object as NSString #"#" if the full name started with a number, and I seem to have gotten the code half working in my UITableViewController with a customised sort descriptor. The problem is that it only works until I quit/relaunch the app.
At this point, the # section from the fetched results appears at the top. It will stay there until I force a data change (add/remove a managed object) and then search for an entry, and clear the search (using a searchDisplayController). At this point the section re-ordering will kick in and the # section will be moved to the bottom...
I'm obviously missing something/have been staring at the same code for too long. Alternatively, there's a much easier way of doing it which I'm not aware of/can't find on Google!
Any help would be appreciated!
Thanks
Sean
The relevant code from my UITableViewController is below.
- (void)viewDidLoad
{
// ----------------------------------
// Various other view set up things in here....
// ...
// ...
// ----------------------------------
NSError *error;
if (![[self artistResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Failed to fetch artists: %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
- (NSFetchedResultsController *)artistResultsController {
if (_artistResultsController != nil) {
return _artistResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Artist" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *initialSort = [[NSSortDescriptor alloc]
initWithKey:#"firstInitial"
ascending:YES
comparator:^(id obj1, id obj2) {
// Various number conditions for comparison - if it's a # initial, then it's a number
if (![obj1 isEqualToString:#"#"] && [obj2 isEqualToString:#"#"]) return NSOrderedAscending;
else if ([obj1 isEqualToString:#"#"] && ![obj2 isEqualToString:#"#"]) return NSOrderedDescending;
if ([obj1 isEqualToString:#"#"] && [obj2 isEqualToString:#"#"]) return NSOrderedSame;
// Else it's a string - compare it by localized region
return [obj1 localizedCaseInsensitiveCompare:obj2];
}];
NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:initialSort, nameSort, nil]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_context
sectionNameKeyPath:#"firstInitial"
cacheName:nil];
self.artistResultsController = theFetchedResultsController;
_artistResultsController.delegate = self;
[nameSort release];
[initialSort release];
[fetchRequest release];
[_artistResultsController release];
return _artistResultsController;}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return nil;
} else {
return [[[_artistResultsController sections] objectAtIndex:section] name];
}
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return nil;
} else {
return [[NSArray arrayWithObject:UITableViewIndexSearch] arrayByAddingObjectsFromArray:
[[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]];
}
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return 0;
} else {
if (title == UITableViewIndexSearch) {
[tableView scrollRectToVisible:self.searchDisplayController.searchBar.frame animated:NO];
return -1;
}
else {
for (int i = [[_artistResultsController sections] count] -1; i >=0; i--) {
NSComparisonResult cr =
[title localizedCaseInsensitiveCompare:
[[[_artistResultsController sections] objectAtIndex:i] indexTitle]];
if (cr == NSOrderedSame || cr == NSOrderedDescending) {
return i;
}
}
return 0;
}
}
}
EDIT: Forgot to mention - my search filter is using a predicate on the fetchedResults controller, so this causes a new fetch request, like so
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
NSFetchRequest *aRequest = [_artistResultsController fetchRequest];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH[cd] %#", searchText];
// set predicate to the request
[aRequest setPredicate:predicate];
// save changes
NSError *error = nil;
if (![_artistResultsController performFetch:&error]) {
NSLog(#"Failed to filter artists: %#, %#", error, [error userInfo]);
abort();
}
}
I ended up going about fixing this a different way.
SortDescriptors seem to have issues with running a custom sort when you are also using CoreData with SQLite for your backend storage. I tried a few things; NSString categories with a new comparison method, the compare block as listed above, and refreshing the table multiple times to try and force an update with the sort criterion.
In the end, I couldn't force the sort descriptor to do an initial sort, so I changed the implementation. I set the firstInitial attribute for artists whose names began with numerics to 'zzzz'. This means that CoreData will sort this correctly (numerics last) off the bat.
After doing this, I then hardcoded my titleForHeaderInSection method to return # for the title if appropriate, as below:
if ([[[[_artistResultsController sections] objectAtIndex:section] indexTitle] isEqualToString:#"zzzz"]) return [NSString stringWithString:#"#"];
return [[[_artistResultsController sections] objectAtIndex:section] indexTitle];
Essentially this means it's sorting numbers into a 'zzzz' grouping, which should be last, and I'm just ignoring that title and saying the title is # instead.
Not sure if there's a better way to do this, but it keeps all of the sorting inside CoreData, which is probably more efficient/scalable in the long run.