Optimizing contacts deletion - ios

I have copied all the local contacts and stored them in core data,now when i use time profiler instrument,it shows me a large amount of time is being spent on deleting local contacts from my app.Can anyone suggest me any optimization techniques to improve my code and app performance.
Here is my code for deletion of contacts from core data:
+(void)deleteContacts
{
[[LoadingIndicator currentIndicator]displayActivity:#"Deleting Old contacts"];
//fetch the data from core data and delete them because you are going to sync again
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication]delegate];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSManagedObjectContext *managedObjectContext=[appDelegate managedObjectContext];
fetchRequest.entity = [NSEntityDescription entityForName:#"PersonEvent" inManagedObjectContext:managedObjectContext];
NSInteger *contactsIdentifier=1;
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"sourceflag == %d", contactsIdentifier];
NSArray * persons = [managedObjectContext executeFetchRequest:fetchRequest error:nil];
//error handling goes here
if (persons.count>0) {
for (NSManagedObject * person in persons) {
[managedObjectContext deleteObject:person];
}
}
NSError *saveError = nil;
[managedObjectContext save:&saveError];
[[LoadingIndicator currentIndicator]displayCompleted:#"Done"];
[[LoadingIndicator currentIndicator]hide];
NSLog(#"Finished deleting local contacts..");
}
The time profiler shows me 94.3% time is spent on
[managedObjectContext save:&saveError];
Any help would be appreciated
Thanks

Maybe PersonEvent entity has relationships that are configured with Cascade deletion rule? And maybe even objects on the other side of those relationships have similar deletion rules? Core Data will fire all those faults individually.
In this case you should prefetch those relationships when fetching for the objects to delete:
fetchRequest.relationshipKeyPathsForPrefetching
= #[#"relationshipName",
#"anotherRelationship",
#"anotherRelationship.subrelationship"];
If no objects are deleted in a cascade together with PersonEvent, and you just delete a lot of PersonEvent, you may try saving in batches. This will probably not reduce the total time needed to save all the deletions, but it won't lock context, persistent store coordinator, and the store file itself for one huge save.
const NSUInteger kBatchSize = 200;
NSUInteger currentCount = 0;
for (NSManagedObject *person in persons) {
currentCount++;
[context deleteObject:person];
if (currentCount == kBatchSize) {
currentCount = 0;
NSError *error;
BOOL saved = [context save:&error];
}
}
if (currentCount != 0) {
NSError *error;
BOOL saved = [context save:&error];
}

Firstly i want to say "Good Question". And appreciate the things on which you have concerns.
Answer :-
In core data managedObjectContext remains have the all changes in that same object it not update it to the actual database.
So when [managedObjectContext save:&saveError] called it starts updating it to core data database. So You can do optimisation by below way follow it & check the time profile performance.
NSError *saveError = nil;
if (persons.count>0) {
for (NSManagedObject * person in persons) {
[managedObjectContext deleteObject:person];
[managedObjectContext save:&saveError];
}
}
[[LoadingIndicator currentIndicator]displayCompleted:#"Done"];
[[LoadingIndicator currentIndicator]hide];
NSLog(#"Finished deleting local contacts..");
Hope it helps you ...!

Related

Deleting a record from Core Data

I'm having trouble deleting records from Core Data SQLite file. I want to be able to delete the corresponding record from my file when I delete a row from my table view.
Here is what I am doing after fetching all records into allContacts array
NSManagedObject *contactRecord = [allContacts objectAtIndex:arc4random() % allContacts.count];
self.managedObjectID = [contactRecord objectID];
Then called my method that prepares my contacts and then display them on the tableview.
When I delete a row from the table, I call this method
-(void)deleteContactFromFile:(contact *)deletedContact
{
NSLog(#"deleted Contact %#",deletedContact.personID);
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = appDelegate.managedObjectContext;
[context deleteObject:[context objectWithID:self.managedObjectID]];
[context save:nil];
}
The funny thing is I get a random record deleted from my core data file, but not the one I selected. I don't know how to deal with ObjectID thing for deleting a specific NSManagedObject.
If my question is not clear enough please tell me to clarify more.
You should be using an NSFetchedResultsController. It will help you to associate every index path of your table view with a specific managed object. You then do not need to fetch all data and filter through them.
For example, if you have the index path and a fetched results controller it is as easy as
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSManagedObjectContext *context = object.managedObjectContext;
[context deleteObject:object];
[context save:nil];
Note that you not need to go to your app delegate to get the managed object context.
Try this:
- (void)deleteContactFromFile:(contact *)deletedContact {
NSManagedObjectContext *context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSFetchRequest *fetchRequest = [NSFetchRequest new];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"EntityName" inManagedObjectContext:context]];
NSError *error;
NSArray *rootArray = [context executeFetchRequest:fetchRequest error:&error];
for (NSManagedObject *object in rootArray) {
if ([context objectWithID:self.managedObjectID]) {
[context deleteObject:object];
}
}
}

Finding duplicate values in core data

i'm inserting new objects into the database by core data. Is there any way to check if there is any duplicate in the database before i insert the values in?
AccountDetails * newEntry = [NSEntityDescription insertNewObjectForEntityForName:#"AccountDetails" inManagedObjectContext:self.managedObjectContext];
newEntry.acc_date=date;
newEntry.bank_id=bank_id1;
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
[self.view endEditing:YES];
everytime i run the app , it reinsert the values again. i want to check if there is any new category in it if there isnt then i will add that new one in only.
thanks in advance..
You can fetch or you can count. Counting is much faster than fetching. Depends on what you are trying to do.
If you just want to insert new and skip duplicates then use -[NSManagedObjectContext countForFetchRequest: error:] to determine if the object exists.
You can pre-build the predicate and just replace the unique value on each loop so that even the cost of the predicate is low. This is fairly performant but not the best solution because it hits the disk on each loop.
Another option would be to change the fetch to have:
Just the unique value
A NSDictionary result
Then grab all of the unique values from your insertable array into an array of strings (for example) then do a single fetch with:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"myUnique in %#", uniqueIDArray]];
Then you have an array of uniques that ARE in the store already. From there as you loop over your objects you check against that array, if the unique in there you skip, otherwise you insert. That will yield the best performance for a straight insert or skip requirement.
You need to fetch from the db and check, your code will be doing something like this helper method I use frequently in my code, if the results.count is > 1, then DUPLICATE found :
- (NSManagedObject*) findOrCreateObjectByValue:(id)value
propertyName:(NSString*)propertyName
entityName:(NSString*)entityName
additionalInfo:(NSDictionary*)additionalInfo
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error
{
NSManagedObject* res = nil;
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:entityName];
[r setPredicate:[NSPredicate predicateWithFormat:#"%K == %#",propertyName,value]];
NSArray* matched = [context executeFetchRequest:r
error:error];
if (matched) {
if ([matched count] < 2) {
res = [matched lastObject];
if (!res) { //No existing objects found, create one
res = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];
[res setValue:value
forKey:propertyName];
}
} else {
if (error) {
*error = [NSError errorWithDomain:#"some_domain"
code:9999
userInfo:#{#"description" : #"duplicates found"}];
}
}
}
return res;
}

Coredata entity with exactly one row

One of my coredata tables (uh, entities) should have ever only one row of data being stored. When the row doesn't exist yet it should be created and if it already exists, the same row should be overwritten (or edited) with new data.
Currently in my implementation a new row is always added to the entity (named 'TempNames'):
/* Store names data in temporary name table. */
TempNames *tempNames = [NSEntityDescription insertNewObjectForEntityForName:#"TempNames" inManagedObjectContext:context];
tempNames.namesData = tempNamesData;
Can anyone give me some hints what is needed to change it to my desired functionality? I suppose NSPredicate is required to achieve what I want?
UPDATED WORKING IMPLEMENTATION:
/* Convert names array into serializable data. */
NSData *tempNamesData = [NSKeyedArchiver archivedDataWithRootObject:names];
/* Store names data in temporary name table. */
TempNames *tempNames = nil;
NSError *error;
NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName:#"TempNames"];
NSArray *records = [context executeFetchRequest:req error:&error];
/* Record already exists. */
if (records.count > 0)
{
tempNames = records.firstObject;
}
else
{
tempNames = [NSEntityDescription insertNewObjectForEntityForName:#"TempNames" inManagedObjectContext:context];
}
tempNames.namesData = tempNamesData;
[context save:&error];
If you insert a new entity every time, you are not doing an insert/update operation, it's always an insert operation, even if the data of that entity is the same. What you should do is first fetch if you have that data in your store and then decide if you need to insert or update it.
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"TempNames"];
NSError *error;
NSArray *entities = [context executeFetchRequest:request error:&error];
if (entities.count > 0) {
// You have already inserted the entity
}
else {
// Insert your new entity
...
// Save changes to the store
NSError *error;
[context save:&error];
}
Try it like this:
TempNames *tempNames = nil;
NSFetchRequest *appRequest = [NSFetchRequest fetchRequestWithEntityName:#"TempNames"];
NSArray *allNames = [context executeFetchRequest:appRequest error:nil];
if (allNames.count > 0) {
// your record exists
tempNames = allNames.firstObject;
} else {
tempNames = [NSEntityDescription insertNewObjectForEntityForName:#"TempNames" inManagedObjectContext:context];
}
tempNames.namesData = tempNamesData;

How does this code use only updates into core data?

This is a SyncEngine from an RW tutorial. I need help understanding how only UPDATED records from the web are fetched and processed into Core Data.
- (void)processJSONDataRecordsIntoCoreData {
NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
// Iterate over all registered classes --- CHECK!
for (NSString *className in self.registeredClassesToSync) {
if (![self initialSyncComplete]) {
NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
NSArray *records = [JSONDictionary objectForKey:#"results"];
for (NSDictionary *record in records) {
[self newManagedObjectWithClassName:className forRecord:record];
}
} else {
NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:#"objectId"];
if ([downloadedRecords lastObject]) {
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:#"objectId" usingArrayOfIds:[downloadedRecords valueForKey:#"objectId"] inArrayOfIds:YES];
int currentIndex = 0;
//if dl count is < current index, there is an updated object dl from the web
for (NSDictionary *record in downloadedRecords) {
NSManagedObject *storedManagedObject = nil;
//Quick check to see if they indeed match, if they do, update the stored object with remote service objects
if ([storedRecords count] > currentIndex) {
storedManagedObject = [storedRecords objectAtIndex:currentIndex];
}
//Othwerwise its a new object and you need to create a new NSManagedObject to represent it in CDdb
if ([[storedManagedObject valueForKey:#"objectId"] isEqualToString:[record valueForKey:#"objectId"]]) {
[self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record];
} else {
[self newManagedObjectWithClassName:className forRecord:record];
}
currentIndex++;
}
}
}
// After all NSMO are created in your context, save it!
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"Unable to save context for class %#", className);
}
}];
// Cleanup time
[self deleteJSONDataRecordsForClassWithName:className];
[self executeSyncCompletedOperations];
}
[self downloadDataForRegisteredObjects:NO];
}
From what I understand, on the first or initial sync, it fetches JSONDictionaryForClassWithName which reads the downloaded data from disk and creates a newManagedObjectWithClassName.
My confusion is in the update else block. downloadedRecords is populated from JSONDataRecordsForClass which simply calls JSONDictionaryForClassWithName. Then it checks to see if there is at least 1 object in that array. If there is it does this:
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:#"objectId" usingArrayOfIds:[downloadedRecords valueForKey:#"objectId"] inArrayOfIds:YES];
This fetches all managedObjectsForClass:sortedByKey which is below:
- (NSArray *)managedObjectsForClass:(NSString *)className sortedByKey:(NSString *)key usingArrayOfIds:(NSArray *)idArray inArrayOfIds:(BOOL)inIds {
__block NSArray *results = nil;
NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className];
NSPredicate *predicate;
if (inIds) {
predicate = [NSPredicate predicateWithFormat:#"objectId IN %#", idArray];
} else {
predicate = [NSPredicate predicateWithFormat:#"NOT (objectId IN %#)", idArray];
}
[fetchRequest setPredicate:predicate];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:
[NSSortDescriptor sortDescriptorWithKey:#"objectId" ascending:YES]]];
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
}];
return results;
}
The next bit which compares the [storedRecords count] > currentIndex is confusing. Can someone please explain this? I think my confusion lies in what the managedObjectsForClass method does with the usingArraysOfIds & inArrayOfIds.
I would expect that at some point it gets the the updatedAt field from the downloaded records and compares it to the updatedAt field of the CoreData fetched records.
This function is processing the stored JSON. The actual remote fetching and updateAt checking happens in downloadDataForRegisteredObjects and mostRecentUpdatedAtDateForEntityWithName.
[storedRecords count] > currentIndex is a bit crazy. Although in defense of the original programmer, writing any decent syncengine will quickly make you go googoo. Basically he needs to work out which records are existing and which ones are new and update the local data store accordingly, that's all.
I had another look and this code is actually horribly broken. It will only works if either you have the same records both locally and remotely. Or if the new objects have an objectId that sort-wise comes after the last object the local store has. Which is not the case with Parse objectId's.
If you are testing with just one device this works because new objects will be inserted locally before being pushed to the server. Therefor you will always have the same amount of records. If additional records get inserted any other way, this code will do weird things.

Checking for duplicates when importing to CoreData

I'm importing data into a Core Data store using RestKit and need to check for duplicates. If the item is already in the store, I'd like to update it with the latest attributes. If it's a new item, I'd like to create it.
The import was slow so I used Instruments and saw that the longest part of importing was checking to see if the item already exists (with a fetch request)
So I'd like to know if checking to see if the item is already in the store, is it faster to:
use countForFetchRequest to see if the item already exists, then executeFetchRequest to return the item to update or
just executeFetchRequest to get the item to update
or is there a better way to do this?
I thought countForFetchRequest would be faster since the entire NSManagedObject isn't returned and only execute the fetch request if I know there's going to be a NSManagedObject.
Thanks
- (Product *)productWithId:(int)productID {
NSManagedObjectContext *context = [Model sharedInstance].managedObjectContext;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"product_id == %d", productID];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Product" inManagedObjectContext:context];
request.predicate = predicate;
request.fetchLimit = 1;
NSError *error = nil;
NSUInteger count = [context countForFetchRequest:request error:&error];
if (!error && count == 1) {
NSArray *results = [context executeFetchRequest:request error:&error];
if (!error && [results count]) {
return [results objectAtIndex:0];
}
return nil;
}
return nil;
}
As far I know, the best way to find and/or import objects within Core Data is described in Implementing Find-or-Create Efficiently.
The documentation describes a find or create pattern that it's based on sorting data: the data you download from the service and the data you grab form the store.
I really suggest you to read the link I provided. You will see a speed up on your performances.
Obviously you should do the work in background, preventing the main thread to freeze, using thread confinement or new iOS Core Data queue API.
Hope that helps.

Resources