Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Opinionated Core Data: Hold On To Your Butts

Opinionated Core Data: Hold On To Your Butts

This presentation looks at the:
1) Principles
2) Architecture
3) Classes
4) ViewControllers

for successfully using and *TESTING* Core Data in your App.

Parveen Kaler

March 11, 2014
Tweet

More Decks by Parveen Kaler

Other Decks in Programming

Transcript

  1. Opinionated Core Data Hold On To Your Butts http://ParveenKaler.com [email protected]

    @kaler
  2. 1.Principles 2.Architecture 3.Classes 4.ViewControllers Hold On To Your Butts

  3. Goaly CoreData Stack Fitbit Jawbone Nike+ Runkeeper Health and fitness

    journal and dashboard Timeline Dashboard
  4. 1. Principles

  5. The Law of Leaky Abstractions "All non-trivial abstractions, to some

    degree, are leaky.” — Joel Spolsky
  6. What does Core Data abstract? Class Abstraction NSPeristentStoreCoordinator SQLite file

    NSManagedObjectModel SQLite schema NSManagedObjectContext SQL statements, query engine NSManagedObject Row in a table
  7. Active Record Principles of Enterprise Application Architecture by Martin Fowler

  8. Active Record • Construct an instance of the Active Record

    from a SQL result set row • Construct a new instance for later insertion into the table • Static finder methods to wrap commonly used SQL queries and return Active Record objects • Update the database and insert into it the data in the Active Record • Get and set the fields • Implement some pieces of business logic Principles of Enterprise Application Architecture by Martin Fowler
  9. Active Record • NSManagedObjectContext does insert, delete, update • NSEntityDescription

    constructs a row • NSManagedObject does have fields though
  10. Magical Record? https://github.com/magicalpanda/MagicalRecord

  11. Datamapper Principles of Enterprise Application Architecture by Martin Fowler

  12. Datamapper @class NSEntityDescription : NSObject + (NSEntityDescription *)entityForName:(NSString *)entityName inManagedObjectContext:(NSManagedObjectContext

    *)context @end
  13. SQLite

  14. SQLite CoreData: annotation: Connecting to sqlite database file at "/var/mobile/Applications/79445866-27E6-437D-B98B-

    F4FC9951E64A/Documents/xxxx.sqlite" CoreData: sql: pragma journal_mode=wal CoreData: sql: pragma cache_size=200 CoreData: sql: SELECT Z_VERSION, Z_UUID, Z_PLIST FROM Z_METADATA CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZDATE, t0.ZFROM, t0.ZMESSAGE, t0.ZSUBJECT, t0.ZTHREADID, t0.ZTO FROM ZMESSAGE t0 ORDER BY t0.ZTHREADID DESC CoreData: annotation: sql connection fetch time: 0.0087s CoreData: annotation: total fetch execution time: 0.0154s for 71 rows. CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZACCESSTOKEN, t0.ZANONYMOUSEMAIL, t0.ZANONYMOUSPHONENUMBER, t0.ZEMAIL, t0.ZFIRSTNAME, t0.ZLASTNAME, t0.ZMEMBERID, t0.ZPHONENUMBER, t0.ZVERIFICATIONCODE FROM ZACCOUNT t0 CoreData: annotation: sql connection fetch time: 0.0041s CoreData: annotation: total fetch execution time: 0.0067s for 0 rows. CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZACCESSTOKEN, t0.ZANONYMOUSEMAIL, t0.ZANONYMOUSPHONENUMBER, t0.ZEMAIL, t0.ZFIRSTNAME, t0.ZLASTNAME, t0.ZMEMBERID, t0.ZPHONENUMBER, t0.ZVERIFICATIONCODE FROM ZACCOUNT t0 CoreData: annotation: sql connection fetch time: 0.0051s CoreData: annotation: total fetch execution time: 0.0079s for 0 rows.
  15. 2. Architecture

  16. Construct The Whole Stack @interface GLYManagedObjectContext : NSManagedObjectContext + (instancetype)createAt:(NSURL

    *)storeURLorNil; @end
  17. Construct The Whole Stack + (instancetype)createAt:(NSURL *)storeURLorNil { NSURL *modelURL

    = [[NSBundle mainBundle] URLForResource:@"Goaly" withExtension:@"momd"]; NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; ! NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; NSString *type = storeURLorNil ? NSSQLiteStoreType : NSInMemoryStoreType; NSDictionary *options = @{ NSMigratePersistentStoresAutomaticallyOption : @YES, NSInferMappingModelAutomaticallyOption : @YES }; NSError *error = nil; if (![coordinator addPersistentStoreWithType:type configuration:nil URL:storeURLorNil options:options error:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } ! GLYManagedObjectContext *ctx = [[GLYManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [ctx setPersistentStoreCoordinator:coordinator]; return ctx; }
  18. Don’t Use Fucking Singletons • How are you gonna unit

    test? You unit test, right? • Explicitly hand the NSManagedObjectModel to a list view • Explicitly hand a NSManagedObject to a detail view • Handle signout by releasing the NSManagedObjectContext
  19. Unit Testing @interface GoalyTests : XCTestCase @property (nonatomic, strong) GLYManagedObjectContext

    *context; @end ! @implementation GoalyTests ! - (void)setUp { [super setUp]; self.context = [GLYManagedObjectContext createAt:nil]; } ! @end
  20. Unit Testing • Fat Model, Skinny Controller • Model shouldn’t

    import UIView or UIViewController • EVERYTHING else should be in the Model
  21. Multiple Stores • Per HTTP service • Static data like

    a food database • Per user like username.sqlite
  22. Multiple Contexts • UI NSManagedObjectContext with NSMainQueueConcurrencyType • Detail view

    NSManagedObjectContext for save/cancel operations • NSManagedObjectContext HTTP client for GET requests with lots of inserts
  23. Parent-Child Contexts NSManagedObjectContext *child; if (child) { if ([child hasChanges]

    && ![child save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } __block NSManagedObjectContext *parent; [parent performBlockAndWait: ^{ NSError *parentError = nil; if (parent.persistentStoreCoordinator.persistentStores.count && [parent hasChanges] && ![parent save:&parentError]) { NSLog(@"Unresolved error %@, %@", parentError, [parentError userInfo]); abort(); } }]; }
  24. 3. Classes

  25. NSManagedObject @interface GLYSleep : NSManagedObject ! @property (nonatomic) int64_t logId;

    @property (nonatomic) NSTimeInterval startTime; @property (nonatomic) int64_t timeInBed; ! + (NSString *)entityName; + (instancetype)instanceInManagedObjectContext:(NSManagedObjectContext*)ctx; ! + (void)mapFromDictionary:(NSDictionary*)response intoManagedObjectContext:(NSManagedObjectContext*)ctx; - (NSDictionary*)mapToDictionary; ! // Keep ugly NSDateFormatter bullshit internal - (NSString*)sleepTimeStringValue; ! @end
  26. NSManagedObject TEST THAT SHIT! YOU CAN DO IT!

  27. Use the API Console

  28. NSManagedObject Unit Tests - (void)testSleep { NSString *json = @"....";

    NSData *responseObject = [json dataUsingEncoding:NSUTF8StringEncoding]; NSError *error = nil; NSDictionary *d = [NSJSONSerialization JSONObjectWithData:responseObject options:0 error:&error]; [GLYSleep mapFromDictionary:d intoManagedObjectContext:self.context]; }
  29. NSFetchRequest • I usually implement Rails-style find, find_by, first, last,

    all, etc • Sometimes I implement it in NSManagedObjectModel, sometimes in NSFetchRequest • I’ve been putting it in NSFetchRequest recently
  30. NSFetchRequest @interface GLYSleepFetchRequest : NSFetchRequest + (GLYSleep*)find:(int64_t)logId; + (GLYSleep*)findOrCreate:(int64_t)logId; +

    (NSArray*)findOrCreate:(NSArray*)logIds; @end
  31. +(NSArray*)findOrCreate:(NSArray*)logIds NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *e =

    [NSEntityDescription entityForName:@"GLYSleep" inManagedObjectContext:ctx]; fetchRequest setEntity:entity]; ! NSPredicate *p = [NSPredicate predicateWithFormat:@"(logId IN %@)", logIds] [fetchRequest setPredicate:p]; ! NSArray *sd = @[[[NSSortDescriptor alloc] initWithKey: @"logId" ascending:YES]]; [fetchRequest setSortDescriptors:sd]; ! NSError *error = nil; NSArray *sleeps = [ctx executeFetchRequest:fetchRequest error:&error];
  32. +(NSArray*)findOrCreate:(NSArray*)logIds for (NSNumber *Id in logIds) { NSPredicate *p =

    [NSPredicate predicateWithFormat:@"logId = %@”, Id]; NSArray *res = [sleeps filteredArrayUsingPredicate:p]; if (res.count == 0) { // create } else { // find } } The above loop is O(n^2). In real code, I have an NSEnumerator that makes it O(n).
  33. 4. ViewControllers

  34. Pass in the minimalist thing possible • Pass in a

    NSManagedObject by default • Pass in a NSFetchRequest for list views • Pass in a child NSManagedObjectContext for edits • Pass in the entire NSManagedObjectContext only if you have to
  35. Creation/Deletion • Alloc/init your CoreData stack in the signin/signup view

    controller • Release your CoreData stack in the signout view controller
  36. TEST THAT SHIT! YOU CAN DO IT!

  37. Opinionated Core Data Hold On To Your Butts http://ParveenKaler.com [email protected]

    @kaler