Yalantis: iOS, Android And Web App Development Company

8 Tips on CoreData Migration

Iterative app development always entails CoreData migration. Working with iterative development, we’ve learned a number of lessons about CoreData migration.  Here are some helpful notes, a few warnings, and a number of useful techniques for our technology savvy readers.

Of course, we won’t be able to completely break down migration step-by-step, as that would take several articles.  And additionally, there’s no need to copy what’s already been said in the excellently written CoreData Versioning & Migration Guide. Our goal here is to share a bit of our firsthand experience.

Why migrate at all?

Since most  development is done in an Agile manner, this approach is  aimed at introducing a lot of changes from version to version. Sometimes these changes are planned and sometimes they’re not. Either way, alterations to the requirements or API changes are supposed to improve an app’s data model. Therefore, you need to perfectly adapt all those changes from version to version without losing user data ( sad, but true).

No doubt, a growing complexity and new techniques used in the process of migration are highly dependent on the persistence framework. In this post we won’t talk about numerous interesting solutions like Realm, Coachbase or YapDatabase, but will focus on the well-known CoreData.

Scheme Migration

As well all know, a model can change significantly during the development process.  We’re talking about .xcdatamodel update, either a completely different one or improved, when it comes to CoreData.

Since the source code is mostly up-to-date with the latest Model version, CoreData tries to map one representation onto another. Basically, migration is all about loading source representation, mapping it onto a representation, verifying results, and eventually saving the new representation. In other words, migration helps us keep our model compatible with changes in the source code.

Now let’s focus specifically on lightweight migration, mapping models, and custom migration policies.

Lightweight Migration

Fortunately, CoreData frees developers from error-prone, routine work by doing a lot of things under the hood for us. Out of the box, CoreData lets a developer apply minor changes to the Model without creating a database from scratch:

For Attributes:

  • Add, remove, rename an Attribute
  • Marking attribute as optional or non-optional

For relationships:

  • Add, remove, rename a Relationship
  • Changing relationship from “to-one” to “to-many,” unordered to ordered and vice-versa

For Entities:

  • Add, remove, rename an Entity
  • Change (add to or remove from) inheritance hierarchy and pull or push properties

> For a full set of features check out Lighweigh Migration Page.

Lightweight migration can be turned on by simply adding two extra flags during the store setup:

NSDictionary *options = @{
 NSMigratePersistentStoresAutomaticallyOption: @YES,
 NSInferMappingModelAutomaticallyOption: @YES
};
if (![coordinator addPersistentStoreWithType:type configuration:configuration URL:url options:options error:&error]) {
 // error-time! lets use some great UX techniques:
 abort();
}

Read also: How to move from JSON to CoreData fast and effectively

tip #1

Before changing anything always make sure you create a new model version: Editor -> Add Model Version... Otherwise, CoreData won’t be able to open the saved store and perform lightweight migration.

tip #2

Don't delete .xcdatamodels used for the published version. This follows from the first tip.

tip #3

Use Renaming ID for renamed Properties and Entities: View -> Utilities -> Show Data Model Inspector -> Edit Renaming ID. The value of Renaming ID in the destination model should be set to the name of the corresponding Property / Entity in the source model.

Custom Migration Policy

If the mapping model set by default doesn’t meet your requirements during manual migration, and some extra work needs to be done, you can use a custom migration policy.

All you need to do is specify your policy class in the Mapping Model Inspector (Select Mapping Model and then go to View -> Utilities -> Show Mapping Model Inspector).

> Check out The Migration Process Guide for a full set of options

tip #4

NSEntityDescription’s associated class is ignored during migration. For example, we have two model versions — 1.0 and an updated 1.1

1.0

 

1.1. During migration, we need to populate every Post with 3 default Attachments:

@interface YALPostMigrationPolicy_1_0_to_1_1 : NSEntityMigrationPolicy
@end

@implementation YALPostMigrationPolicy_1_0_to_1_1

