Core Data - mixing lightweight and custom migration - ios

So I have version 1 of my Core Data app in the App Store and now I started working on version 2.
I have some small changes to my data base model, and some custom code I need to run after those changes to complete the upgrade from version 1 to version 2.
I can use Core Data's lightweight migration to deal with the model change, and I can run my custom code after the migration is done.
The problem is, i'm not sure what will happen in the future, when I'll build version 3,4,5...
suppose this is the case:
version 1 to version 2 - use lightweight migration
version 2 to version 3 - use custom migration with model mapping
version 3 to version 4 - use lightweight migration again
and so on..
I'm not sure how to build a mechanism that deal with this mix of lightweight and custom migrations.
I couldn't find any code online or in Core Data docs that talk about this issue, I mean this is a very common issue for most of the Core Data apps out there, is there any best practice examples for this issue?

Two methods are written below:
I personally like this method since it's just genius IMO, you can tell from the detail of my explanation why I'm so excited about this method. This method has been written in Marcus Zarra's core data second edition book. It's an implementation for an automatic Progressive migration process for both heavy and lightweight use.
A method I came up with for a possible implementation after the OP asked if there was a way to avoid having to use mapping models for each migration upgrade
Method 1
This is where Progressive Migration comes in. You create mapping models for each version upgrade that will deal between two model versions. A mapping model will tell the migration manager how to map an old version of the data store to the new version of the data store. So a mapping model requires a source and a destination.
You then have as many mapping models required that will cover all the version upgrade increments. So if you have 4 versions, youll have 3 mapping models that will aid the transition between each version upgrade. In the end youll have them named: V1toV2MappingModel, V2to3MappingModel, V3toV4MappingModel.
In each mapping model you can then toggle the options to let the migration manager know how you wish to migrate. Whether its via a custom policy or a normal copy migration. So you say that for v1 to v2 you require a light weight migration, simply select the appropriate mapping model, you then use the model/mapping editor to make the desired connections/mappings between old attributes to new attributes and if its a custom migration required, then go to the right mapping model, choose the product entity mapping that you wish to customise the migration for, then in the entitymapping inspector you'll see that you can apply a custom migration policy, just copy the name of your migration policy say for example MigrationPolicyV2toV3 since its that particular version that you wanted to have customised.
So in the image above you can see on the left hand side in the name of the mapping model that it's for Version 1 to Version 2. Notice that the attribute mapping for the ProductToProduct entity mapping is empty - in the middle. This is because if you look on the right of the image in the entity mapping inspector where it says Custom Policy, I've put the name of my migration policy. This then lets the migration manager know (during the migration process) how to map the attributes and values from the source to the destination - version 1 to version 2 - and it knows this because of the migration policy inputted. This also causes the value type to be changed to custom. letting you know its going to be a custom migration when it comes to the ProductToProduct entity mapping.
Then you have to define in your migration policy which will determine how you wish to copy the values over. This is where you do your custom stuff.
As you can see from the image above, its the custom migration policy I've set for the ProductToProduct entity mapping. You'll notice that I'm not actually doing anything custom, all this could have been done without the migration policy and could have been achieved by simply doing a copy migration (lightweight migration) by adjusting a few values in the entityMapping inspector and adjusting the Attribute mapping values in the image before. I only did all of this migration policy custom stuff just as an exercise so that I can learn and be prepared for the future just INCASE I ever needed to do a heavy migration. Better learn it now than later hey ;)
That's it for doing custom stuff. I suggest you read up on the following apple developer reference on NSEntityMigrationPolicy and the methods required to do more custom stuff so that you know how to have full control throughout the migration process whenever a particular revision upgrade requires some or full custom migration.
And for any custom/heavyweight migrations - where in my case I use a migration policy so that I can do some custom code stuff during the migration from V2 to V3 in my data store - then you create something called a 'migration policy' so that THAT mapping model will adhere to the custom migration rules your specify.
And you simply apply all the appropriate transitions/mappings for each mapping model so that the migration manager knows how to upgrade from one store to the next.
All you need then is some recursive code that will look at the existing store's meta data to determine whether it's compatible with the most current version, and if its not, it will then do a recursive migration for you, automatically, following the rules from each mapping model as it upgrades through the versions until the store is up to date with the current version.
So this way you can accommodate all users with any version and have them brought up to speed to the current version's store. So if a user is at version 1, it will recursively go from V1 to V2, then migrate to v3 all the way up to your current version. The same applies if the user has any other version.
This does mean that it will take a little longer in the migration process, but its a good way of migrating all your users no matter which version they have.
To find this progressive migration code, you need to read a book called Core Data 2nd Edition - Data storage and management for iOS, OS X, and iCloud, Chapter 3 Versioning and Migration sub chapter 3.6 Progressive Data Migration (An Academic Exercise) from pages 54 to 59.
He talks you through the code,and tells you step by step how to write the progressivelyMigrateURL:ofType:toModel:error: method. Once you've written the method with him, he then tells you how to call this method on application startup so that your users can have their stores automatically migrated progressively too.
So you should probably write the method first, then follow my steps up above or you can read through migration policies in the subchapters before.
I practically learned all this in the last 48 hours and have it all up and running now. Able to do lightweight migrations for some versions and then have custom migrations for my other versions all done automatically.
Method 2 - Another solution albeit more cumbersome IMO: You can always have lightweight migration setup bearing in mind that you apparently can achieve even complex situations with lightweight migration. But in the instances where heavy migration is required for a specific version upgrade, you can always do if statements where you can check the current store, and if and ONLY if the current version of the persistent store matches an upgrade where heavy migration is required, you then tell the migration manager to perform a heavy migration and to follow a mapping model with a migration policy for that instance only, and then resume lightweight migration if there are more version upgrades to do to get the persistent store to the most recent model version.

