The latest app uses Core Data to manage a small database (only four entities in the model) for keeping track of a set of documents and related stuff. I was dreading having to really get to grips with Core Data, having skirted around it a few times in the past and been put off by the huge volume of very dull-looking databasey documentation that surrounds it.
Shouldn’t have worried. It’s a doodle. And actually almost fun to use! Yep, databases and fun are just about compatible, after all.
Quick summary, as I’m bound to forget the ins and outs very rapidly – I’ve got to get back to the game programming stuff, it’s getting itchier by the week and is definitely overdue a good scratch.
There are four main objects in the Core Data “stack”:
- the PSC (“persistent store controller”) – the connection to the underlying database, implemented by
NSPersistentStoreController
- the MOM (“managed object model”) – the database schema, implemented by
NSManagedObjectModel
- the MOC (“managed object context”) – the in-memory working copy of the database, implemented by
NSManagedObjectContext
- the FRC (“fetched results controller”) – used to obtain sorted lists of a given entity, implemented by
NSFetchedResultsController
- MOs (“managed objects”) – the actual data, implemented by
NSManagedObject
Generally you have single instances of the first three living in some convenient central location (the AppDelegate, in other words), implemented as readonly lazily-instantiated properties.
The PSC is initialised with the the URL of the database, e.g.
dbURL = [NSURL fileURLWithPath:[[self applicationDocumentsDirectory]
stringByAppendingPathComponent: @"ODW.sqlite"]];
You use the data modelling tool in Xcode to build up the MOM, and you make it accessible to code like this:
mom = [NSManagedObjectModel mergedModelFromBundles:nil];
The MOC is created using the PSC:
NSPersistentStoreCoordinator *psc = [self persistentStoreCoordinator];
if (psc != nil) {
moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:psc];
}
To obtain a set of entities, use an FRC. First define a getter the same way as for the PSC, MOM and MOC:
- (NSFetchedResultsController*)notesFRC {
if (notesFRC != nil) {
return notesFRC;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Note" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"projectref" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:@"Notes"];
frc.delegate = self;
notesFRC = frc;
return notesFRC;
}
Next, use the FRC’s fetchedObjects property:
- (void)showNote:(Note *)note {
NSLog(@"ViewController.showNote: %@ %@", note.projectref, note.timestamp);
NSArray* notes = self.notesFRC.fetchedObjects;
if ([notes containsObject:note]) {
[self showNoteViewForNote:note];
} else {
NSLog(@"ARGH - couldn't find note: notes=%@", notes);
abort(); // dunno what to do about this yet
}
}
Piece of cake, isn’t it? OK, the error handling leaves a bit to be desired, but you get the idea…
To insert new entities into the database you need to get the entities’ descriptions from the MOC and tell the MOM to create new instances. Any relationships between entities need to be set up:
// create a new Note
NSEntityDescription *noteDesc = [NSEntityDescription entityForName:@"Note" inManagedObjectContext:self.moc];
Note *note = [NSEntityDescription insertNewObjectForEntityForName:[noteDesc name] inManagedObjectContext:self.moc];
note.client = [UserDefaults instance].defaultClient;
note.projectref = [[UserDefaults instance] makeNewDefaultProjectRef];
note.subject = [UserDefaults instance].defaultSubject;
note.timestamp = [NSDate date];
// note.present is derived from the relationship between the Note and Person tables
NSEntityDescription *personDesc = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.moc];
Person* person = [NSEntityDescription insertNewObjectForEntityForName:[personDesc name] inManagedObjectContext:self.moc];
person.name = [UserDefaults instance].defaultPerson;
[note addPresent:[NSSet setWithArray:[NSArray arrayWithObject:person]]];
[person addNotes:[NSSet setWithArray:[NSArray arrayWithObject:note]]];
NSError *error = nil;
if (![self.moc save:&error]) {
NSLog(@"ERROR saving new Note: %@", [error localizedDescription]);
abort();
}
The Note and Person classes are derived from NSManagedObject and generated by Xcode. Any changes to instances of these classes result in the FRC’s delegate being informed:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
NSLog(@"ViewController.controllerDidChangeContent: %@", controller);
// save the changes
NSError* error = nil;
if (![moc save:&error]) {
NSLog(@"ERROR saving changes to database: %@", [error localizedDescription]);
abort();
} else {
NSLog(@"DATABASE WAS UPDATED");
}
}
And that’s pretty much it for database access via code.
A couple of handy debug hints:
1. To see what’s actually going on with the underlying SQLite database, add the following command-line argument to the Run scheme:
-com.apple.CoreData.SQLDebug 1
2. To see what’s actually in the underlying SQLite database, fire up sqlite3 against the file. This is easier to do using the Simulator, as all you have to do is cd to your app’s Documents directory and run sqlite3 against the file. Use the .dump command to see what’s in the database:
blaptop2:~ al $ cd /Users/al/Library/Application\ Support/iPhone\ Simulator/5.0/Applications/16F83964-62D2-43BB-80DA-08FFD528DEC5/Documents
blaptop2:Documents al$ sqlite3 ODW.sqlite
SQLite version 3.7.7 2011-06-25 16:35:41
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE ZNOTE ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZPEN INTEGER, ZTIMESTAMP TIMESTAMP, ZCLIENT VARCHAR, ZPROJECTREF VARCHAR, ZSUBJECT VARCHAR );
INSERT INTO "ZNOTE" VALUES(1,1,10,NULL,350514572.612056,'Bobbins plc','JAWZ002','Wobblage');
INSERT INTO "ZNOTE" VALUES(2,1,18,NULL,350564320.877214,'Bobble','JAWZ001','Silliness');
INSERT INTO "ZNOTE" VALUES(3,1,21,NULL,350564456.552048,'cats','JAWZ101','cat flap thing');
CREATE TABLE Z_1PRESENT ( Z_1NOTES INTEGER, Z_3PRESENT INTEGER, PRIMARY KEY (Z_1NOTES, Z_3PRESENT) );
INSERT INTO "Z_1PRESENT" VALUES(1,1);
INSERT INTO "Z_1PRESENT" VALUES(1,3);
INSERT INTO "Z_1PRESENT" VALUES(2,5);
INSERT INTO "Z_1PRESENT" VALUES(3,8);
INSERT INTO "Z_1PRESENT" VALUES(3,9);
INSERT INTO "Z_1PRESENT" VALUES(3,10);
INSERT INTO "Z_1PRESENT" VALUES(3,12);
INSERT INTO "Z_1PRESENT" VALUES(2,13);
CREATE TABLE Z_1TODO ( Z_1NOTE INTEGER, Z_4TODO INTEGER, PRIMARY KEY (Z_1NOTE, Z_4TODO) );
CREATE TABLE ZPEN ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZPENWIDTH INTEGER, ZPENCILTYPE VARCHAR, ZPENCOLOUR BLOB );
CREATE TABLE ZPERSON ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZNAME VARCHAR );
INSERT INTO "ZPERSON" VALUES(1,3,1,'Al Pearson');
INSERT INTO "ZPERSON" VALUES(2,3,2,'Fred Bloggs');
INSERT INTO "ZPERSON" VALUES(3,3,1,'Sid');
INSERT INTO "ZPERSON" VALUES(4,3,2,'Al Pearson');
INSERT INTO "ZPERSON" VALUES(5,3,1,'Fred Flintstone');
INSERT INTO "ZPERSON" VALUES(6,3,2,'Barney Rubble');
INSERT INTO "ZPERSON" VALUES(7,3,2,'Al Pearson');
INSERT INTO "ZPERSON" VALUES(8,3,1,'Flo');
INSERT INTO "ZPERSON" VALUES(9,3,1,'Mr Norris');
INSERT INTO "ZPERSON" VALUES(10,3,3,'me');
INSERT INTO "ZPERSON" VALUES(11,3,2,'Al');
INSERT INTO "ZPERSON" VALUES(12,3,1,'Will');
INSERT INTO "ZPERSON" VALUES(13,3,1,'Wilma Flintstone');
CREATE TABLE ZTODO ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZTIMESTAMP TIMESTAMP, ZDETAILS VARCHAR );
CREATE TABLE Z_PRIMARYKEY (Z_ENT INTEGER PRIMARY KEY, Z_NAME VARCHAR, Z_SUPER INTEGER, Z_MAX INTEGER);
INSERT INTO "Z_PRIMARYKEY" VALUES(1,'Note',0,3);
INSERT INTO "Z_PRIMARYKEY" VALUES(2,'Pen',0,0);
INSERT INTO "Z_PRIMARYKEY" VALUES(3,'Person',0,13);
INSERT INTO "Z_PRIMARYKEY" VALUES(4,'ToDo',0,0);
CREATE TABLE Z_METADATA (Z_VERSION INTEGER PRIMARY KEY, Z_UUID VARCHAR(255), Z_PLIST BLOB);
INSERT INTO "Z_METADATA" VALUES(1,'4C7BE92C-6C67-4B81-AD20-2E683F8CD647',X'62706C6973743030D601020304050607090A1314155F101E4E5353746F72654D6F64656C56657273696F6E4964656E746966696572735F101D4E5350657273697374656E63654672616D65776F726B56657273696F6E5F10194E5353746F72654D6F64656C56657273696F6E4861736865735B4E5353746F7265547970655F10125F4E534175746F56616375756D4C6576656C5F10204E5353746F72654D6F64656C56657273696F6E48617368657356657273696F6EA10850110182D40B0C0D0E0F10111254546F446F5350656E56506572736F6E544E6F74654F102050D605BDF65E5153589B7E7D6A7ACBA3AF5EC01FF74F4FBCDCF1338B4FA405474F102082DF4AEE8DB079BF78A8C8992C1528B0990FD0FA9CF10C2FA397473A6A05B7BD4F1020241ECFCF14A741B7EBA752DF9212490FAB646C462BC4B03649CB41C0394830B24F1020F2F8F869CBDB18B7B600B1C35CFCC4FB5766D322D8C220309E275770068750485653514C6974655132100300080015003600560072007E009300B600B800B900BC00C500CA00CE00D500DA00FD012001430166016D016F0000000000000201000000000000001600000000000000000000000000000171');
CREATE INDEX ZNOTE_ZPEN_INDEX ON ZNOTE (ZPEN);
COMMIT;
sqlite> .quit
blaptop2:Documents al$