Les nouveautés de Core Data sous iOS 8 (Partie 1)
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 rienNSUpdatedObjectIDsResultType
: la requête retourne les IDs des lignes qui ont été mises à jour (unNSArray
deNSManagedObjectID
)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.
- Premièrement, nous créons une
NSBatchUpdateRequest
avec l’entité que l’on veut mettre à jour - On spécifie quelles sont les property à mettre à jour via
propertiesToUpdate
. Dans notre cas on veut mettre la propertyread
à@YES
pour tous les articles. - On spécifie le type de résultat de la requête à
NSUpdatedObjectsCountResultType
- Enfin on passe la requête à la base de données grâce à la méthode
[NSManagedObjectContext executeRequest:error:]
qui retourne unNSPersistentStoreResult
. Ce résultat est casté enNSBatchUpdateResult
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.