Simplifiez-vous CoreData Avec MagicalRecord CocoaHeads Rennes #13 Olivier Halligon Septembre 2013
Apr 22, 2015
Simplifiez-vous CoreDataAvec MagicalRecord
CocoaHeads Rennes #13
Olivier Halligon
Septembre 2013
CoreData : rappels
• Framework Cocoa pour iOS et OSX• Permet de gérer un graphe d’objets et sa persistance
• Gestion de relations
• Gestion des transactions, d’annulation…
• Gestion des futures (faulting)
•N’est pas une base de données relationnelle• On peut l’utiliser en InMemory-only
• Peut faire persister les données dans une base SQLite mais aussi en XML
• On peut l’utiliser comme abstraction d’un WebService (via NSIncrementalStore)
Exemple de modèle de données
Complexité de CoreData
NSAtomicStoreNSPersistent
StoreNSManagedObjectModel
NSFetc
hResul
t
Contro
ller
NSAttribute
Description
NSManagedObjectID
NSEntityMapping
NSManagedObject
NSFetch
Request
NSManaged
ObjectContex
t
NSEntityDescription
NSIncr
ementa
l
Store
NSPersistentStoreCoordinator
Complexité de CoreData
• Beaucoup de classes à prendre en main quand on commence• Dont pour la plupart le rôle semble abstrait de prime abord
• Même si ça vient avec la pratique, peu encourageant au début
• Beaucoup de lignes de code pour des opérations simples• Une dizaine de lignes rien que pour récupérer un objet dans notre graphe
• Du coup répétitif pour des actions fréquentes
• Nécessité d’utiliser des NSString pour décrire les noms d’entités à récupérer
• Pas d’autocomplétion, pas de vérification à la compilation
• Nécessité de transtyper (caster) les résultats
Exemple de Requête
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdFetching.html#//apple_ref/doc/uid/TP40002484-SW1
- (void)logAllRennesSessions{ NSManagedObjectContext *moc = [self managedObjectContext]; NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Session" inManagedObjectContext:moc]; NSFetchRequest *request = [[NSFetchRequest alloc] init]; [request setEntity:entityDescription];
// Set sort descriptor NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; [request setSortDescriptors:@[sortDescriptor]];
// Set predicate NSPredicate *predicate = [NSPredicate predicateWithFormat:@"city.name == %@", @"Rennes"]; [request setPredicate:predicate]; NSArray *array = [moc executeFetchRequest:request error:nil]; if (array == nil) { NSLog(@"Some error occured"); } else { NSLog(@"Sessions in Rennes: %@", array); }}
Exemple de Requête
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdFetching.html#//apple_ref/doc/uid/TP40002484-SW1
- (City*)findOrCreateCityWithName:(NSString*)cityName{ NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext]; // Find the city if it exists, create it if not NSManagedObject* foundCity = nil; NSEntityDescription* cityEntity = [NSEntityDescription entityForName:@"City" inManagedObjectContext:context]; NSFetchRequest* cityRequest = [[NSFetchRequest alloc] init]; [cityRequest setEntity:cityEntity]; NSPredicate* cityPredicate = [NSPredicate predicateWithFormat:@"name == %@", cityName]; [cityRequest setPredicate:cityPredicate]; [cityRequest setFetchLimit:1]; NSArray* cities = [context executeFetchRequest:cityRequest error:nil]; if (cities.count == 0) { foundCity = [[NSManagedObject alloc] initWithEntity:cityEntity insertIntoManagedObjectContext:context]; [foundCity setValue:cityName forKey:@"name"]; } else { foundCity = [cities objectAtIndex:0]; } return (City*)foundCity;}
MagicalRecord
• ActiveRecord pour Objective-C• Pattern bien connu des programmeurs Ruby
• Utiliser les objets du MDD directement : [Session findAll]
• Un framework «wrapper» pour faciliter votre code• Requêtes implicites, lecture plus claire
• Plus facile à prendre en main pour débuter en CoreData
• Reste utile même quand vous n’êtes plus débutant pour avoir un code concis
• N’empêche pas de continuer à utiliser les méthodes du framework Apple
- (void)logAllRennesSessions{
}
NSArray* array = [Session findByAttribute:@"city.name" withValue:@"Rennes" andOrderBy:@"date" ascending:NO]; NSLog(@"Sessions in Rennes: %@", array);
- (void)logAllRennesSessions{
}
NSManagedObjectContext *moc = [self managedObjectContext]; NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Session" inManagedObjectContext:moc]; NSFetchRequest *request = [[NSFetchRequest alloc] init]; [request setEntity:entityDescription];
// Set predicate NSPredicate *predicate = [NSPredicate predicateWithFormat:@"city.name == %@", @"Rennes"]; [request setPredicate:predicate];
// Set sort descriptor NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; [request setSortDescriptors:@[sortDescriptor]]; NSArray *array = [moc executeFetchRequest:request error:nil]; NSLog(@"Sessions in Rennes: %@", array);
Exemple de Requête NSManagedObjectContext *moc = [self managedObjectContext]; NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Session" inManagedObjectContext:moc];
Avec MagicalRecord
// Set sort descriptor NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; [request setSortDescriptors:@[sortDescriptor]];
// Set predicate NSPredicate *predicate = [NSPredicate predicateWithFormat:@"city.name == %@", @"Rennes"]; [request setPredicate:predicate];
[Session andOrderBy:@"date" ascending:NO];ByAttribute:@"city.name" withValue:@"Rennes" NSArray* array = NSLog(@"Sessions in Rennes: %@", array);
NSArray *array = [moc executeFetchRequest:request error:nil]; NSLog(@"Sessions in Rennes: %@", array);
find
NSFetchRequest *request = [[NSFetchRequest alloc] init]; [request setEntity:entityDescription];
Démo
Comparatif
Nombre de lignes de code CoreData MagicalRecordConfiguration CoreData dans l’AppDelegate 70~90 1
logAllSessions 12 1
emptyGraphObject 17 3
newSessionWithSubject:date:summary:lecturer: 10 6
findOrCreateCityWithName: 24 7
findOrCreatePersonWithFirstName: 25 9
buildFetchedResultsController 24 1
findSimilarSessions 13 2
Total 230~250 65
Avant Après
70% de code en moins !
Possibilités offertes par MagicalRecord
• Catégories pour avoir des méthodes de commodité• Récupérer tous les objets qui suivent un prédicat
• findAll, findAllSortedBy:ascending:, findAllByAttribute:withValue:orderBy:ascending:, findAllSortedBy:ascending:withPredicate:, …
• Compter le nombre d’entités• countOfEntities, countOfEntitiesWithPredicate:
• Récupérer un seul objet• findFirst, findFirstWithPredicate:, findFirstWithPredicate:sortedBy:ascending: findFirstByAttribute:withValue:
• Créer et supprimer des entités• createEntity, deleteEntity, deleteAllMatchingPredicate:, truncateAll, …
• Construire des requêtes• createFetchRequest, requestAll, requestAllWithPredicate:, requestAllWhere:isEqualTo:, …
• Utiliser les Fetch Results Controllers• fetchAllSortedBy:ascending:withPredicate:groupBy:delegate:
Possibilités offertes par MagicalRecord
• Faciliter la gestion des Contextes• Toutes les méthodes de MR ont une variante avec et sans contexte• Sans contexte précisé, MR utilise son defaultContext
• Un ManagedObjectContext par défaut, ainsi qu’un contexte par thread• [NSManagedObjectContext defaultContext], [NSManagedObjectContext contextForCurrentThread]
• Passer un ManagedObject d’un thread à l’autre, d’un contexte à l’autre• NSManagedObject *objectOnThreadTwo = [objectOnThreadOne inThreadContext];
• NSManagedObject *objectInCtxTwo = [objectInCtxOne inContext:otherContext];
• Création simple d’un contexte fils à la demande• contextWithParent:, contextThatPushesChangesToDefaultContext
• Gestion des sauvegardes• En cascade jusqu’au PersistentStore (ex: app en bkg) : saveToPersistentStoreWithCompletion:• Sur un contexte local via une API avec des blocks : saveWithBlock:completion:, saveInBackgroundWithBlock:…
context WithParent:
save:
Possibilités offertes par MagicalRecord
• Simplifier la phase d’initialisation• Initialisation en une ligne
• [MagicalRecord setupCoreDataStack];
• Très pratique pour les Tests Unitaires
• InMemoryStore : toujours partir sur une base vide, ne pas polluer la base de prod• [NSBundle bundleForClass:self] pour fonctionner même en phase de TU• [MagicalRecord setupCoreDataStackWithInMemoryStore];
• Possibilité de créer un NSManagedObjectModel unifié d’après les MOM du bundle• defaultManagedObjectModel, mergedObjectModelFromMainBundle
• Support d’iCloud• setupCoreDataStackWithiCloudContainer:localStoreNamed:
• setupCoreDataStackWithiCloudContainer:contentNameKey:localStoreNamed:cloudStorePathComponent:completion:
MagicalRecord avec CocoaPodspod install
pod update
xcodeproj "MRDemo"platform :ios, '5.0'
pod "MagicalRecord", "~>2.0"
Podfile
-(BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)options{ [MagicalRecord setupCoreDataStack]; … return YES;}
AppDelegate.m
#ifdef __OBJC__#define MR_SHORTHAND#define MR_ENABLE_ACTIVE_RECORD_LOGGING 0#import "CoreData+MagicalRecord.h"#endif
Pods-MagicalRecord-prefix.pch
#ifdef __OBJC__#define MR_SHORTHAND#import "CoreData+MagicalRecord.h"#endif
YourApp-Prefix.pch
Références• CoreData Programming Guide
• https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/cdProgrammingGuide.html
•MagicalRecord• https://github.com/magicalpanda/MagicalRecord (don’t forget the wiki)
• http://nshipster.com/core-data-libraries-and-utilities/
• CocoaPods• http://cocoapods.org/ & http://docs.cocoapods.org/
• https://github.com/CocoaPods/Specs (don’t forget the wiki)
• Code source de la démo• https://github.com/CocoaHeads-Rennes/13-MagicalRecord