- (NSSet *)newDefaultAttachmentsInManagedObjectContext:(NSManagedObjectContext *)context {
 NSMutableSet *output = [[NSMutableSet alloc] init];
 for (NSInteger i = 0; i < 3; i++) {
 YALAttachment *attachment = [NSEntityDescription insertNewObjectForEntityForName:@"YALAttachment" inManagedObjectContext:context];
 attachment.title = @"Title";
 attachment.data = [NSData dataWithBytes:&i length:sizeof(i)];

 [output addObject:attachment];
 }

 return output;
}
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sourceInstance
 entityMapping:(NSEntityMapping *)mapping
 manager:(NSMigrationManager *)manager
 error:(NSError **)error {
 BOOL created = [super createDestinationInstancesForSourceInstance:sourceInstance
 entityMapping:mapping
 manager:manager
 error:error];
 if (!created) return created;

 NSArray *mappedPosts = [manager destinationInstancesForEntityMappingNamed:mapping.name
 sourceInstances:@[sourceInstance]];
 NSParameterAssert(mappedPosts.count == 1);

 YALPost *post = mappedPosts[0];
 post.title = @"Migrated!";
 NSSet *defaultAttachments = [self newDefaultAttachmentsInManagedObjectContext:manager.destinationContext];
 post.attachments = defaultAttachments;

 return YES;
}

@end

Also, it’s important to note that during -[NSManagedObject awakeFromInsert] uuid is assigned to default value.

@implementation YALAttachment

- (void)awakeFromInsert {
 [super awakeFromInsert];

 self.uuid = [[NSUUID UUID] UUIDString];
}

@end

Some explanation:

First of all, we’re passing control to default the migration implementation by calling [super createDestinationInstancesForSourceInstance:entityMapping:manager:error:] The migrated instance is then retrieved. We also perform some manual manipulations in the post and assign generated default attachments to it. Nothing special here, right?

However, all the migrated attachments don’t have uuid after the migration. How did that happen? It’s clear that -[NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:] should return an instance of the associated class (YALAttachment in our case). In fact, it returnsNManagedObject  but notYALAttachment during migration.

As a result, -[YALAttachment awakeFromInsert]  implementation  is not invoked. Therefore, you need to be aware of the instance’s class during migration. You might wonder why Apple did this. The answer may be fairly simple: there is no guarantee, that the model represents the actual class API.

In order to write class-agnostic migration we need to omit using any custom class. Everything we have is NSManagedObject and KVC:

@implementation YALPostMigrationPolicy_1_0_to_1_1

- (NSSet *)newDefaultAttachmentsInManagedObjectContext:(NSManagedObjectContext *)context {
 NSMutableSet *output = [[NSMutableSet alloc] init];
 for (NSInteger i = 0; i < 3; i++) {
 NSManagedObject *attachment = [NSEntityDescription insertNewObjectForEntityForName:@"YALAttachment" inManagedObjectContext:context];
 [attachment setValue:[[NSUUID UUID] UUIDString] forKey:@"uuid"];
 [attachment setValue:@"Title" forKey:@"title"];
 [attachment setValue:[NSData dataWithBytes:&i length:sizeof(i)] forKey:@"data"];

 [output addObject:attachment];
 }

 return output;
}
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sourceInstance
 entityMapping:(NSEntityMapping *)mapping
 manager:(NSMigrationManager *)manager
 error:(NSError **)error {
 BOOL created = [super createDestinationInstancesForSourceInstance:sourceInstance
 entityMapping:mapping
 manager:manager
 error:error];
 if (!created) return created;

 NSArray *mappedPosts = [manager destinationInstancesForEntityMappingNamed:mapping.name
 sourceInstances:@[sourceInstance]];
 NSParameterAssert(mappedPosts.count == 1);

 NSManagedObject *post = mappedPosts[0];
 [post setValue:@"Migrated" forKey:@"title"];
 NSSet *defaultAttachments = [self newDefaultAttachmentsInManagedObjectContext:manager.destinationContext];
 [post setValue:defaultAttachments forKey:@"attachment"];

 return YES;
}

