Результаты поиска по страницам из запросов основных данных

#objective-c #ios #core-data

#objective-c #iOS #основные данные

Вопрос:

У меня есть относительно простая база данных core data sqlite. Я пытаюсь получать результаты из БД по одной странице за раз.

 
    NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:[...]];

    [request setPredicate:[NSPredicate predicateWithFormat:@"flaggedTime != nil"]];

    NSSortDescriptor* sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"flaggedTime" ascending:NO];
    [request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];

    [request setFetchLimit:pageSize];
    [request setFetchOffset:((pageIndex - 1) * pageSize)];    

    NSArray* results = [self.context executeFetchRequest:request error:NULL];
  

Размер страницы равен 30, индекс страницы для тестовых данных равен 1, 2, 3 или 4 (в БД около 80 элементов, поэтому индекс страницы = 4 не должен возвращать никаких элементов).
Предикат и сортировка работают нормально, результаты успешно возвращаются. Ограничение выборки тоже работает нормально. Ошибки не возвращаются.

Проблема: я всегда получаю результаты с первой страницы, как если бы fetchOffset не был установлен. Я пытался удалить предикат и сортировку, но безрезультатно. Единственная ситуация, когда я мог заставить fetchOffset работать, это когда я использовал значения меньше 30. Конечно, это бессмысленно для разбиения на страницы…

Кто-нибудь знает, почему? Я буду очень благодарен за каждый ответ.

Обновление: я говорю об iOS. Протестировано на 4.2 и 5.0.

Обновление 2: для упрощения проблемы.

 
    NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:[...];

    NSError* error = nil;

    NSManagedObjectContext* context = [...];

    NSUInteger count = [context countForFetchRequest:request error:amp;error];
    assert(error == nil);

    NSLog(@"Total count: %u", count);

    request.fetchOffset = 0;    
    request.fetchLimit = 30;

    NSLog(@"Fetch offset: %u, limit: %u", request.fetchOffset, request.fetchLimit);

    NSArray* page1 = [context executeFetchRequest:request error:amp;error];
    assert(error == nil);

    NSLog(@"Page 1 count: %u", page1.count);

    request.fetchOffset = 30;    
    request.fetchLimit = 30;

    NSLog(@"Fetch offset: %u, limit: %u", request.fetchOffset, request.fetchLimit);

    NSArray* page2 = [context executeFetchRequest:request error:amp;error];
    assert(error == nil);

    NSLog(@"Page 2 count: %u", page2.count);
  

дает:

Общее количество: 34
Смещение выборки: 0, ограничение: 30
Количество страниц 1: 30
Смещение выборки: 30, ограничение: 30
Количество страниц 2: 30 (ОШИБКА: должно быть 4)

Комментарии:

1. Можем ли мы увидеть код, в котором вы устанавливаете pageSize и pageIndex? Кроме того, код, в котором вы отображаете результаты?

2. Это не имеет значения. Проблема здесь. Я понимаю, что вы мне не верите и ожидаете найти простое решение где-то в другом месте, но параметры, которые входят в метод, — это то, что я написал выше. Но мне любопытно — у вас действительно есть рабочий код, который использует как fetchLimit, так и fetchOffset?

3. У меня есть рабочий код, который использует fetchLimit, fetchOffset и sortDescriptors — так же, как и вы. и мой код работает нормально.

4. Может ли быть разница при открытии базы данных или настройке запроса? Я знаю, что в документах указано «Смещения игнорируются во вложенных запросах, таких как подзапросы». но я думаю, что мой запрос действительно прост.

5. @Sulthan Возможно, вам придется предоставить общий доступ к вашему файлу momd, чтобы помочь воспроизвести ошибку.

Ответ №1:

Я только что создал демонстрационный проект, который пытается воссоздать ваш сценарий в его простейшей форме. Я создал пустой проект, добавил 34 объекта, затем запросил его с помощью того же кода, который вы указали выше. Ниже приведен пример:

 CDAppDelegate * delegate = (CDAppDelegate*)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [delegate managedObjectContext];
for(int i = 0; i < 34;i  ){
    CDObject * object = [NSEntityDescription insertNewObjectForEntityForName:@"CDObject"
                                                      inManagedObjectContext:context];
    [object setValue:i];
}
[delegate saveContext];

NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:[NSEntityDescription entityForName:@"CDObject"
                               inManagedObjectContext:context]];

 NSError* error = nil;

 NSUInteger count = [context countForFetchRequest:request error:amp;error];
 assert(error == nil);

 NSLog(@"Total count: %u", count);

 request.fetchOffset = 0;    
 request.fetchLimit = 30;

 NSLog(@"Fetch offset: %u, limit: %u", request.fetchOffset, request.fetchLimit);

 NSArray* page1 = [context executeFetchRequest:request error:amp;error];
 assert(error == nil);

 NSLog(@"Page 1 count: %u", page1.count);

 request.fetchOffset = 30;    
 request.fetchLimit = 30;

 NSLog(@"Fetch offset: %u, limit: %u", request.fetchOffset, request.fetchLimit);

 NSArray* page2 = [context executeFetchRequest:request error:amp;error];
 assert(error == nil);

 NSLog(@"Page 2 count: %u", page2.count);

