7

Ok. This is the exact same question as here: Why is NSFetchedResultsController loading all rows when setting a fetch batch size?

But the solution for his doesn't solve for mine.

I have a screen that has several thousand records and is slow to load them all. I set the batch size to 30 (roughly three times the cells on the screen) and it still loops through and loads all the batches for some reason.

Here is the code

- (NSFetchedResultsController *)guestCardFetchedResultsController
{
    if (guestCardFetchedResultsController != nil) {
        return guestCardFetchedResultsController;
    }

    // SELECT * from GuestCard
    NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"GuestCard" inManagedObjectContext:self.context];
    [fetchRequest setEntity:entity];
    // ORDER BY updated DESC
    NSSortDescriptor* updatedSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"created" ascending:NO];
    [fetchRequest setSortDescriptors:@[updatedSortDescriptor]];
    fetchRequest.fetchBatchSize = 30;
    NSString *cacheName = self.isReportProblemView ? @"reportProblemGuestCardsAll" : @"guestCardsAll";

    [NSFetchedResultsController deleteCacheWithName:cacheName];
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.context sectionNameKeyPath:@"sectionIdentifier" cacheName:cacheName];
    aFetchedResultsController.delegate = self;
    self.guestCardFetchedResultsController = aFetchedResultsController;

    // Clean up

    NSError *error = nil;
    if (![[self guestCardFetchedResultsController] performFetch:&error]) {
    }

    return self.guestCardFetchedResultsController;
}

I'm not doing anything terribly interesting in this scenario. Here's some of the delegate code (excluding the cell creating, which I confirmed only is called for the number of cells on screen):

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    if ([self.guestCardFetchedResultsController.fetchedObjects count] == 0) {
        return 1;
    }
    // Return the number of sections.
    return [[self.guestCardFetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if ([self.guestCardFetchedResultsController.fetchedObjects count] == 0) {
        return 1;
    }
    // Return the number of rows in the section.
    id <NSFetchedResultsSectionInfo> sectionInfo = [guestCardFetchedResultsController sections][section];
    return [sectionInfo numberOfObjects];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    if ([self.guestCardFetchedResultsController.fetchedObjects count] == 0) {
        return @"";
    }

    return [[self.guestCardFetchedResultsController sections][section] name];
}
Community
  • 1
  • 1
Bob Spryn
  • 17,742
  • 12
  • 68
  • 91
  • Is your entity `created` property a method on your custom entity object (meaning, did you define a method `-(*whatever*)created{}` in the class implementation) or a basic attribute? Same question for `sectionIdentifier` on the entity – RyanR Oct 19 '13 at 17:14

3 Answers3

8

After reading the docs on the NSFetchedResultsController initializer, I think the problem is that you have a sort in the fetch request (created) that doesn't naturally sort the same as the section key path (sectionIdentifier). The specific sentence in the docs I'm looking at says:

If this key path is not the same as that specified by the first sort descriptor in fetchRequest, they must generate the same relative orderings

I recommend modifying your fetch request to sort on sectionIdentifier first, then created. I think that'll fix your issue.

 NSSortDescriptor* updatedSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"created" ascending:NO];
 NSSortDescriptor* sectionSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"sectionIdentifier" ascending:NO];
 // It's critical the sectionSortDescriptor is first
 [fetchRequest setSortDescriptors:@[sectionSortDescriptor, updatedSortDescriptor]];

Note that if either the created or sectionIdentifier properties are actually methods on your entity class, that will definitely force Core Data to load all your data before it can sort/section, because it needs to execute that method on each entity first.

RyanR
  • 7,728
  • 1
  • 25
  • 39
  • 1
    Dammit. That was it. Not sure how I missed that over and over. sectionIdentifier IS a method because I want to section by the whole day, not the exact timestamp. Now, to figure out how to do that... Thanks Ryan! – Bob Spryn Oct 19 '13 at 21:29
  • 1
    I came across the case that I thought different fields have the same orderings. However, in some cases they are not. Even though I read the doc over and over again to try to catch what went wrong to cause the crash when there were more than 6 items in my tableView. Your emphasis on this saved my day. Wasted almost 3 days on this already. Thanks with upvoting. – LiweiZ Nov 17 '14 at 20:17
  • @Bob Spryn, did you ever find a solution to this? I am trying to do the exact same thing and all batches are loaded initially. I have a key path which is a transient property to get sections into Dates. Oddly though even setting nil for key path section also loads all batches. – alionthego Jun 07 '18 at 04:10
5

When using fetchBatchSize rather than returning the objects in a NSArray it returns a special batch faulting array. Although it does load in all the object IDs all the objects in the array are faults, meaning their data was not loaded, i.e. like a placeholder object. Then as you loop the array (or the table reload does) when access an object's property, it performs the database query for that object's properties including the next objects up to the fetchBatchSize (the minimum appears to be 4 by the way). The best way to debug this internal behaviour by seeing the queries is to edit your scheme and add a run argument:

-com.apple.CoreData.SQLDebug 1

That will output to console the various queries. You'll see the first query for the row IDs, then each batch loading of data.

So what you might be experiencing is it being slow to load in all the row IDs and creating the faulted objects, however that should be fast.

Another possibility is when it accesses the sectionIdentifier property to build the sections, that causes the object to be loaded in too, and thus causes all batches to load. To solve that you could try setting propertiesToFetch to include the sectionIdentifier, that way the first query should load in that property and then when it is accessed the object shouldn't be loaded in.

malhal
  • 26,330
  • 7
  • 115
  • 133
-1

I've had the same question and in the end I concluded, that it works as it should.

When you set fetchBatchSize fetchRequest loads all data to cache because otherwise this parameter would be useless. I guess it's used for using data just after loading. But when you getting object as [fetchResultController objectAtIndexPath:indexPath] it is fulfilled by ManagedObjectContext (I suppose), not by fetchRequest.

So you can set fetchBatchSize to 0 and every time you access fault it will be requested from persistent store. Or you can set other value and also set fetchLimit and fetchOffset every time you scroll table view. But you still have to know overall objects number.

pakaz
  • 21
  • 5
  • No this is not correct. In many other cases it works as it should. This one instance it does not. When you set a batch size, it fetches the count of all rows, but should only grab the data for the first batch. – Bob Spryn Oct 18 '13 at 21:30
  • You're right, in some other cases is works. But there's only one case where I use `sectionNameKeyPath`. Setting it to nil fixes the problem, but it's not what I want. – pakaz Oct 19 '13 at 08:05
  • Changing `sectionNameKeyPath` could help. In my case, I set "section.order" instead of "self.section.order" and it solved the problem. – pakaz Oct 19 '13 at 08:49