Coredata: Backup and Restore a database file - objective-c

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.

Related

Correct way of Dropping and Recreating Core Data Store

I wanted to confirm that my method of dropping a Core Data store (sqlite) is correct. It seems to work without crashing but wanted to confirm this is the correct method. Afterwards when the user connects to the database a new sqlite file is generated automatically.
Here is my code to drop data store:
- (BOOL)dropDataStore{
// ----------------------
// This method removes all traces of the Core Data store
// ----------------------
NSError *_error = nil;
NSURL *_storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Store.sqlite"];
NSPersistentStore *_store = [persistentStoreCoordinator persistentStoreForURL:_storeURL];
// Remove the SQL store and the file associated with it
if ([persistentStoreCoordinator removePersistentStore:_store error:&_error]) {
[[NSFileManager defaultManager] removeItemAtPath:_storeURL.path error:&_error];
}
if (_error) {
return NO;
}
persistentStoreCoordinator = nil;
managedObjectContext = nil;
managedObjectModel = nil;
return YES;
}
This is not a good method.
At a minimum, SQLite on iOS 7 and up creates journal files in the same directory as its main file. If you leave those behind, you may get old data persisting into the new file, or corrupt data. So at a minimum you need to also remove Store.sqlite-shm and Store.sqlite-wal.
If your Core Data model uses binary attributes and has "allow external storage" enabled for any of those attributes, then values for those attributes may be stored in external files. Obviously your code wouldn't remove those, but the location of these files is not documented.
If you expect to need to remove Core Data persistent store files, you should really put the files into their own custom subdirectory instead of directly in the documents directory. Then, you can just recursively remove everything in that directory and not care whether Core Data is creating extra files outside of the persistent store file.

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/

howto resolve UIDocumentStateSavingError and UIDocumentStateClosed

I have an iPhone-iCloud app. Now one document has the state UIDocumentStateSavingError and UIDocumentStateClosed. I can see the file on developers.icloud.com with the status "file upload is pending". But i don't know what to do now
When i try to delete the file with the code:
NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[fileCoordinator coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForDeleting error:nil byAccessor:^(NSURL* writingURL) {
NSFileManager* fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtURL:writingURL error:nil];
}];
the file is still there. I tried to delete the app on all devices but nothing changed. How can I delete this file or resolve the problem?
It's long ago but still unanswered, so if your problem persists:
For me it works to prepend the [fileManager removeItemAtURL:...] with a [fileManager setUbiquitous:NO...] on the file and then let the fileManager delete the local copy.
Take care to use NSError and boolean result handling of the fileManager actions to be sure it really works. So if you get back an NSError you could see the reason why it failed. That's always good practice.
I stumbled into your question because I get the message "File upload is Pending" after such a deleted file is re-created even if it's some hours between deletion and creation.

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.