[request release];
  

Журнал выглядит так, как показано:

 2011-11-04 14:53:04.530 CDCoreDataTest[77964:207] Total count: 34
2011-11-04 14:53:04.531 CDCoreDataTest[77964:207] Fetch offset: 0, limit: 30
2011-11-04 14:53:04.532 CDCoreDataTest[77964:207] Page 1 count: 30
2011-11-04 14:53:04.533 CDCoreDataTest[77964:207] Fetch offset: 30, limit: 30
2011-11-04 14:53:04.533 CDCoreDataTest[77964:207] Page 2 count: 4
  

Используя ваш код, я смог запустить его и запустить без проблем. Это было сделано с iOS 5.0, запущенной на симуляторе. Ваш код выглядит правильным для меня, потому что вы пытаетесь выполнить, поэтому должно быть что-то, что происходит с запросом выборки или самим контекстом…

Комментарии:

1. Я думаю, что проблема OP заключается в концептуальной путанице с «страницей» и «подкачкой». В этой подразумеваемой модели данных или явном коде нет ничего по сути похожего на страницу. Он не «просматривает страницы» через что-либо, поскольку этот термин обычно используется в программировании.

2. Все еще пытаюсь воспроизвести проблему на вновь созданной модели. Пожалуйста, наберитесь терпения.

Ответ №2:

pageOffset Свойство NSFetchRequest является (иногда?) игнорируется при выполнении запросов на выборку в несохраненном контексте. Обратите внимание, что в коде по OP контекст никогда не сохраняется, в то время как во фрагменте кода, прикрепленном к ответу @kcharwood, он фактически сохраняется. Вот пример:

 - (void) printArrayOfTestEntities:(NSArray *)array{
    NSMutableString * s = [NSMutableString new];
    NSArray * numbers = [array valueForKey:@"someField"];
    for (NSNumber * number in numbers){
        [s appendString:[NSString stringWithFormat:@"%d ", [number intValue]]];
    }

    NSLog(@"fetched objects: %@ rcount: %d", s, array.count);
}

/* example itself */

NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:persistentStoreCoordinator];

for(int i = 0; i < 34;i  ){
    NSManagedObject * object = [NSEntityDescription insertNewObjectForEntityForName:@"TestEntity"
                                                      inManagedObjectContext:context];
    [object setValue:@(i) forKey:@"someField"];
}

NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"TestEntity"];
request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"someField" ascending:YES]];
request.fetchLimit = 30;

NSArray * result = [context executeFetchRequest:request error:nil];
[self printArrayOfTestEntities:result];

request.fetchOffset = 30;
result = [context executeFetchRequest:request error:nil];
[self printArrayOfTestEntities:result];

[context save:amp;error];

request.fetchOffset = 0;
result = [context executeFetchRequest:request error:nil];
[self printArrayOfTestEntities:result];

request.fetchOffset = 30;
result = [context executeFetchRequest:request error:nil];
[self printArrayOfTestEntities:result];
  

Журнал:

 2014-03-01 11:30:06.986 coredatatest[19771:70b] fetched objects: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29  
count: 30
2014-03-01 11:30:06.990 coredatatest[19771:70b] fetched objects: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29  
count: 30
2014-03-01 11:30:06.995 coredatatest[19771:70b] fetched objects: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29  
count: 30
2014-03-01 11:30:06.997 coredatatest[19771:70b] fetched objects: 30 31 32 33   
count: 4
  

Комментарии:

1. Вопрос давно назрел, и я считаю, что тогда это была актуальная проблема. Несохраненный контекст.

2. Благодаря этому ответу я смог найти ошибку в своей программе. Я просто хочу добавить, что извлеченные объекты являются несохраненными. Я не могу поверить, что Apple не документирует эту проблему.

3. Следует отметить одну вещь — этот пример работает, потому что используемый вами контекст является базовым контекстом, а не его дочерним элементом. Запуск такого же сценария в дочернем контексте все равно даст плохие результаты, даже если они будут сохранены, пока изменения не будут сохранены в постоянном хранилище. Это связано с тем, что свойство fetchOffset для работы использует sqlite и базу данных на диске. Это действительно должно вызывать предупреждение в objective-c, но вместо этого они просто возвращают плохие результаты.

Ответ №3:

Core Data имеет встроенную подкачку, и это очень просто, просто установите fetchBatchSize в запросе выборки. При установке выборки просто запрашивает все объекты как ошибки, в основном просто указатель и его индекс строки, который занимает минимум памяти, в особый вид массива, называемый массивом с ошибками. Я бы предположил, что в большинстве случаев это нормально, хотя я не изучал точно, сколько памяти он будет использовать для огромного количества строк. Затем, когда вы перебираете этот массив при доступе к свойству сбойной записи, он запрашивает базу данных для этой записи и следующих записей вплоть до числа, установленного для размера пакета. Это позволяет вам зацикливать ваши результаты один за другим, как обычно, но в фоновом режиме данные загружаются пакетами. Начиная с iOS 9.2 минимальный размер пакета равен 4, поэтому нет смысла устанавливать число ниже этого.