If you can refetch data from a server or anything like that, the best way to deal with this is by removing your old model and immediately recreating a new one with the current structure. This is embedded in a singleton class and is called every time a new managedobjectcontext has to be created. My code for this is the following:
- (NSManagedObjectContext *)managedObjectContext {
if (__managedObjectContext != nil) {
return __managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
__managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[__managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return __managedObjectContext;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (__persistentStoreCoordinator != nil) {
return __persistentStoreCoordinator;
}
NSURL *storeURL = [[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:#"YOURDB.sqlite"];
NSError *error = nil;
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
if (error.code == 134100) {
if ( [[NSFileManager defaultManager] fileExistsAtPath: [storeURL path]] ) {
NSDictionary *existingPersistentStoreMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType: NSSQLiteStoreType URL: storeURL error: &error];
if ( !existingPersistentStoreMetadata ) {
// Something *really* bad has happened to the persistent store
[NSException raise: NSInternalInconsistencyException format: #"Failed to read metadata for persistent store %#: %#", storeURL, error];
}
if ( ![[self managedObjectModel] isConfiguration: nil compatibleWithStoreMetadata: existingPersistentStoreMetadata] ) {
if (![[NSFileManager defaultManager] removeItemAtURL: storeURL error: &error] ) {
NSLog(#"*** Could not delete persistent store, %#", error);
abort();
} else {
[__persistentStoreCoordinator addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: storeURL options: nil error: &error];
}
}
}
} else {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
return __persistentStoreCoordinator;
}

The simplest way is keeping old attributes in new version and not using them anymore. if you need map them to new attributes, just do the mapping after lightweight DB upgrade. The world will keep peaceful.
The benefits:
no need to make mapping model.
keep using light weight upgrade.
Let me give you example:
let's say we have a entity named Student with attribute name in V1, and we gonna map it to a new attribute firstName in V2.
and we have a method as below, so then we can call this method after regular lightweight CoreData upgrade, enjoy!
- (void)migrateStudentRecords {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Student"];
NSArray *result = [defaultContext executeFetchRequest:fetchRequest error:error];
if (result && result.count > 0) {
[result enumerateObjectsUsingBlock:^(Student *_Nonnull newRecord, NSUInteger idx, BOOL * _Nonnull stop) {
newRecord.firstName = newRecord.name;
newRecord.name = nil;
}];
if (defaultContext.hasChanges) {
[defaultContext save:error];
}
}
}

Related

Remove core data model on iOS app update

My question is related to migration. I cannot do a lightweight migration as there are a lot of changes with attribute types and new relationships. I don't have time for a heavy weight migration since the code is not mine and needs faster delivery.
The workaround, which could work is when the app is upgraded, app should remove the old data and data model as the data is of no use and can be downloaded from the server again. On the app did finish launching, get the .db URL and just remove it and recreate it for the very first time after the upgrade?
After some research, all the methods are pointed to light weight migration. If there is a better way please assist.
-(void) removeCoreDataAndReset{
NSError *error;
NSPersistentStoreCoordinator *storeCoordinator = storeCordinator;
for (NSPersistentStore *store in storeCoordinator.persistentStores) {
[storeCoordinator removePersistentStore:store error:&error];
[[NSFileManager defaultManager] removeItemAtPath:store.URL.path error:&error];
}
// Initialise managedobjectcontext , Store Coordinator etc
}
Reinitialise all after this method as you do in statrt
To remove the persistent store, you need to remove:
The actual persistent store file. This is located wherever you put it. You tell Core Data where it is when you call addPersistentStoreWithType:configuration:URL:options:error:, so if you're not sure, check there.
The journal files. These will have the same name as the persistent store file, but with -wal and -shm added to the end. This is very important, because in most cases nearly all of the existing data is in these files.
You can remove files with methods on NSFileManager. If you do this, do it before accessing Core Data in any way, i.e. before creating any Core Data objects of any kind.

CoreData: How to correctly use migratePersistentStore to create a backup copy

I'm really struggling with this. I'm trying to create a backup of my active core data base. According to Apple the best option is not to use the File Manager but the method migratePersistentStore. However I don't really understand this. I have my PersistentStoreCoordinator in my AppDelegate. So if I'm migrating the persistent store my coordinator will lose it after successfully moving it correctly? So the store is now just at the new location but not at the old anymore? So do you have any example program code for this of how my app still could keep running with the original copy?
Or can't i just copy all files using the filemanager with the same prefix instead of migrating?! So much easier...
You can create a separate NSPersistentStoreCoordinator for migration only, and continue using your regular one for CoreData stack. Also you can use NSMigrationManager for migration:
NSMigrationManager* manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel
destinationModel:targetModel];
BOOL migratedSuccessfully = [manager migrateStoreFromURL:sourceStoreURL
type:type
options:nil
withMappingModel:mappingModel
toDestinationURL:destinationStoreURL
destinationType:type
destinationOptions:nil
error:error];
Also I'm not sure you can migrate when your DB is opened, probably you'll need to lock it or something.

Configuration for RestKit in-memory Core Data store

I need to split my Core Data persistent storage (managed by RestKit) into two parts. One part should persist only in memory and not be saved to disk, and the other part should be saved. Usually it is done by adding configurations to Core Data object model and creating two stores for each configuration. But RestKit's RKManagedObjectStore method - (NSPersistentStore )addInMemoryPersistentStore:(NSError *)error; doesn't take configuration name and adds persistent store with configuration nil:
- (NSPersistentStore *)addInMemoryPersistentStore:(NSError **)error
{
if (! self.persistentStoreCoordinator) [self createPersistentStoreCoordinator];
return [self.persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:error];
}
According to Core Data documentation, this prevents from using any configurations in persistent store coordinator.
I can reload this method and make it use configuration, but first I want to ask:
Are there any reasons for me to not to do this? There must be reasons why addInMemoryPersistentStore looks like it looks. May be someone had tried to make the same thing as I'm going to do and found that it doesn't work?
I would say that you should create your own version of the method which takes a configuration parameter. I don't think that you're missing anything, I just think that if RestKit was to offer support for 100% of the Core Data configuration API out of the box then there would be a lot of code to maintain and very few people would benefit from all of the extra effort.
Your best option is to subclass and add the method you want, calling super as appropriate, and then instantiate that subclass and pass it when you are configuring your Core Data stack (managed object store).

iOS - Keep the old sqlite database while updating to new version

I've found some other question but I didn't get any clear idea how to keep the data from old database while application update in ios.
Case 1:
Can I keep the old database?
if Case 1 is YES:
Can I insert new column or doing any changes in the old database and will it be safe?
if case 1 is NO:
Can I get the old database data in new database? Will the old database will be removed?
Case 2: If I give a different name to new data base (it'll be included in bundle)? If giving a new name keeps the old database can I delete the old database programatically?
What will be the best practice? Give a new name to database file for keeping the old one and then copy the old to to new database and delete the old database file? Just start using the old one?
Looking for help.. :)
Case 1: Can I keep the old database?
Yes, updating your application won't delete the database file stored in the documents directory. (But if it is in your bundle, it'll be removed)
if Case 1 is YES: Can I insert new column or doing any changes in the old database and will it be safe?
That'll depend on your implementation. You can use ALTER TABLE query for adding or removing column. You need to handle the new changes in your code, else it can cause problems.
Adding a column won't cause any issues in normal scenarios (Depends on your insert query statements and new field constraints)
It's fairly Case 1 as assuming you have copy your .sqlite file to document folder and when you update the application it will look for the database the can update its table and update its database without loosing anything.
If I understand your question (you're looking to update a model that's already deployed via the App Store), yes you can perform upgrades to an existing model using the .xcdatamodeld format in Xcode. The Core Data Versioning doc from Apple covers this topic comprehensively.
This can be a fiddly process, if you have precious user data stored in your model, you'll want to test this exhaustively before pushing out your updates.
To add a new version to your model;
Select your xcdatamodel file (e.g. model.xcdatamodel)
Click Editor > Add Model Version
Name the new model version (e.g. Model 2), based on Model 1
Open the Utilities pane
Select 'Model 2' for the Current Model Version
Then add this method to your controller class implementation file, to help make small changes to the data model (changing field types, adding columns etc).
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Model.sqlite"];
NSError *error = nil;
// for performing minor changes to the coredata database
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],
NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES],
NSInferMappingModelAutomaticallyOption, nil];
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
Also, worth noting: Apple recommends you only store user relevant files in the Documents folder. SQLite database files and similar should be stored in /Library/Application Support.

Adding a new entity to coredata - can I still use lightweight migration?

I have an existing core data set and I want to add an entity to it. I'm a little confused over whether I can use lightweight migration after adding a new entity to transition existing users to the new model.
The current modal is (just showing entities):
Story 1toMany-> Sentences
I need:
Story 1toMany-> Sentences 1toMany-> Media
Can I use the lightweight migration tool to do this?
I've read in the documentation:
For Core Data to be able to generate an inferred mapping model,
changes must fit an obvious migration pattern, for example:
Simple addition of a new attribute Removal of an attribute A
non-optional attribute becoming optional An optional attribute
becoming non-optional, and defining a default value Renaming an entity
or property
But this question seems to suggest lightweight migration will still work with the addition of an entity. Since the new media entity is optional I can't see how it would practically be a problem.
Yes, you will likely be able to use lightweight migration. In my experience, I've found you'll want to Add Model Version... under the Editor menu before you make changes to your CoreData Model. This way there is a before and after scenario to map. Then, you'll need to set the new model as the current model. (You can now add an entity to the Core Data model. Be sure that you are working on the correct model.)
Finally, you need to make sure you pass options for initializing the PersistentStoreCoordinator.
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool: YES],NSMigratePersistentStoresAutomaticallyOption,[NSNumber numberWithBool:YES],NSInferMappingModelAutomaticallyOption, nil];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {...
Check the core data video "Mastering core data" from wwdc 2010. They talk about migration for your specific case. Long story short: yes, you can use lightweight migration. Just pass the options dictionary when initializing the NSPersistentStoreCoordinator instance:
NSDictionary *dictionary=[NSDictionary dictionaryWithObjects:#[ [NSNumber numberWithBool:YES], [NSNumber numberWithBool:YES]] forKeys:#[ NSMigratePersistentStoresAutomaticallyOption, NSInferMappingModelAutomaticallyOption]];

Resources