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. What does Core Data abstract? Class Abstraction NSPeristentStoreCoordinator SQLite file

    NSManagedObjectModel SQLite schema NSManagedObjectContext SQL statements, query engine NSManagedObject Row in a table
  2. 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
  3. Active Record • NSManagedObjectContext does insert, delete, update • NSEntityDescription

    constructs a row • NSManagedObject does have fields though
  4. 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.
  5. 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; }
  6. 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
  7. Unit Testing @interface GoalyTests : XCTestCase @property (nonatomic, strong) GLYManagedObjectContext

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

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

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

    NSManagedObjectContext for save/cancel operations • NSManagedObjectContext HTTP client for GET requests with lots of inserts
  11. 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(); } }]; }
  12. 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
  13. 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]; }
  14. 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
  15. +(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];
  16. +(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).
  17. 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
  18. Creation/Deletion • Alloc/init your CoreData stack in the signin/signup view

    controller • Release your CoreData stack in the signout view controller