Update read-only Core Data sqlite in main bundle - objective-c

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.

Related

Coredata: Backup and Restore a database file

I've searched and have not found much regarding creating a backup and restoring a database file, so I am hoping someone smarter than me can point me in the right direction.
This is on OSX.
I successfully created a backup, restoring has been the main issue. However I'm almost there, I've successfully restore a database file BUT I need to restart the app for it to work.
Restore method:
[_managedObjectContext reset];
NSPersistentStore *currentStore = self.persistentStoreCoordinator.persistentStores.lastObject;
[_persistentStoreCoordinator removePersistentStore:currentStore error:&error];
NSFileManager *filemanager;
filemanager = [NSFileManager defaultManager];
if ([filemanager fileExistsAtPath:[url path]]) {
//If the file exist, remove file
if ([filemanager removeItemAtURL:url error:&error]) {}
}
[_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:urlbackup options:nil error:&error];
currentStore = self.persistentStoreCoordinator.persistentStores.lastObject;
[_persistentStoreCoordinator migratePersistentStore:currentStore toURL:url options:options withType:NSSQLiteStoreType error:&error];
That all works, IF I restart the app. How can I get it to work without restarting the app? It appears I need to reload all the data and refresh the view.
I've tried the following after without success:
[arrayController rearrangeObjects];
[tableView reloadData];
Again, if I restart the app everything is fine, the backup file is restored. Any pointers would be greatly appreciated.
You want to put the code that creates the Core Data stack somewhere reusable, which means not in the app delegate if that's where it currently is. You should also destroy the views, or at least the view controllers references to any currently existing managed objects, contexts, etc.
Now, once that's done, the owner of the context can destroy it, the persistent store and the model, and then re-run the stack creation code. After that you can recreate your view (or pass the new context / managed object to the VC).
It's simply about reorganising your code so it's reusable and ensuring that you don't leave any old references to invalidated objects.

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.

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/

Core Data: How to handle new versions?

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.

FMDB - Failed to modify DB from Cocoa

I am working on a Cocoa application which talks to a local SQLite database with FMDB. I ran into an issue that I can't do any insert or update operation on DB. Select queries run perfectly fine, so I would assume my db connection settings are correct.
The structure of my code is basically like this:
FMDatabase* db=[FMDatabase databaseWithPath:[[NSBundle mainBundle] pathForResource:#"DBName" ofType:#"sqlite"]];
if(![db open])
{
NSLog(#"Could not open db.");
}
db.traceExecution=YES;
[db beginTransation];
[db ExecuteUpdate:"INSERT INTO test (title) VALUES(?)", [NSNumber numberWithInt]:2],nil];
[db commit];
[db close];
No exceptions or warnings were thrown during execution, the console output regarding db.traceExecution is like following:
<FMDatabase: 0x100511fd0> executeUpdate: BEGIN EXCLUSIVE TRANSACTION;
<FMDatabase: 0x100511fd0> executeUpdate: INSERT INTO test (title) VALUES(?);
obj: 2
<FMDatabase: 0x100511fd0> executeUpdate: COMMIT TRANSACTION;
The testing database is simply just a one column table of INT type.
Everything looks fine except that the db file is not updated at all. It's really confusing to me as the Select query works perfectly fine. I checked the path of the database, it is pointing to the right one. First I suspect it's caused by file permission, but the issue remain the same even if I allowed everyone to be able to read/write.
I have been stucked with this problems for many hours and couldn't find a proper solution. Can anyone shed some light on this? Thanks!
Databases in the bundle are read only. If the file doesn't exist at the destination folder where you define, you should copy it from the bundle to the library or documents folder and then connect to that. That means it will copy on first use of that path.
Here's a function to 'prepare' the database by copying it to the destination from the bundle. It copies it to library (from my iOS app) but you can copy wherever you want. In my case, it was contacts.db.
I called this method from ensureOpened.
- (BOOL)ensureDatabasePrepared: (NSError **)error
{
// already prepared
if ((_dbPath != nil) &&
([[NSFileManager defaultManager] fileExistsAtPath:_dbPath]))
{
return YES;
}
// db in main bundle - cant edit. copy to library if !exist
NSString *dbTemplatePath = [[NSBundle mainBundle] pathForResource:#"contacts" ofType:#"db"];
NSLog(#"%#", dbTemplatePath);
NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
_dbPath = [libraryPath stringByAppendingPathComponent:#"contacts.db"];
NSLog(#"dbPath: %#", _dbPath);
// copy db from template to library
if (![[NSFileManager defaultManager] fileExistsAtPath:_dbPath])
{
NSLog(#"db not exists");
NSError *error = nil;
if (![[NSFileManager defaultManager] copyItemAtPath:dbTemplatePath toPath:_dbPath error:&error])
{
return NO;
}
NSLog(#"copied");
}
return YES;
}