Статьи

iOS 8: базовые данные и пакетные обновления

Core Data существует уже много лет на OS X, и Apple не потребовалось много времени, чтобы перенести его на iOS. Несмотря на то, что фреймворк не привлекает столько внимания, как расширения или передача обслуживания, он продолжает развиваться год за годом, и в этом году, с выпуском iOS 8 и OS X Yosemite, ничем не отличается.

Apple представила несколько новых функций в платформе Core Data, но наиболее заметными являются пакетные обновления и асинхронная выборка. Разработчики просили эти функции в течение многих лет, и Apple наконец-то нашла способ интегрировать их в Core Data. В этом руководстве я покажу вам, как работают пакетные обновления и что они означают для платформы Core Data.

Core Data отлично справляется с управлением графами объектов. Даже сложные графы объектов со многими сущностями и отношениями не являются большой проблемой для Core Data. Однако у Core Data есть несколько слабых мест, и одним из них является обновление большого количества записей.

Проблема проста для понимания. Всякий раз, когда вы обновляете запись, Core Data загружает запись в память, обновляет запись и сохраняет изменения в постоянном хранилище, например, в базе данных SQLite.

Если базовым данным требуется обновить большое количество записей, необходимо загрузить каждую отдельную запись в память, обновить запись и отправить изменения в постоянное хранилище. Если количество записей слишком велико, iOS просто выручит из-за нехватки ресурсов. Даже если устройство под управлением OS X может иметь ресурсы для выполнения запроса, оно будет медленным и занимать много памяти.

Альтернативный подход заключается в обновлении записей в пакетном режиме, но это также требует много времени и ресурсов. На iOS 7 это единственный вариант, который есть у разработчиков iOS. Это больше не относится к iOS 8.

На iOS 8 и OS X Yosemite можно напрямую поговорить с постоянным магазином и сообщить ему, что вы хотите изменить. Обычно это включает обновление атрибута или удаление ряда записей. Apple называет эту функцию пакетными обновлениями.

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

Когда вы будете использовать пакетные обновления? Apple рекомендует использовать эту функцию только в том случае, если традиционный подход требует слишком много ресурсов или времени. Если вам нужно пометить сотни или тысячи сообщений электронной почты как прочитанные, то пакетное обновление — лучшее решение для iOS 8 и OS X Yosemite.

Чтобы проиллюстрировать, как работают пакетные обновления, я предлагаю вернуться к Done , простому приложению Core Data, которое управляет списком дел. Мы добавим кнопку на панель навигации, которая пометит каждый элемент в списке как выполненный.

Загрузите или клонируйте проект из GitHub и откройте его в Xcode 6. Запустите приложение в iOS Simulator и добавьте несколько задач.

Откройте TSPViewController.m и объявите свойство checkAllButton типа UIBarButtonItem в расширении частного класса вверху.

1
2
3
4
5
6
7
8
9
@interface TSPViewController () <NSFetchedResultsControllerDelegate>
 
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
 
@property (strong, nonatomic) UIBarButtonItem *checkAllButton;
 
@property (strong, nonatomic) NSIndexPath *selection;
 
@end

Инициализируйте элемент кнопки панели в методе TSPViewController класса TSPViewController и установите его в качестве элемента кнопки левой панели элемента навигации.

1
2
3
4
5
// Initialize Check All Button
self.checkAllButton = [[UIBarButtonItem alloc] initWithTitle:@»Check All» style:UIBarButtonItemStyleBordered target:self action:@selector(checkAll:)];
     
// Configure Navigation Item
self.navigationItem.leftBarButtonItem = self.checkAllButton;

Метод checkAll: довольно прост, но есть несколько предостережений, на которые стоит обратить внимание. Посмотрите на его реализацию ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
— (void)checkAll:(id)sender {
    // Create Entity Description
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@»TSPItem» inManagedObjectContext:self.managedObjectContext];
     
    // Initialize Batch Update Request
    NSBatchUpdateRequest *batchUpdateRequest = [[NSBatchUpdateRequest alloc] initWithEntity:entityDescription];
     
    // Configure Batch Update Request
    [batchUpdateRequest setResultType:NSUpdatedObjectIDsResultType];
    [batchUpdateRequest setPropertiesToUpdate:@{ @»done» : @YES }];
     
    // Execute Batch Request
    NSError *batchUpdateRequestError = nil;
    NSBatchUpdateResult *batchUpdateResult = (NSBatchUpdateResult *)[self.managedObjectContext executeRequest:batchUpdateRequest error:&batchUpdateRequestError];
     
    if (batchUpdateRequestError) {
        NSLog(@»Unable to execute batch update request.»);
        NSLog(@»%@, %@», batchUpdateRequestError, batchUpdateRequestError.localizedDescription);
         
    } else {
        // Extract Object IDs
        NSArray *objectIDs = batchUpdateResult.result;
         
        for (NSManagedObjectID *objectID in objectIDs) {
            // Turn Managed Objects into Faults
            NSManagedObject *managedObject = [self.managedObjectContext objectWithID:objectID];
             
            if (managedObject) {
                [self.managedObjectContext refreshObject:managedObject mergeChanges:NO];
            }
        }
         
        // Perform Fetch
        NSError *fetchError = nil;
        [self.fetchedResultsController performFetch:&fetchError];
         
        if (fetchError) {
            NSLog(@»Unable to perform fetch.»);
            NSLog(@»%@, %@», fetchError, fetchError.localizedDescription);
        }
    }
}

