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.

9a2349983b8d0e1cd2ca8132daa3685a?s=128

Parveen Kaler

March 11, 2014
Tweet

Transcript

  1. Opinionated Core Data Hold On To Your Butts http://ParveenKaler.com pk@smartfulstudios.com

    @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 pk@smartfulstudios.com

    @kaler