Les nouveautés de Core Data sous iOS 8 (Partie 1)

Par Pierre Felgines, le 06.10.2014

En juin dernier, Apple présentait iOS 8. Et chaque nouvelle version d’iOS apporte son lot de nouveautés. En parcourant la liste complète des différences d’API entre iOS 7 et iOS 8, nous sommes tombés sur deux nouveaux types de requête de base de données dans Core Data : les requêtes de mise à jour par lots (batch update requests) et les requêtes asynchrones (asynchronous fetch requests). Dans cet article nous allons nous intéresser au premier type de requête.

Les requêtes de mise à jour par lots

La première nouveauté intéressante de l’API de Core Data sous iOS 8 est l’apparition des batch update requests ou requêtes de mise à jour par lots. Avant iOS 8, il n’y avait pas de méthode dans cette API qui permettait de mettre à jour une colonne d’un ensemble de lignes de base de données à une même valeur, en une seule opération. Il fallait le faire à la main, ce qui pouvait s’avérer couteux comme nous le verrons un peu plus tard. Désormais Core Data fournit une méthode générique et efficace pour effectuer cette opération.

Apple a introduit une nouvelle sous-classe de NSPersistentStoreRequest appelée NSBatchUpdateRequest. Cette classe est utilisée pour effectuer une mise à jour d’un ensemble d’objets en base de données, sans pour autant les charger en mémoire.

Si on regarde en détail l’API de cette classe, on voit que les instances de NSBatchUpdateRequest ont une property nommée propertiesToUpdate. Cette property est utilisée pour spécifier les attributs des entités à mettre à jour, ainsi que leurs valeurs finales.

Il y a également une property resultType qui est utilisée pour définir quel sera le type de retour de la requête. Cette property peut prendre trois valeurs différentes :

  • NSStatusOnlyResultType (par défaut): la requête ne retourne rien
  • NSUpdatedObjectIDsResultType: la requête retourne les IDs des lignes qui ont été mises à jour (un NSArray de NSManagedObjectID)
  • NSUpdatedObjectsCountResultType: la requête retourne le nombre de lignes mises à jour

En pratique : iOS 7 VS iOS 8

Prenons un exemple pour étudier la différence d’implémentation d’une mise à jour par lot, pour chaque version d’iOS. Imaginons que chaque ligne de base de données représente un article, et que chaque article possède une property read qui indique si l’utilisateur l’a lu ou non.

Nous voulons implémenter une fonctionnalité qui marque tous les articles comme lus.

Sous iOS 7, nous pourrions écrire quelque chose comme cela :

- (void)markAllAsRead {
  // Create fetch request for Article entity
  NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Article"];
  // Execute the fetch request and get the array of articles
  NSArray * articles = [self.managedObjectContext executeFetchRequest:fetchRequest error:NULL];
  // Update the managed objects one by one
  for (Article * article in articles) {
    article.read = @YES;
  }
  // Save the context to persist changes
  NSError * error = NULL;
  [self.managedObjectContext save:&error];
  if (error) {
    // Handle the error
  }
}

Très simple. On récupère tous les articles et on change la property read pour chacun d’eux. Enfin on sauvegarde le contexte pour persister les données.

Sous iOS 8, par contre, nous pouvons utiliser une NSBatchUpdateRequest pour effectuer la même action.

- (void)markAllAsRead {
  // Create the batch update request for Article entity
  NSBatchUpdateRequest * batchUpdateRequest =
    [NSBatchUpdateRequest batchUpdateRequestWithEntityName:@"Article"];
  // Specify the properties to update and their final values
  batchUpdateRequest.propertiesToUpdate = @{@"read": @YES};
  // Set up the result of the operation
  batchUpdateRequest.resultType = NSUpdatedObjectsCountResultType;
  // Execute the request
  NSError * error = NULL;
  NSBatchUpdateResult * result =
    (NSBatchUpdateResult *)[self.managedObjectContext executeRequest:batchUpdateRequest
                                                               error:&error];
  if (!result) {
    // Handle the error
  } else {
    // result is of type NSUpdatedObjectsCountResultType
    NSAssert(result.resultType == NSUpdatedObjectsCountResultType, @"Wrong result type");
    // Handle the result
    NSLog(@"Updated %d objects.", [result.result intValue]);
  }
}

Regardons ce qui se passe ici.

  1. Premièrement, nous créons une NSBatchUpdateRequest avec l’entité que l’on veut mettre à jour
  2. On spécifie quelles sont les property à mettre à jour via propertiesToUpdate. Dans notre cas on veut mettre la property read à @YES pour tous les articles.
  3. On spécifie le type de résultat de la requête à NSUpdatedObjectsCountResultType
  4. Enfin on passe la requête à la base de données grâce à la méthode [NSManagedObjectContext executeRequest:error:] qui retourne un NSPersistentStoreResult. Ce résultat est casté en NSBatchUpdateResult pour avoir accès au nombre de lignes qui ont été mises à jour.

Performance

Les deux exemples ci-dessus effectuent la même opération, à savoir marquer tous les articles comme lus, mais diffèrent au niveau de leurs performances. Regardons plus en détail ce qu’il se passe dans chaque exemple.

Dans le premier exemple, sous iOS 7, on commence par charger tous les articles en mémoire avec une NSFetchRequest. En pratique cela veut dire qu’une requête SQL de type SELECT est envoyée à la base de données pour récupérer les articles. Puis Core Data charge les articles en mémoire en créant N NSManagedObject, où N est le nombre d’articles récupérés. Cette étape peut être très longue pour un grand nombre d’articles.
Tous les managed objects sont ensuite mis à jour en changeant la valeur de read à YES. Les changements sont persistés quand on sauvegarde le contexte. En pratique, N requêtes SQL de type UPDATE sont générées et envoyées à la base de données lors de la sauvegarde.
Au total, N + 1 requêtes sont envoyées à la base de données.

A l’inverse, dans le second exemple sous iOS 8, on n’utilise pas de NSFetchRequest. Il n’y a pas de requête de type SELECT et pas de surcharge de mémoire. A la place on utilise une NSBatchUpdateRequest qui envoie une seule requête de type UPDATE à la base donnée, requête qui s’occupe de mettre à jour toutes les lignes en une seule opération.
Au total, une seule requête a été envoyée à la base de données.

Utiliser une requêtes de mise à jour par lot évite d’envoyer N requêtes à la base de données et de créer N managed object en mémoire. Pour avoir une idée de la différence de performances, la mise en place d’un benchmark très simple montre que mettre à jour un millier d’articles est environ dix fois plus performant avec une batch request qu’avec une simple fetch request.

Conclusion

Les requêtes de mise à jour par lot NSBatchUpdateRequest introduites sous iOS 8 dans l’API de Core Data sont très intéressantes en termes de performances car ne chargent aucun contenu en mémoire et transmettent la requête SQL de mise à jour directement à la base de données. Cela signifie que votre code ne repose pas sur la représentation en mémoire de vos objects (NSManagedObject).

A noter. Comme les batch update requests ne créent pas de managed objects, aucune validation n’est effectuée lors de la mise à jour des lignes. C’est à vous de vérifier que les valeurs sont correctes lorsqu’elles sont passées à la base de données.