Core Data: How to handle new versions? - objective-c

I've released an app with a Core Data sqlite database. In the new version of my app, I've created a new "Model Version" of my xcdatamodel in XCode. In the new version an entity is removed and some new attributes are added to one of the entities.
When updating to the new app version, I'm getting this sql error:
The model used to open the store is incompatible with the one used to create the store
How can I handle this error? All data in the database are downloaded from the web, so maybe the easiest way is to just delete the current sqlite file when this error occurs and start from scratch -- but what do people do when the database contains data that cannot be regenerated?
SOLUTION:
I've created an Mapping Model in Xcode and changed my persistentStoreCoordinator getter to handle an option dictionary to the addPersistentStoreWithType:configuration:URL:options:error: method with the key NSMigratePersistentStoresAutomaticallyOption.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (__persistentStoreCoordinator != nil)
{
return __persistentStoreCoordinator;
}
NSURL *cacheURL = [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *storeURL = [cacheURL URLByAppendingPathComponent:#"MyDatabase.sqlite"];
NSString *storePath = [storeURL path];
NSError *error = nil;
NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
__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;
}

The problem you're having is that you have to migrate the data from the old core data files to the new core data files. This is why you're getting the "incompatible" error in your question. If you change your core data model then you will need to supply the old version and the new version and tell the system how to move the data from the old version to the new version.
To do this you need to use core data versioning (using bundles) and create migration schemes. Its a complicated process that is probably to difficult to explain in this answer. Normally you can create a new version of your core data files and it will migrate the data automatically, but there are times when you could have issues.
Best thing to do would be to look up core data versioning in google. A quick search turns up this quite comprehensive tutorial http://www.timisted.net/blog/archive/core-data-migration/. It looks pretty good.

Related

Today Extension The model used to open the store is incompatible with the one used to create the store with userInfo dictionary

I am attempting to create an iOS Today Extension. I would like to connect to the main apps CoreData SQL DB. However i am receiving this error when i attempt to connect.
"The model used to open the store is incompatible with the one used to create the store} with userInfo dictionary"
Accessing Core Data SQL Database in iOS 8 Extension
The creation of the Database happens and I am able to insert records etc.
My extension controller code now is using similar code to the CoreData code in the app delegate.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
// -- Changed for Today Screen --//
//NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory]
// stringByAppendingPathComponent: #"CoreDB_2014.sqlite"]];
NSURL *storeUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com.THISAPP.APPNAME"];
NSLog(#"StoreURL1: %#", storeUrl);
storeUrl = [storeUrl URLByAppendingPathComponent:#"CoreDB_2014A.sqlite"];
NSLog(#"StoreURL2: %#", storeUrl);
NSError *error = nil;
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if(![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error]) {
}
return persistentStoreCoordinator;
}
Any help would be appreciated.
Thanks
That error is pretty self-explanatory: it means that you changed the Core Data model so that it no longer matches the one used to create the persistent store file that you're trying to open. Those have to match. If you change the model, you need to either (a) use more than one model version and perform a migration to the new model, or (b) use a different persistent store file (or delete the existing one and start over).
Core Data models often change while an app is being developed. In most cases, for a pre-release app, the developer will use option (b) and delete previous test data. If that's not feasible, you'll need to do model versioning and migration to update the data store in place.

CoreData Lightweight Migration + Custom

i'm trying to archive a lightweight migration with some handling after that. I already did the lightweight process and i need some help now handling my entities.
On the old model i used to have an entity "Car" and now i added the entity "Person" with the relationship Person has Cars.
So, after the lightweight migration i need to add a default person "John" and add all cars to him.
Does anyone have some idea?
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"CoreData.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSDictionary *options = #{
NSMigratePersistentStoresAutomaticallyOption : #YES,
NSInferMappingModelAutomaticallyOption : #YES
};
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
You can catch if a lightweight migration is going to occur. See this answer for details. There you can set a flag and based on that execute a method after normal startup in which you insert the desired entities.
Notice, though, that lightweight migration should migrate all your existing entities to the new store version, so there is usually no logically compelling reason to use this hook. Instead, you cold just query your (new or old version) store if it contains "John" and his cars and insert them if not.
If this is a one-time upgrade to the database, it might make sense to use a custom migration policy class rather than lightweight migration. Here's a nice tutorial on customizing that process:
http://9elements.com/io/index.php/customizing-core-data-migrations/
If you want to then mix inferred migration from one pair of models with custom migration between another pair of models, I've written a description of the Core Data methods involved and sample iterative migration class.

Delegate for ios app upgrade

Is there any delegate method that will be called when the user upgrades to or reinstalls a newer version of the iOS app?
I use Core Data to cache some information from server. When the schema of any entity is changed, I need to manually delete the SQLite database from the simulator, otherwise the app will crash on startup, with an error "The model used to open the store is incompatible with the one used to create the store." If there is any delegate method for app upgrade, the deletion could be automated.
You need to use CoreData versioning:
http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/CoreDataVersioning/Articles/Introduction.html
Daniel Smith's answer is the proper one, but I just want to add how my app determines its been updated. I look keep a 'current version' string in the defaults. When the app starts up, I compare it to the current version:
defaults has no string - this is the first run of the app
defaults version is different - the user updated the app
defaults is the same - user just restarted the app
Sometimes its nice to know the above. Make sure to save the defaults immediately after you set the tag and do whatever versioning you want, so a crash doesn't have you do it again.
EDIT: how not to crash if he model changes. I use this now, keep the old repository, and tweaking the model, on every tweak it just removes the old one (if it cannot open it) and creates a new one. This is modeled on Apple's code but not sure about what changes I made. In any case you don't get a crash if the model changes.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
//LTLog(#"_persistentStoreCoordinator = %#", _persistentStoreCoordinator);
if (_persistentStoreCoordinator)
{
return _persistentStoreCoordinator;
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *path = [[appDelegate applicationAppSupportDirectory] stringByAppendingPathComponent:[_dbName stringByAppendingPathExtension:#"SQLite"]];
storeURL = [NSURL fileURLWithPath:path];
BOOL fileExists = [manager fileExistsAtPath:path];
if(!fileExists) {
_didCreateNewRepository = YES;
}
if(_createNewRepository) {
[manager removeItemAtURL:storeURL error:nil];
if(fileExists) _didDestroyOldRepository = YES;
_didCreateNewRepository = YES;
}
while(YES) {
__autoreleasing NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if ([_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
break;
} else {
_persistentStoreCoordinator = nil;
[manager removeItemAtURL:storeURL error:&error];
if(fileExists) {
_didDestroyOldRepository = YES; // caller didn't want a new one but got a new one anyway (old one corrupt???)
_didCreateNewRepository = YES;
}
#ifndef NDEBUG
LTLog(#"CORE DATA failed to open store %#: error=%#", _dbName, error);
#endif
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
Typical reasons for an error here include:
* The persistent store is not accessible
* The schema for the persistent store is incompatible with current managed object model
Check the error message to determine what the actual problem was.
*/
//LTLog(#"Unresolved error %#, %#", error, [error userInfo]);
//abort();
}
}
return _persistentStoreCoordinator;
}
Follow the blog its good:
http://blog.10to1.be/cocoa/2011/11/28/core-data-versioning/

Updating core data entry

I'm working with core data to save scores for a game, and need help updating an entry when a score is beaten. This is where I'm at. I write the data the first time the app launches like this:
LevelData *levelOne = [NSEntityDescription insertNewObjectForEntityForName:#"LevelData" inManagedObjectContext:context];
levelOne.levelNum = #"1";
levelOne.topScore = #"0";
levelOne.isPassed = #"No";
if (![context save:&error]) {
NSLog(#"coudlnt save: %#", [error localizedDescription]);
}
I then read out the data at the end of the level like this:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"levelNum == 1"];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"LevelData" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSError *error;
NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
for (LevelData *info in fetchedObjects) {
NSLog(#"Is Passed: %#", info.levelNum);
NSLog(#"Top Score: %#", info.topScore);
NSLog(#"Is Passed: %#", info.isPassed);
}
What I'm stuck doing is updating the topScore entry and writing it back to the data store without creating a new entry, but updating the existing one.
And help/example would be so so helpful and much appreciated.
Thanks, Kyle
Well, updating the entry from the data store is easy. Fetch, update, save. As you seem to have a handle on. The real issue with core data is defining that initial data store. The design of core data does not bring in the concept of a database, so typically, one does not exist until the backend implements it for you (after your application calls its first save).
What I have seen developers typically do to work around this is set up AppDelegate to implement a quick and dirty hack: save an empty data base. They then comment out this code; retrieve the file from disk, rename it, modify it, add it to the project's support files.
Now, revisiting AppDelegate, they test on application launch the existence of the datastore on the client's machine. If none exists, they know to inject the default data store from their support files.. basically copying it to the appropriate path and renaming it appropriately as well.
This gets overly complicated with versioning. Another approach to this is to write a plist of the defaults into the main bundle and import this on the first launch and store one key in user defaults to let me know if this is necessary. It requires a little more boilerplate code, but it is a great way to get things set up. And you can encode versioning data to support upgrade merging a lot easier. If you can handle core data well, manipulating XML shouldn't be much more difficult.

Update read-only Core Data sqlite in main bundle

I am using a read-only Core Data sqlite from the Main Bundle, works well. When i add a new version of the database (more read-only data) to the main bundle it still reads the "Old" version of the database.
Anyone that can help me understand why and what to do to get the new database version the current one when a current user download an update with the new version of the database?
This is part of trying to solve the problem in this post: Same problem when accessing updated database from documents directory
===SOLUTION====
I solved this by changing the name of the new database in the "new" main bundle and it works like a dream. Also, if this is an update i delete the old database in the documents directory to clean up.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
//===READ DATABASE FROM MAIN BUNDLE===//
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *storeUrl = [[NSBundle mainBundle] URLForResource:kNewDB withExtension:#"sqlite"];
//=== IF THE OLD DATABASE STILL EXIST DELETE IT FROM DOCUMENT DIRECTORY ===//
NSURL *oldDatabasePathURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"database.sqlite"];
NSString *oldDatabasePath = [oldDatabasePathURL path];
if ([fileManager fileExistsAtPath:oldDatabasePath]) {
//Remove old database from Documents Directory
[fileManager removeItemAtURL:oldDatabasePathURL error:nil];
}
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
NSError *error;
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
return persistentStoreCoordinator;
}
You must have a place in your code where you check to see if a copy of the database file exists in some writable directory (possibly your Documents directory) and if not, then you copy it there. This is a very common approach to take when you need to make changes to your database. The problem is, when you update your app, the file already exists, so it is never copied over again.
There are two approaches to take to fix your problem:
(Preferable): Don't copy the database in the first place. Since it is read only, you don't need to, and it just takes up extra space on the device. Simply open the database using the path of the file that is in the main bundle.
Instead of checking to see if a file exists in the writable directory, check to see if it is newer than the one in the main bundle. (not by using the date, since they could have installed the program and created the file after your update was submitted to the app store for approval, which would result in the new one not being copied over. You need to check the version of the database, possibly by storing another file in your app bundle which stores the version info, or determining it with version specific code). If not, then copy it over again.
PeterK, I was having the same issue when using the tutorial at http://www.raywenderlich.com/12170/core-data-tutorial-how-to-preloadimport-existing-data-updated to use a read-only sqlite database by Core Data. All was fine, until I had to update my database and re-release my target app. As you know, the proposed code in that tutorial only copies in the new database if no database exists in the Application's Documents directory.
I did not think that renaming my database (and updating the copying code) was a good design approach, so I got my design working by following Inafziger's preferred advise and reading up on iOS file structure. I provide the below only to show how to implement Inafziger's proposal. And of note is that this approach likely only works if your app does not change the contents of the Core Data information as it is read in as read-only.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
// Updated processing to now just have the NRPersistentStoreCoordinator point to the sqlite db in the
// application's Bundle and not by copying that db to the app's Documents directory and then using
// the db in the Documents directory.
NSURL *refactoredStoreURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#“NameOfYourDatabase ofType:#"sqlite"]];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
// Added to ensure the NSPersistentStoreCoordinator reads the Bundle's db file as read-only since
// it is not appropriate to allow the app to modify anything in the Bundle
NSDictionary *readOnlyOptions = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSReadOnlyPersistentStoreOption, nil];
// Use the URL that points to the Bundle's db file and used the ReadOnly options
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:refactoredStoreURL options:readOnlyOptions error:&error]) {
// Your logic if there is an error
}
return _persistentStoreCoordinator;
}
I hope this helps the next reader of this question.