@end

As you see, this approach forces us to move custom logic to separate objects. It makes testing code much easier. However, it may lead to breaking encapsulation in some cases.

tip #5

Keep your code clean by moving deprecated APIs to the custom migration policy. Since migration policy subclass represents a “snapshot” on the versions timeline, it’s a perfect place to put deprecated APIs. However, this would require you to rewrite deprecated functions in correspondence with KVC usage without any direct messaging.

Migration

tip #6

Do as little as possible during migration.

Due to the change in the way the preview is stored, migration took about 12 seconds for 1k of records in one of the apps we developed! Time Profiler happened to be using a huge amount of UIImage rendering, which was part of the logic for the destination model. In order to speed it up, we did a little trick — moved the preview to the private destination path directly, instead of using public API. It saved us about 11 seconds!

Why is this important? Users don’t want to wait. They are likely to reboot the app after 5-10 seconds of waiting. Obviously, breaking migration leads to even more complex issues, such as losing user-data. Therefore, do as few manipulations as possible during migration.

Read also: Code Review via GitLab merge requests

tip #7

Use progressive migration.

Imagine your app has 4+ versions (1.0, 1.1, 1.2 and 2.0) which are all being used simultaneously. Some of your users haven’t updated the app yet. If we do migration manually, then the number of mapping models equals the sum of n — 1, where “n” is the number of released versions. Therefore, adding a new version becomes a real nightmare.

On the other hand, progressive migration leads you to n-1 mapping models. As a result you have 6 versus 3 models for 4 released versions.

We won’t cover the implementation of progressive migration in this article, but for now check out objc.io custom migration and SLCoreDataStack internals.

tip #8

Initialize your app wisely.

From our experience it’s best to initialize an app’s resources on the background thread. This allows us to perform migration and other time-consuming work without the fear of being terminated by watchdog. Here’s an example of a simplified app init:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
 // 1
 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 // 2
 self.window.rootViewController = [[YALInitializationViewController alloc] init];

 [self.window makeKeyAndVisible];

 // 4
 dispatch_queue_t initQueue = dispatch_queue_create("com.bundle.identifier", DISPATCH_QUEUE_SERIAL);
 dispatch_async(initQueue, ^{
 SLCoreDataStack *stack = [SLCoreDataStack sharedInstance];
 // 5
 if (stack.requiresMigration) {
 NSError *error = nil;
 // 6
 if (![stack migrateDataStore:&error]) {
 DLog(@"%@", error);
 abort(); // happy holidays!
 }
}

 // intialize the rest

 dispatch_async(dispatch_get_main_queue(), ^{
 // 7
 self.window.rootViewController = [[YALTimelineViewController alloc] init];
 });
 });

 return YES;
}
  

In this example we’re using SLCoreDataStack, which encapsulates stack setup and progressive migration and handles merging policies between main and background contexts out of the box.

Description:

  • Alloc/init key window.
  • Assign an empty view controller, that shows progress HUD as a root view controller of the key window.
  • Create dispatch queue for actual initialization.
  • Check whether migration is required or not.
  • Launch actual migration. At this point there are a few extras you need to add: error handling and progress indication. Unfortunately, by default you are unable to grab migration progress. However, in our internal fork there is access to NSProgress instance, which reflects progress value.
  • Continue application flow as usual.

As a result you’re getting more control over the init flow. But it brings complexity to the initialization during State Restoration.

Read also: Loose coupling using Dependency Injection and Service Locator

That’s it for today. As you can see, it can be tricky to maintain a project when it comes to versioning, migration and backwards compatibility. However, CoreData really simplifies  the process of the migration. We hope our experience with CoreData migration is helpful! Let us know if you have any questions!

Useful links:

Insights

How Much Does It Cost to Build a Marketplace App Like Etsy?

Insights

Medical App Development: How to Build a Logbook App for Electronic Health Records

Tech

How to Achieve Loose Coupling?