NSFetchedResultsController находит неправильное количество объектов

Я вижу ситуацию, когда NSFetchRequest возвращает разное количество объектов в зависимости от того, выполняется ли он напрямую через NSManagedObjectContext или как часть построения NSFetchedResultsController.

Образец кода:

- (void)setupResultsController {
    NSError *error = nil;
    NSManagedObjectContext *ctx = [[DataManager sharedInstance] mainObjectContext];

    // Create a fetch request and execute it directly
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

    NSEntityDescription *entity = [Song entityInManagedObjectContext:ctx];
    [fetchRequest setEntity:entity];
    [fetchRequest setFetchBatchSize:20];

    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"section" ascending:YES];
    NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSArray *sortDescriptors = @[sortDescriptor, nameDescriptor];

    [fetchRequest setSortDescriptors:sortDescriptors];
    NSArray *debugResults = [ctx executeFetchRequest:fetchRequest error:&error];
    NSLog(@"Count from context fetch: %lu", (unsigned long)debugResults.count);

    // Use the request to populate a NSFetchedResultsController
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
                                                             initWithFetchRequest:fetchRequest
                                                             managedObjectContext:ctx
                                                             sectionNameKeyPath:@"section"
                                                             cacheName:@"Detail"];
    [aFetchedResultsController performFetch:&error];
    NSLog(@"Count from results controller fetch: %lu", (unsigned long)[[aFetchedResultsController fetchedObjects] count]);

    _songResultsController = aFetchedResultsController;
}

Выполнение вышеуказанного приводит к появлению сообщений журнала:

2020-01-10 11: 05: 07.892772-0500 asb7 [12985: 105052] Подсчет из выборки контекста: 10

2020-01-10 11: 05: 07.893259-0500 asb7 [12985: 105052] Счетчик из выборки контроллера результатов: 9

Разница между двумя выборками заключается в том, что в NSFetchedResultsController отсутствует последний добавленный объект. Крайняя странность в том, что после запуска приложения несколько, казалось бы, случайного числа раз, счетчики начинают согласовываться и выбирается новый объект.

Изменить:

Результаты станут согласованными, если я передам nil в качестве имени кеша или если я удалю второй дескриптор сортировки. Очевидно, они вызывают нежелательные изменения поведения, но могут быть подсказками.

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

После еще нескольких экспериментов у меня есть объяснение ... если не решение. Добавление новых объектов не меняет дату модификации моего файла .sqlite. Он обновляет .sqlite-shm и .sqlite-wal, но я предполагаю, что они не учитываются при оценке использования кеша. Использование touch из сеанса терминала устраняет проблему при следующем запуске.

(Xcode 10.1, macOS 10.13.6, цель развертывания 10.3, симулятор iOS 12.1 и устройство 10.3.2)

Другое изменение:

Я загрузил заархивированный каталог проекта, который демонстрирует проблему, по адресу https://github.com/PhilKMills/CacheTest

Что я получаю: первый запуск, 3 записи для обоих выборок; второй прогон, 6 и 3. Я считаю вполне возможным, что это зависит от моей конкретной версии программного обеспечения, но я не в состоянии выполнить обновление в данный момент. Чужие результаты были бы наиболее интересными.

Примечание: без назначения делегата FRC проблема не возникает.