Мы начнем с создания экземпляра NSEntityDescription для объекта TSPItem и используем его для инициализации объекта NSBatchUpdateRequest .

1
2
3
4
5
// Create Entity Description
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@»TSPItem» inManagedObjectContext:self.managedObjectContext];
 
// Initialize Batch Update Request
NSBatchUpdateRequest *batchUpdateRequest = [[NSBatchUpdateRequest alloc] initWithEntity:entityDescription];

Мы устанавливаем тип результата запроса пакетного обновления на NSUpdatedObjectIDsResultType , что означает, что результатом запроса пакетного обновления будет массив, содержащий идентификаторы объектов, экземпляры класса NSManagedObjectID , записей, которые были изменены запросом пакетного обновления.

1
2
// Configure Batch Update Request
[batchUpdateRequest setResultType:NSUpdatedObjectIDsResultType];

Мы также заполняем свойство propertiesToUpdate запроса на пакетное обновление. Для этого примера мы устанавливаем propertiesToUpdate в NSDictionary содержащий один ключ, @"done" , со значением @YES . Это просто означает, что для каждой записи TSPItem будет выполнено , а это именно то, что нам нужно.

1
2
// Configure Batch Update Request
[batchUpdateRequest setPropertiesToUpdate:@{ @»done» : @YES }];

Несмотря на то, что пакетные обновления обходят контекст управляемого объекта, выполнение запроса на пакетное обновление выполняется путем вызова executeRequest:error: для экземпляра NSManagedObjectContext . Первый аргумент является экземпляром класса NSPersistentStoreRequest . Чтобы выполнить пакетное обновление, мы передаем запрос на пакетное обновление, которое мы только что создали. Это работает NSBatchUpdateRequest поскольку класс NSBatchUpdateRequest является подклассом NSPersistentStoreRequest . Второй аргумент — указатель на объект NSError .

1
2
3
// Execute Batch Request
NSError *batchUpdateRequestError = nil;
NSBatchUpdateResult *batchUpdateResult = (NSBatchUpdateResult *)[self.managedObjectContext executeRequest:batchUpdateRequest error:&batchUpdateRequestError];

Как я упоминал ранее, пакетные обновления обходят контекст управляемого объекта. Это дает пакетным обновлениям их мощность и скорость, но это также означает, что контекст управляемого объекта не знает об изменениях, которые мы сделали. Чтобы исправить эту проблему, нам нужно сделать две вещи:

  • превратить управляемые объекты, которые были обновлены при пакетном обновлении, в ошибки
  • скажите контроллеру полученных результатов выполнить выборку для обновления пользовательского интерфейса

Это то, что мы делаем в следующих нескольких строках метода checkAll: . Сначала мы проверяем, был ли запрос на пакетное обновление успешным, проверяя batchUpdateRequestError на nil . В случае успеха мы извлекаем массив экземпляров NSManagedObjectID из объекта NSBatchUpdateResult .

1
2
// Extract Object IDs
NSArray *objectIDs = batchUpdateResult.result;

Затем мы objectIDs массив objectIDs и запрашиваем контекст управляемого объекта для соответствующего экземпляра NSManagedObject . Если контекст управляемого объекта возвращает действительный управляемый объект, мы превращаем его в ошибку, вызывая refreshObject:mergeChanges: передавая управляемый объект в качестве первого аргумента. Чтобы принудить управляемый объект к ошибке, мы передаем NO как второй аргумент.

01
02
03
04
05
06
07
08
09
10
11
// Extract Object IDs
NSArray *objectIDs = batchUpdateResult.result;
 
for (NSManagedObjectID *objectID in objectIDs) {
    // Turn Managed Objects into Faults
    NSManagedObject *managedObject = [self.managedObjectContext objectWithID:objectID];
     
    if (managedObject) {
        [self.managedObjectContext refreshObject:managedObject mergeChanges:NO];
    }
}

Последний шаг — сообщить контроллеру полученных результатов выполнить выборку для обновления пользовательского интерфейса. Если это не удалось, мы регистрируем соответствующую ошибку.

1
2
3
4
5
6
7
8
// Perform Fetch
NSError *fetchError = nil;
[self.fetchedResultsController performFetch:&fetchError];
 
if (fetchError) {
    NSLog(@»Unable to perform fetch.»);
    NSLog(@»%@, %@», fetchError, fetchError.localizedDescription);
}

Хотя это может показаться громоздким и довольно сложным для легкой операции, имейте в виду, что мы обходим стек основных данных. Другими словами, нам нужно позаботиться о том, чтобы домашние хозяйства обычно выполнялись Core Data.

Создайте проект и запустите приложение в iOS Simulator или на физическом устройстве. Нажмите или коснитесь элемента панели кнопок справа, чтобы проверить каждый элемент списка в списке.

Пакетные обновления являются отличным дополнением к платформе Core Data. Это не только отвечает потребностям, которые испытывали разработчики в течение многих лет, это не сложно реализовать, если вы помните несколько основных правил. В следующем уроке мы подробнее рассмотрим асинхронную выборку, еще одну новую функцию платформы Core Data.