I am using a NSFetchResultsController to display 100,000 + records in a UITableView. This works but it is SLOW, especially on an iPad 1. It can take 7 seconds to load which is torture for my users.
I'd also like to be able to use sections but this adds at least another 3 seconds onto the laod time.
Here is my NSFetchResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (self.clientsController != nil) {
return self.clientsController;
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Client" inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
[request setPredicate:[NSPredicate predicateWithFormat:#"ManufacturerID==%#", self.manufacturerID]];
[request setFetchBatchSize:25];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"UDF1" ascending:YES];
NSSortDescriptor *sort2= [[NSSortDescriptor alloc] initWithKey:#"Name" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObjects:sort, sort2,nil]];
NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:#"Name", #"ManufacturerID",#"CustomerNumber",#"City", #"StateProvince",#"PostalCode",#"UDF1",#"UDF2", nil];
[request setPropertiesToFetch:propertiesToFetch];
self.clientsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
return self.clientsController;
}
I have an index on ManufacturerID which is used in my NSPredicate. This seems like a pretty basic NSFetchRequest - anything I can do to speed this up? Or have I just hit a limitation? I must be missing something.
First: you can use the NSFetchedResultsController's cache to speed up display after the first fetch. This should quickly go down to a fraction of a second.
Second: you can try to display the only the first screenful and then fetch the rest in the background. I do this in the following way:
When the view appears, check if you have the first page cache.
If not, I fetch the first page. You can accomplish this by setting the fetch request's fetchLimit.
In case you are using sections, do two quick fetches to determine the first section headers and records.
Populate a second fetched results controller with your long fetch in a background thread.
You can either create a child context and use performBlock: or
use dispatch_async().
Assign the second FRC to the table view and call reloadData.
This worked quite well in one of my recent projects with > 200K records.
I know the answer #Mundi provided is accepted, but I've tried implementing it and ran into problems. Specifically the objects created by the second FRC will be based on the other thread's ManagedObjectContext. Since these objects are not thread safe and belong to their own MOC on the other thread, the solution I found was to fault the objects as they are being loaded. So in cellForRowAtIndexPath I added this line:
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
object = (TapCellar *)[self.managedObjectContext existingObjectWithID:[object objectID] error:nil];
Then you have an object for the correct thread you are in. One further caveat is that the changes you make to the objects won't be reflected in the background MOC so you'll have to reconcile them. What I did was make the background MOC a private queue MOC and the foreground one is a child of it like this:
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:coordinator];
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_privateManagedObjectContext];
}
Now when I make changes in the main thread, I can reconcile them easily by doing this:
if ([self.managedObjectContext hasChanges]) {
[self.managedObjectContext performBlockAndWait:^{
NSError *error = nil;
ZAssert([self.managedObjectContext save:&error], #"Error saving MOC: %#\n%#",
[error localizedDescription], [error userInfo]);
}];
}
I wait for it's return since I'm going to reload the table data at this point, but you can choose not to wait if you'd like. The process is pretty quick even for 30K+ records since usually only one or two are changed.
Hope this helps those who are stuck with this!
Related
My situation is more than 200 objects would come to me one by one per second through a callback function, obviously I cannot save a entity every single time in this callback function, that will cause UI suspend, so my question is how to insert batch data with core data?
------------------------------------------------------------
Edit:
thanks, guys, I re-edit my code like this, and there are some issues,
if I receive "1", "2", "3"...from the callback function frequently, my tableview often shows "1", "3", "2"... , they are in wrong order. if anything wrong, plz let me know.
in AppDelegate.m file:
- (NSManagedObjectContext *)mainManagedObjectContext {
if (_ mainManagedObjectContext != nil) {
return _managedObjectContext;
}
_mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainManagedObjectContext.parentContext = [self rootManagedObjectContext];
return _mainManagedObjectContext;
}
- (NSManagedObjectContext*)rootManagedObjectContext {
if (_rootManagedObjectContext != nil) {
return _rootManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_rootManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_rootManagedObjectContext setPersistentStoreCoordinator:coordinator];
return _rootManagedObjectContext;
}
in the callback function I do this, I tried to replace _mainManagedObjectContext
with _rootContext or _workContextint callback function, but data never show on the tableview;
- (void)saveChatMessage:(ChatMessage*)msg userInfo:(UserInfo*)user chatType:(ChatType)chatType receiverID:(long long)receiverID receiverName:(NSString*)receiverName
{
ChatObject *chatObject = [NSEntityDescription
insertNewObjectForEntityForName:#"ChatObject"
inManagedObjectContext: _mainManagedObjectContext];
chatObject.receiveTime = [NSNumber numberWithLongLong:[NSDate date].timeIntervalSince1970];
chatObject.text = msg.text;
chatObject.richText = msg.richText;
chatObject.senderID = [NSNumber numberWithLongLong:user.userID];
chatObject.senderName = user.userName;
chatObject.chatType = [NSNumber numberWithInteger:chatType];
chatObject.isFromHost = [NSNumber numberWithBool:user.isOrganizer];
chatObject.receiverID = [NSNumber numberWithLongLong:receiverID];
chatObject.receiverName = receiverName;
NSError *error;
if (![_workerContext save:&error]) {
}
if (![_rootContext save:&error]) {
}
}
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"ChatObject" inManagedObjectContext:_mainManagedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"receiveTime" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_mainManagedObjectContext sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
You have to use background contexts. I recommend this setup:
RootContext (background) for saving --> is parent of
MainContext (main thread) for UI --> is parent of
WorkerContext (background) for insert, update, delete operations
Root context and main context should be set up at app start. You can create a worker context in your web callbacks (when you have the new data available) and then use the context with the block APIs:
workerContext.performBlock() {
// update data here
}
When you call save() on the worker context, the UI context gets the changes and can update itself gracefully (e.g. via NSFetchedResultsControllerDelegate). Make sure you also call save on the root context to persist the data to the persistent store.
This is the basic outline of a good approach. In case you are not familiar with some of the mentioned techniques, you can find good and detailed explanations of many of these on this platform.
You can insert all data in a background thread and save the context when you are done - that will update the context in other threads. Just take care that all saving is done on that background thread to avoid concurrency problems.
My question is similar to the following one but has differences
I use Core Data. The problem is if I use the current NSManagedObjectContext then I may get an incorrect result. For example:
From the beginning the db is filled.
My app deletes all the entities via the current context.
I get check if db filled and get NO but in reality the db is still filled because the context should be saved to apply the changes.
And example of my solution:
- (BOOL)isDBFilled {
//separate context to
NSManagedObjectContext *context = [[[NSManagedObjectContext alloc] init] autorelease];
[context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[context setPersistentStoreCoordinator:coordinator];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:... inManagedObjectContext:context];
[request setEntity:entity];
[request setFetchLimit:1];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
if (!results) {
LOG(#"Fetch error: %#", error);
abort();
}
if ([results count] == 0) {
return NO;
}
return YES;
}
So is it normal and safe to create another context just to check if db is filled? Or Is there better ways to perform this checking except of creating a separate BOOL variable which will be changed before db changes and after them?
It is fine to use a separate context to check if the database is populated or not. The overhead is minimal, so I would have no objections.
Please note that Apple does not necessarily agree. From the "Core Data Snippets":
Neither should a view controller create a context for its own use (unless it’s a nested context).
That being said, I do not see the need for this. If you delete all entities, save and then check should get an empty database. If you delete all entities and do not save and then check, you should get a non-empty database. You can easily - and more easily - do this with one context.
PS: autorelease seems to indicate that you are using a very old SDK. How about upgrading?
I have to fetch data from multiple entities which are associated to different screens of an iPhone App, now the scenario is as follows, when user press a Sync button i will have to fetch data from all these entities (i have about 12 entities) and send all this data to a server via a web service, and all these entities have no relationship among them, now my question is what is the best approach to do this task, should i write 12 different fetch requests in a single method, or is there some other better approach, if any one can guide with some tutorial link, that will me most appreciated, thanx in advance.
You can use a for loop to accomplish your task like this. I've used this code to delete all entries of the Database.
NSArray *allEntities = [[[[UIApplication sharedApplication] delegate] managedObjectModel] entities];
NSError *error;
for (NSEntityDescription *entityDescription in allEntities)
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entityDescription];
fetchRequest.includesPropertyValues = NO;
fetchRequest.includesSubentities = NO;
NSArray *items = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(#"Error requesting items from Core Data: %#", [error localizedDescription]);
}
//Do whatever you need to do here
}
I am only seeing the saved context after I restart my app. I have two view controllers, one that allows a user to save a core data record and the other to display. When I save a new record, then go to the table view to see all the records created, I do not see the new record. This is how I create a record:
NSManagedObjectContext *context = [self managedObjectContext];
Property *myProperty = [NSEntityDescription
insertNewObjectForEntityForName:#"Property"
inManagedObjectContext:context];
myProperty.aptDescription = curProperty.description;
NSError *error;
[context save:&error];
[appDelegate saveContext];
And then i retreive it doing this:
ATAppDelegate *appDelegate = (ATAppDelegate *)[[UIApplication sharedApplication]delegate];
_managedObjectContext = [appDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Property" inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:entity];
NSError *error;
self.myProperties = [_managedObjectContext executeFetchRequest:fetchRequest error:&error];
But like I said the new object is not shown when I go back into the table view immediately after creating the record. What is the issue?
Sounds like you are not implementing the delegate methods of the NSFetchedResultsController properly. Although you did not share any of your view code so it is impossible to be certain.
In addition, after a save you are not checking the BOOL return from the -save:. You should always, always, always check that BOOL and at least log the error. You could be throwing an error and never know it.
The code below does not delete the entity. The "delete was successful" message appears on the console so the entity is found. All other operations I use succeed.
I am using RestKit 0.20.
NSManagedObjectContext *context = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
NSError *error = nil;
NSFetchRequest * fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity: [NSEntityDescription entityForName:#"Auction" inManagedObjectContext:context]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"AuctionID = %d", auctionID];
[fetchRequest setPredicate:predicate];
NSArray *result = [context executeFetchRequest:fetchRequest error:&error];
if(result.count) {
Auction *block = result[0];
[context deleteObject:block];
BOOL status = [context save:&error];
if (status == NO) {
NSLog(#"delete falied for AuctionID:%d, error: %#", auctionID, error);
}
else {
[context processPendingChanges];
NSLog(#"delete was successful for AuctionID:%d", auctionID);
}
}
Why might this delete operation not succeed and what is the solution to making it work.
I found this solution :
In fact, you have to fetch datas from the persistentstore and not the current created managed context :
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"MyModel"];
NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:#"id" ascending:NO];
fetchRequest.sortDescriptors = #[descriptor];
// Setup fetched results
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
// AND TO DELETE A MODEL :
[[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext deleteObject:myobject];
i am doing the same thing and have nearly same code.
In my code also, i get delete done and saved....
But, its not deleted when i am checking DB.
the problem is not with simulator... SURE bcz i am getting same problem on device also.
there is something called child context, it might be the cause...Check these links http://restkit.org/api/0.20.0-dev/Classes/RKManagedObjectRequestOperation.html#//api/name/managedObjectContext
RestKit 0.20 — What is the preferred way to create a new NSManagedObject?
. If you found solution pls share here
#Sumitiscreative I ran into the same issue today. What if found was that normally using Core Data you have to use
[NSManagedObject save:]
for it to store the changes. I dug through Restkit a bit and found this
[[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext deleteObject:(NSManagedObject *)];
[[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext saveToPersistantStore:(NSError *)];
Calling this after the above delete method is working to remove the object out of the DB.
**Edit - Also I would have just made this a comment but i don't have the option
#Lance
Hey, pls update your restkit with the latest version.
As now, this works in the latest version , if your server related configuration is correct. and if you get success codes for your delete request from server. Then, restkit automatically deletes the data.
If you need to delete any data externally then, you can use persistentStoreManagedObjectContext and after deleting, save it.
Also, if you want to check at your end that whether its correctly deleting via restkit or not. what you can do is ...
make delete request, after success
check with same id, if item exists. (just for help)