THE MENTAL BLOG
THE MENTAL BLOG
2010
Core Data (CD) is a technology introduced to the Mac and iPhone in Mac OS X 10.4 and iPhone OS 3.0, respectively. It provides a means for developers to graphically define a data model, and have that mapped behind the scenes to a relational database. Previous to that, developers either had to use a custom technology like SQLite, or Cocoa’s Keyed-Archiving (KA).
Mental Case for iPhone was originally written to run on iPhone OS 2.0, and it was built on top of KA. More recently, this has become a considerable hindrance, with many users complaining about performance for large libraries. So getting from KA to CD has become an A1 priority.
Last week we finally took the plunge, and were surprised to find that Apple offered no guidance or sample code for this type of migration, even though it must be very common. Having now almost completed the transition, I thought I would blog the solution we came up with, to save others some trouble.
There are two main approaches you could take to migrating data from KA to CD. If you don’t mind introducing new classes for your model, you could leave the existing KA classes as they are, read in the whole legacy KA file, and then walk through the object tree, generating your CD objects as appropriate.
There are a few disadvantages to this approach. Firstly, you will need to make wholesale changes throughout your application, replacing code that uses the old KA classes with code that uses the new CD classes. These changes could be far reaching.
A second disadvantage is that, in all likelihood, you will have two complete copies of your data in memory during the migration: the original KA object graph, and the CD object graph you are forming. This could be a problem on a low-memory device like the iPhone.
The other approach, and the one which we settled on, was basically to do the migration in-place. Instead of creating whole new classes, we took the existing NSCoding-compliant classes, and simply made them subclasses of NSManagedObject. The encodeWithCoder: method from NSCoding need never be used again, so you can gut that, but the initWithCoder: method will be used to do the migration.
@implementation MTModelObject
@dynamic uniqueId;
-(id)initWithCoder:(NSCoder *)coder {
MTManagedObjectContext *context = [MTManagedObjectContext importingModelContext];
NSEntityDescription *entityDescription =
[NSEntityDescription entityForName:NSStringFromClass([self class])
inManagedObjectContext:context];
if ( self = [super initWithEntity:entityDescription insertIntoManagedObjectContext:context] ) {
self.uniqueId = [coder decodeObjectForKey:@"uniqueId"];
}
return self;
}
-(void)awakeFromInsert {
[super awakeFromInsert];
self.uniqueId = [[NSProcessInfo processInfo] globallyUniqueString];
}
-(void)encodeWithCoder:(NSCoder *)coder {
// static unsigned classVersionNumber = 1;
// [coder encodeInt:classVersionNumber forKey:@"modelObjectClassVersion"];
// [coder encodeObject:self.uniqueId forKey:@"uniqueId"];
// [coder encodeObject:self.modelContext forKey:@"modelContext"];
}
@end
You can see in the example above that the contents of encodeWithCoder: have simply been commented out, because it should never be needed again. In a typical initWithCoder: method, you would typically chain to the super’s init method, but here we are instead creating an object in our CD managed object context, with the NSManagedObject initializer initWithEntity:insertIntoManagedObjectContext:. To access the managed object context, we have setup a global importing context. If you want to be neater, you could probably subclass NSKeyedUnarchiver, and add the context to that as an instance variable, so that it gets passed in with the NSCoder argument. But this migration is a once off affair, so it didn’t seem so important to us to make this flexible code.
After the CD object has been created, you can then decode the data as usual, and set the properties. One thing to be careful of here: it seems the initWithEntity:insertIntoManagedObjectContext: method can return a different object to the one allocated. (That’s why we always assign to self in our initializers, right?) The problem with this is if you try to set a relationship from both sides, you’ll get an exception, because the objects won’t correspond properly. The solution is to just set relationships from one side, and let CD handle the inverse relationships.
Here’s an example:
-(id)initWithCoder:(NSCoder *)coder {
MTManagedObjectContext *context = [MTManagedObjectContext importingModelContext];
NSEntityDescription *entityDescription =
[NSEntityDescription entityForName:NSStringFromClass([self class])
inManagedObjectContext:context];
if ( self = [super initWithEntity:entityDescription insertIntoManagedObjectContext:context] ) {
self.notes = [coder decodeObjectForKey:@"notes"];
This initializer is from a container class, which stores note objects. We decode and set the notes property here, and in the original KA code, we also set the other side of the relationship — the note’s container — in the initWithCoder: method of the note class. This caused an exception. To fix the problem, we simply removed the line setting the container from the note class, and let CD handle the inverse relationship for us.
Other things to be aware of are that, as with any use of CD, dealloc methods should generally be rewritten as didTurnIntoFault methods. You might also move some of your initializer code to awakeFromFetch or awakeFromInsert.
That covers the migration, but it leaves one last piece of the puzzle, and it is a big one: how do you get your new objects to work in your existing application code? Most of our model objects have properties for simple types like float, BOOL, and int. In CD, you work with object types like NSNumber. To address this, we simply gave our CD attributes names like isEditableNumber, which would hold a NSNumber boolean value, and then used wrapper accessor methods to box and unbox the objects. The rest of the code would work with a BOOL property called isEditable, but setting this would effectively box the BOOL into an NSNumber, which would be stored in the object model. By doing this, the changes required in the rest of the app were minimal.
Keyed-Archiving to Core Data Migration
17/04/10
In the past, Mac and iPhone applications tended to use Keyed-Archiving to store their data. Core Data offers advantages to developers and users alike, but how do you go about migrating your Keyed-Archiving app to Core Data?