person Phillip Mills    schedule 10.01.2020    source источник
comment
Остается ли проблема, если вы не используете кеш FRC?   -  person pbasdf    schedule 10.01.2020
comment
@pbasdf Это синхронизирует 2 выборки и делает то, что я пытался: удаление второго дескриптора сортировки. Обновление вопроса ....   -  person Phillip Mills    schedule 10.01.2020
comment
Несколько строк запроса: 1) использование SQLDebug показывает что-нибудь полезное в журналах; 2) изменение режима журнала, чтобы избежать использования режима WAL, меняет поведение; 3) влияет ли изменение includesPendingChanges на запрос fetchrequest.   -  person pbasdf    schedule 11.01.2020
comment
Я попробовал пункт 3 на раннем этапе без изменений. Я вернусь к вам о других, когда выясню, как это сделать. Спасибо!   -  person Phillip Mills    schedule 11.01.2020
comment
@pbasdf Похоже, что избавление от режима WAL решает проблему. Я только что провел пару тестов после добавления опции NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}, и время модификации файла .sqlite обновляется. Спасибо еще раз! (Вы должны добавить сюда ответ.)   -  person Phillip Mills    schedule 11.01.2020
comment
Похоже, это скорее обходной путь, чем ответ, но в будущем он может помочь кому-то другому, поэтому я добавлю его (когда у меня будет немного больше времени).   -  person pbasdf    schedule 11.01.2020
comment
Но это безумие; конечно, если бы это было нормально, мы бы все об этом знали! Это сделало бы Core Data невозможным для использования. Есть ли что-то странное в том, как вы добавляете объекты?   -  person matt    schedule 12.01.2020
comment
@matt Я не вижу ничего необычного. Если я использую sqlite3 из командной строки для проверки базы данных, записи, которые я ожидаю увидеть, сообщаются правильно, и, как я уже упоминал, использование контекста для выполнения выборки также дает правильный ответ. (Программа работала правильно в предыдущих версиях, хотя я не уверен, было ли это до WAL.)   -  person Phillip Mills    schedule 13.01.2020
comment
Что ж, я думаю, этого по крайней мере достаточно, чтобы отправить отчет об ошибке в Apple. Esp, если дескрипторы сортировки изменяют поведение.   -  person matt    schedule 13.01.2020
comment
Дело в дескрипторах сортировки на самом деле является просто доказательством того, что это действительно проблема с кешем. Это одна из вещей, которые CoreData должен использовать для проверки правильности кеша. Я только что собрал крошечное приложение, которое воспроизводит проблему. Я позабочусь о том, чтобы сделать это общедоступным, чтобы люди могли тыкать в него.   -  person Phillip Mills    schedule 13.01.2020
comment
Еще одна деталь, которая может сделать это редкой проблемой, - это ваш стек CoreData. Чтобы изменения попали в магазин без прохождения контекста FRC, я полагаю, у вас есть отдельный контекст в том же постоянном координаторе хранилища (или, действительно, отдельный PSC, обращающийся к тому же хранилищу)? Если так, это может сделать его достаточно редким, чтобы его не подбирали раньше.   -  person pbasdf    schedule 13.01.2020
comment
Тот же контекст. Приложению не нужен какой-либо фон или какой-либо другой сложный доступ. Если у вас есть возможность взглянуть на мое мини-приложение на github, вы увидите, что оно невероятно простое. При создании мини я узнал еще одну деталь: без делегата FRC числа остаются синхронизированными.   -  person Phillip Mills    schedule 13.01.2020
comment
Но, черт возьми, я собирался предложить вам использовать делегат FRC, чтобы обнаружить, что источник изменился!   -  person matt    schedule 13.01.2020
comment
Пробовал ваш примерный проект. Проблема все еще существует в Xcode 11.3, Simulator 13.3, работающем на macOS 10.15.2.   -  person pbasdf    schedule 14.01.2020
comment
Спасибо. Я только что отправил отчет об ошибке через систему обратной связи Apple.   -  person Phillip Mills    schedule 14.01.2020


Ответы (1)


Я подозревал, что проблема связана с использованием кеша FRC, хотя не знал, почему это может быть. Таким образом, можно было бы отказаться от использования кэша FRC. Однако это не идеально.

После дальнейших экспериментов OP кажется, что проблема связана с отметкой времени в файле .sqlite, связанном с постоянным хранилищем. По заключению (*), если FRC обнаруживает, что метка времени изменилась, он понимает, что его кеш может быть устаревшим, и перестраивает его, тем самым обнаруживая вновь добавленные объекты. Однако, поскольку CoreData по умолчанию использует "режим журнала WAL" SQLite (см. здесь и здесь), обновления базы данных записываются не в .sqlite файл, а вместо этого в отдельный .sqlite-wal файл: отметка времени в .sqlite файле, следовательно, не изменяется, даже если база данных была обновлена. FRC продолжает использовать свой кеш, не зная о дополнительных объектах (или даже о других изменениях).

Поэтому второй обходной путь - отказаться от режима журнала SQLite WAL. Этого можно достичь, указав требуемый режим журнала при загрузке постоянного хранилища, используя

NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}

см. здесь.

(*) Фактически, это задокументировано здесь:

«Контроллер проверяет кэш, чтобы определить, действительно ли его содержимое. Контроллер сравнивает текущее имя объекта, хэш версии объекта, дескрипторы сортировки и путь ключа раздела с теми, которые хранятся в кэше, а также дату модификации файла кэшированной информации и файла постоянного хранилища ».

person pbasdf    schedule 12.01.2020