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

    View Slide

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

    View Slide

  3. Goaly
    CoreData
    Stack
    Fitbit
    Jawbone
    Nike+
    Runkeeper
    Health and fitness journal and dashboard
    Timeline
    Dashboard

    View Slide

  4. 1. Principles

    View Slide

  5. The Law of Leaky Abstractions
    "All non-trivial abstractions, to some degree, are leaky.”
    — Joel Spolsky

    View Slide

  6. What does Core Data abstract?
    Class Abstraction
    NSPeristentStoreCoordinator SQLite file
    NSManagedObjectModel SQLite schema
    NSManagedObjectContext SQL statements, query engine
    NSManagedObject Row in a table

    View Slide

  7. Active Record
    Principles of Enterprise Application Architecture by Martin Fowler

    View Slide

  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

    View Slide

  9. Active Record
    • NSManagedObjectContext does insert, delete, update
    • NSEntityDescription constructs a row
    • NSManagedObject does have fields though

    View Slide

  10. Magical Record?
    https://github.com/magicalpanda/MagicalRecord

    View Slide

  11. Datamapper
    Principles of Enterprise Application Architecture by Martin Fowler

    View Slide

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

    View Slide

  13. SQLite

    View Slide

  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.

    View Slide

  15. 2. Architecture

    View Slide

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

    View Slide

  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;
    }

    View Slide

  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

    View Slide

  19. Unit Testing
    @interface GoalyTests : XCTestCase
    @property (nonatomic, strong) GLYManagedObjectContext *context;
    @end
    !
    @implementation GoalyTests
    !
    - (void)setUp
    {
    [super setUp];
    self.context = [GLYManagedObjectContext createAt:nil];
    }
    !
    @end

    View Slide

  20. Unit Testing
    • Fat Model, Skinny Controller
    • Model shouldn’t import UIView or UIViewController
    • EVERYTHING else should be in the Model

    View Slide

  21. Multiple Stores
    • Per HTTP service
    • Static data like a food database
    • Per user like username.sqlite

    View Slide

  22. Multiple Contexts
    • UI NSManagedObjectContext with NSMainQueueConcurrencyType
    • Detail view NSManagedObjectContext for save/cancel operations
    • NSManagedObjectContext HTTP client for GET requests with lots of
    inserts

    View Slide

  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();
    }
    }];
    }

    View Slide

  24. 3. Classes

    View Slide

  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

    View Slide

  26. NSManagedObject
    TEST THAT SHIT! YOU CAN DO IT!

    View Slide

  27. Use the API Console

    View Slide

  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];
    }

    View Slide

  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

    View Slide

  30. NSFetchRequest
    @interface GLYSleepFetchRequest : NSFetchRequest
    + (GLYSleep*)find:(int64_t)logId;
    + (GLYSleep*)findOrCreate:(int64_t)logId;
    + (NSArray*)findOrCreate:(NSArray*)logIds;
    @end

    View Slide

  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];

    View Slide

  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).

    View Slide

  33. 4. ViewControllers

    View Slide

  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

    View Slide

  35. Creation/Deletion
    • Alloc/init your CoreData stack in the signin/signup view controller
    • Release your CoreData stack in the signout view controller

    View Slide

  36. TEST THAT SHIT! YOU CAN DO IT!

    View Slide

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

    View Slide