Friday, February 24, 2017

Lightweight Migration Example

My existing app TasteBank is a personal taste tracking app.  It originally allowed you to enter a list of restaurants and track details about the dishes you ate at that restaurant.  I decided I wanted to make it a more generic taste tracker.  I wanted to add categories to the app so you could track your tastes not just in dishes at restaurants, but also your taste in wines, or coffee.

Here is the core data diagram for before:


And after:



So I basically added 1 new entity called Category and some new attributes.

According to apple docs, in order for me to merge these two different version of my data model, I would need to perform a lightweight migration.  Adding attributes or entities can usually be done using lightweight migrations.

I wanted to make sure that the existing data would not be overwritten and that the list of restaurants and dishes would just be moved to a category called Restaurants.

The following is a step by step list of what I had to do to complete this successfully.  There are many examples of the first few steps on the internet already, the last few steps are specific to my data.

Create a new database model version:

1. Click on my database .xcdatamodeld file in project navigator
2. Go to Editor in menu bar, and select Add Model Version...
3. Name the version of data model, I used TastebankVer2 and hit Finish
4. A little green arrow shows which version of the data model is currently being used, to make the new version the current active version, you click on the data model you just added, then go to utilities->Model Version->select current version in drop down list



Add code to enable lightweight migration:


1. Open App delegate file
2. Go to addPersistentStoreWithType:configuration:URL:options:error: method

Apple's suggested code shows:






NSError *error = nil;
NSURL *storeURL = <#The URL of a persistent store#>;
NSPersistentStoreCoordinator *psc = <#The coordinator#>;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
    [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
 
BOOL success = [psc addPersistentStoreWithType:<#Store type#>
                    configuration:<#Configuration or nil#> URL:storeURL
                    options:options error:&error];
if (!success) {
    // Handle the error.
}

Here is my persistentStoreCoordinator code:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }
    NSError *error = nil;

    NSURL *storeURL =                        
        [[self applicationDocumentsDirectoryURLByAppendingPathComponent:@"TasteB.sqlite"];
    
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:         [self managedObjectModel]];

    BOOL success = [ _persistentStoreCoordinator       
        addPersistentStoreWithType:NSSQLiteStoreType 
        configuration:nil URL:storeURL    
        options:@{NSMigratePersistentStoresAutomaticallyOption:@YES,      
        NSInferMappingModelAutomaticallyOption:@YES} error:&error];
        
    if (!success) {
    
         NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    
    return _persistentStoreCoordinator;
}

At this point the lightweight migration is activated and should work right away for adding attributes and stuff.

Copy old data into new structure

However I also wanted my existing data restructured.  I already had a list of restaurants and dishes in the old version of my app.

    ---------->      



When moving to the new version, I wanted all of this data moved under a new category called Restaurants, and I needed to update the category attribute on the Restaurant entities.
      ----->    ------>  

I put this code in my App delegate:

-(void)makeCategory{
    NSError *error = nil;
    
    NSFetchRequest *fetch = [[NSFetchRequest allocinitWithEntityName:@"Category"];
    
    NSUInteger numberOfcategories = [self.managedObjectContext countForFetchRequest:fetch        
        error:&error];
    
    //if there are no existing categories, then create a new one called Restaurants
    if( numberOfcategories == 0 ){
        NSLog(@"Categories not found, creating Restaurant Category");

        Category *c=[NSEntityDescription insertNewObjectForEntityForName:@"Category"    
            inManagedObjectContext:self.managedObjectContext];
        c.name=@"Restaurants";
        
        //if there are existing restaurants then set their category attribute to point to the new         //Restaurant category

        NSFetchRequest *fetch1 = [[NSFetchRequest allocinitWithEntityName:@"Restaurants"];
        if ([self.managedObjectContext countForFetchRequest:fetch1 error:&error] >0) {
            
            //make the new restaurant category relation

            NSArray *fetchedObjs= [self.managedObjectContext executeFetchRequest:fetch1 
               error:&error];
            for (Restaurants *r in fetchedObjs) {
                r.category=c;
                
            }
        
        }
    }
}

I called this function from didFinishLaunchingWithOptions: method . 


References:
https://the-nerd.be/2012/02/05/how_to_do_a_lightweight_core_data_migration/
https://www.objc.io/issues/4-core-data/core-data-migration/