Core Data reverts to previous state without apparent reason - ios

A few customers of a Core Data based iOS application report that they occassionally lose data. The reports are very odd, which is the reason I'd like to ask for your take on this. The customers report that when they reopen the application after some time (minutes, hours, or next day), some of their data is lost as if the underlying database reverted to a previous state.
I have been working with Core Data for several years and have never run in an issue like this before. The application is fairly simple, which means I only use one managed object context and the changes are committed before the application goes to the background.
I realize that this is a long shot, but what could be some potential causes of this type of problem or what checks can I make to gather more information? Unfortunately, I cannot reproduce the issue myself, which would make all this a lot easier.
Update:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Prime.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:#{ NSMigratePersistentStoresAutomaticallyOption : #YES, NSInferMappingModelAutomaticallyOption : #YES } error:&error]) {
// Error Handling
}
return _persistentStoreCoordinator;
}

Check to see if you have put the save message in the appropriate appDelegate methods so that there is not a way to resign the application without saving. applicationWillResignActive and applicationWillTerminate should cover all of your needs.
Aside from that proper error handling and logging should gain you a lot of ground. I personally like to log these types of errors to a file that can be sent to me upon request. But that might be overkill for your particular application. This is written from memory so forgive any errors.
NSError *error = nil;
if (![[self managedObjectContext] save:&error])
{
NSString *errorString = #"%# ERROR : %# %#", [[NSDate date] description], [error localizedDescription], [error userInfo]);
NSString *documentsDirectory = [NSHomeDirectory()
stringByAppendingPathComponent:#"Documents"];
NSString *filePath = [documentsDirectory
stringByAppendingPathComponent:#"errorLog.txt"];
// Write to the file
[errorString writeToFile:filePath atomically:YES
encoding:NSUTF8StringEncoding error:&error];
}

You should check if save: method reports any error, for example like this:
NSError *error = nil;
if (![[self managedObjectContext] save:&error])
{
NSLog(#"Error %#", action, [error localizedDescription]);
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
// if there is a detailed errors - we can dump all of them
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError localizedDescription]);
}
}
else { // in other case lets dump all the info we have about the error
NSLog(#" %#", [error userInfo]);
}
}
One of the most common fail reasons is validation error, for example, it may expect a field to be presented while user haven't entered it or it may expect the value to be less than XX characters and so on.
Basically that means if it takes too long to provide the client with new build with debug info you can ask them to send you example of data they use to enter.

I can't be sure this is the reason, but it could be that when the app goes to the background, for some reason sometimes the maximum allowed time to handle this (backgroundTimeRemaining) is exceeded. From the Apple docs:
This property contains the amount of time the application has to run in the background before it may be forcibly killed by the system. While the application is running in the foreground, the value in this property remains suitably large. If the application starts one or more long-running tasks using the beginBackgroundTaskWithExpirationHandler: method and then transitions to the background, the value of this property is adjusted to reflect the amount of time the application has left to run.
If your app gets killed because saving the context takes too long, Core Data may decide to restore the previous state to at least get something consistent. If you can get log results from some users reporting this issue, you could try logging the property value after having tried to save the context, to check this.

Related

Migrate persistent store to new location

I need to move my app's database to a new location and stop it from being shared through iCloud automatically.
From my understanding, Apple previously documented that a database should be placed in the Documents directory of an application, but now says that the database should be placed in the Library directory. This is because iOS now shares all data from the Documents directory through iCloud.
My issue is that I need to move my database from the Documents directory to the Library directory. Below is the code I use to do this.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
// Get the URL of the persistent store
NSURL *oldURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"App.sqlite"];
bool oldStoreExists = [[NSFileManager defaultManager] fileExistsAtPath:[oldURL path]];
// Get the URL of the new App Group location
NSURL *newURL = [[self applicationLibraryDirectory] URLByAppendingPathComponent:#"App.sqlite"];
bool shouldMoveStore = false;
NSURL *storeURL = nil;
if (!oldStoreExists) {
//There is no store in the old location
storeURL = newURL;
} else {
storeURL = oldURL;
shouldMoveStore = true;
}
NSError *error = nil;
NSDictionary *options = #{ NSMigratePersistentStoresAutomaticallyOption:#YES, NSInferMappingModelAutomaticallyOption:#YES };
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
NSLog(#"Error Moving Store: %ld\nDescription: %#\nReason: %#\nSuggested Fix:%#", (long)error.code, error.localizedDescription, error.localizedFailureReason, error.localizedRecoverySuggestion);
abort();
}
if (shouldMoveStore)
[self movePersistentStoreFrom:oldURL To:newURL];
return _persistentStoreCoordinator;
}
-(void) movePersistentStoreFrom:(NSURL *)oldURL To:(NSURL *)newURL {
// Get the reference to the current persistent store
NSPersistentStore *oldStore = [_persistentStoreCoordinator persistentStoreForURL:oldURL];
// Migrate the persistent store
NSError *error = nil;
[_persistentStoreCoordinator migratePersistentStore:oldStore toURL:newURL options:nil withType:NSSQLiteStoreType error:&error];
if (error) {
NSLog(#"Error Moving Store: %ld\nDescription: %#\nReason: %#\nSuggested Fix:%#", (long)error.code, error.localizedDescription, error.localizedFailureReason, error.localizedRecoverySuggestion);
abort();
}
}
This code seems to work in moving the persistent store, but on the next run of the application the old store is found again and the app attempts to migrate the store a second time. At this point a crash occurs, and I'm assuming it is due to this double migration call. In classic Xcode style, the stack trace is pretty much useless.
Update -
The migratePersistentStore function does not remove ("cut/paste") the old sqlite database, instead it just makes a "copy". It looks like I'll need to remove the old one programmatically.
I switched the code to check if the newStoreExists as opposed to the !oldStoreExists, and the migration does work.
My new issue is that the table rows of the migrated data have become randomized. Any help with this would be appreciated!
Update 2 -
As it turns out, any data migrations like moving the store location or updating the database will randomize your data if you are using Core Data. You need to put an attribute into your table and use a NSSortDescriptor inside of your NSFetchRequest to manually sort the data and keep it ordered.
If you never migrate the data (I previously had not) the core data fetch request always come in the order it was saved.

IOS Coredata UNIQUE constraint failed:

I am trying to insert data into coredata but i am getting error like ,
CoreData: error: (1555) UNIQUE constraint failed: ZSIDETABLENAME.Z_PK
Scenario :-
First: from appdelegate.m I am copying data from my SQL file to document directory's sql file .
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
// Create the coordinator and store
NSString* from;
NSString* to;
NSArray* mainPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [mainPath objectAtIndex:0]; // Get documents folder
/*VoiceRecords.plist file*/
from=[[NSBundle mainBundle]pathForResource:#"News" ofType:#"sqlite"];
to=[documentsDirectory stringByAppendingPathComponent:#"News.sqlite"];
[[NSFileManager defaultManager]copyItemAtPath:from
toPath:to error:nil];
from=nil;
to=nil;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"News.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
/*
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.
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.
If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.
If you encounter schema incompatibility errors during development, you can reduce their frequency by:
* Simply deleting the existing store:
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]
* Performing automatic lightweight migration by passing the following dictionary as the options parameter:
#{NSMigratePersistentStoresAutomaticallyOption:#YES, NSInferMappingModelAutomaticallyOption:#YES}
Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
It works correctly. Then i am trying to save url entered by user in coredata in mainviewcontroller.m.
- (void)feedParserDidFinish:(MWFeedParser *)parser {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"IWorld"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"feedurl = %#", self.Feedlink];
NSManagedObjectContext *context = [self managedObjectContext];
if ([[context executeFetchRequest:fetchRequest error:NULL] count] == 0) {
NSError *error = nil;
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"feedurl contains %#",check];
[fetchRequest setPredicate:predicate];
NSMutableArray *checkp = [[context executeFetchRequest:fetchRequest error:&error]mutableCopy];
if (checkp.count<=0) {
NSManagedObject *newDevice = [NSEntityDescription insertNewObjectForEntityForName:#"IWorld" inManagedObjectContext:context];
[newDevice setValue:self.Name forKey:#"name"];
[newDevice setValue:self.WebsiteName forKey:#"websitename"];
[newDevice setValue:self.Feedlink forKey:#"feedurl"];
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
//Save value in lockbox
NSArray *keys = [[[newDevice entity] attributesByName] allKeys];
NSDictionary *dict = [newDevice dictionaryWithValuesForKeys:keys];
}
}
NSFetchRequest *fetchrequestforside=[NSFetchRequest fetchRequestWithEntityName:#"Sidetablename"]; //Sidetablename is entity
fetchrequestforside.predicate=[NSPredicate predicateWithFormat:#"feedurl = %#", self.Feedlink];
NSManagedObjectContext *context = [self managedObjectContext];
if ([[context executeFetchRequest:fetchrequestforside error:NULL] count] == 0) {
// Create a new managed object
NSError *error = nil;
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"feedurl contains %#",check ]; //check is urlstring
[fetchrequestforside setPredicate:predicate];
NSMutableArray *checkp = [[context executeFetchRequest:fetchrequestforside error:&error]mutableCopy];
if (checkp.count<=0) //if coredata will find url then notstore in coredata else stored procedure
{
NSManagedObject *newDevice1 = [NSEntityDescription insertNewObjectForEntityForName:#"Sidetablename" inManagedObjectContext:context];
if (self.Name)
{
[newDevice1 setValue:self.Name forKey:#"name"];
}
if (self.WebsiteName)
{
[newDevice1 setValue:self.WebsiteName forKey:#"websitename"];
}
if (self.Feedlink)
{
[newDevice1 setValue:self.Feedlink forKey:#"feedurl"];
}
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]); // Getting error here through nslog //CoreData: error: (1555) UNIQUE constraint failed: ZSIDETABLENAME.Z_PK
}
Note: this all happen at first time. Second time when I will run application all work correctly. Don't know where is issue?
You are getting a primary key constraint violation on the sidetablename table.
Thus, I believe your problem is in how you created and are copying your seed database.
Unfortunately, you did not provide that information. Thus, I'll just guess as to how you did it.
I assume you created the database using a mac app or you did it in the iOS simulator. You then copied the sqlite file from wherever it was created into the bundle resources.
Since WAL has been the default mode since iOS/OSX 7/10.9, I assume you created your seed database in WAL mode. This means that you likely missed the -wal and -shm files. If you specified that any data attributes should use external storage, you missed that data as well.
When creating a seed database that you will copy, you need to also copy the -shm/wal files, or you need to create the database in rollback journal mode, which will have all added data in the file.
You do that by setting the following option when adding the persistent store (if you already use options, just incorporate this one in with the others).
NSDictionary *options = #{NSSQLitePragmasOption:#{#"journal_mode":#"DELETE"}};
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options
error:&error]) {
// Handle error
}
This is how you would add the persistent store when you are creating the database that you want to copy to the bundle. It will ensure that everything is in the actual database file.
Now, after you run your code to create your seed file, you should go to that directory and see if there are any other files laying around. The best way to do this is in Terminal. Go to that directory and do "ls -lat" which will list all files sorted by time.
Caveat: Both SQLite and Core Data can create extra files that go with the sqlite file, depending on configuration of both entities. Thus, you should, in general, treat each core data persistent store as a file package. In other words, you should create a separate directory just for the core data store. That way, when either SQLite or core data decides to create extra files, everything is confined to a single directory.
When you copy the files into the bundle resources, from where you created your seed database, you need to make sure you copy everything.
Likewise, you should copy everything from the bundle to the device when deploying.
However, instead of using the file system, you should consider these methods on NSPersistentStoreCoordinator...
/* Used for save as - performance may vary depending on the type of old and
new store; the old store is usually removed from the coordinator by the
migration operation, and therefore is no longer a useful reference after
invoking this method
*/
- (NSPersistentStore *)migratePersistentStore:(NSPersistentStore *)store
toURL:(NSURL *)URL
options:(nullable NSDictionary *)options
withType:(NSString *)storeType
error:(NSError **)error;
/* copy or overwrite the target persistent store in accordance with the store
class's requirements. It is important to pass similar options as
addPersistentStoreWithType: ... SQLite stores will honor file locks, journal
files, journaling modes, and other intricacies. Other stores will default
to using NSFileManager.
*/
- (BOOL)replacePersistentStoreAtURL:(NSURL *)destinationURL
destinationOptions:(nullable NSDictionary *)destinationOptions
withPersistentStoreFromURL:(NSURL *)sourceURL
sourceOptions:(nullable NSDictionary *)sourceOptions
storeType:(NSString *)storeType
error:(NSError**)error NS_AVAILABLE(10_11, 9_0);
Thus, I believe that if you create your database properly, then copy it fully and properly to both your resource bundle and into your application, then your issues will go away.
I got this error when I was the creating/updating coredata entity on background thread, doing the same on main thread resolved the issue.

core data memory foot print in iOS keeps growing

I am trying to backup a Core Data SQLite database. This code successfully processes the running database and merges the WAL file. Unfortunately, everytime it runs I see a bump of about 3-5 MB in my memory footprint. This is causing issues after the program has run for a while. Can someone help me reclaim the memory? I thought setting everything to nil would dealloc all of the objects from RAM, but that doesn't seem to be it.
-(void) backupDatabaseWithThisTimeStamp: (int) timeStamp withCompletionBlock:(void (^)(void))completion {
NSDate *backupDate = [NSDate date];
NSError *error;
[self.defaultPrivateQueueContext save:&error];
if (error) {
NSLog(#"error -> %#",error);
}
dispatch_async(self.backupQueue, ^{
// Let's use the existing PSC
NSPersistentStoreCoordinator *migrationPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
// Open the store
id sourceStore = [migrationPSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self persistentStoreURL] options:nil error:nil];
if (!sourceStore) {
NSLog(#" failed to add store");
migrationPSC = nil;
} else {
NSLog(#" Successfully added store to migrate");
NSError *error;
NSLog(#" About to migrate the store...");
id migrationSuccess = [migrationPSC migratePersistentStore:sourceStore toURL:[self backupStoreURLwithTimeStamp: timeStamp] options:[self localStoreOptions] withType:NSSQLiteStoreType error:&error];
if (migrationSuccess) {
NSLog(#"store successfully backed up");
// Now reset the backup preference
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tempContext.persistentStoreCoordinator = migrationPSC;
tempContext.undoManager = nil;
// clip out data
[CDrawColorData purgeDataOlderThan:backupDate fromContext:tempContext];
migrationPSC = nil;
tempContext = nil;
}
else {
NSLog(#"Failed to backup store: %#, %#", error, error.userInfo);
migrationPSC = nil;
}
}
migrationPSC = nil;
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion();
}
});
});
}
self.backupQueue = _backupQueue = dispatch_queue_create("backup.queue", DISPATCH_QUEUE_SERIAL);
localStoreOptions =
- (NSDictionary*)localStoreOptions {
return #{NSMigratePersistentStoresAutomaticallyOption:#YES,
NSInferMappingModelAutomaticallyOption:#YES,
NSSQLitePragmasOption:#{ #"journal_mode" : #"DELETE" }};
}
commenting out everything that happens after the migrationSuccess point does not effect the memory footprint.
All of the issues that I've seen are directly accountable to the Xcode Scheme.
Product->Scheme
Select Run-Options
UNCHECK -> Queue Debugging (enable backtrack recording)
Once this was done, all of the memory footprint growth immediately disappeared.
Due to too few reps i'm writing this as an "answer" (even if it might not be the solution): I think it's good practice to think about "cache_size" as another NSSQLitePragmasOption and to limit it accordingly:
NSSQLitePragmasOption:#{ #"journal_mode" : #"DELETE", #"cache_size" : #"50"}
see www.sqlite.org for declarations
I think it's likely to be because all relationships in a Core Data object graph are strong references. So you're all but guaranteed retain cycles. For further diagnostics you should use Instruments' leaks tool and see what type of object appears to be leaking.
You're therefore likely to want to call reset on any instances you create of NSManagedObjectContext before discarding them. This will forcibly fault all active objects, thereby breaking any retain cycles (unless you access the objects again, naturally). It's not explicit in the documentation that a dealloc automatically prompts a reset and therefore it doesn't appear to be a contractual guarantee.

Another way to do an "initial sync"

I'm working on an app that has to work entirely offline and then sync up to a server.
The server can (and probably will) change depending on the project that is currently underway.
I've written the synchronisation stuff and it works really well. Everything gets updated both ways.
However, the initial sync of a device is basically a big info dump of thousands of records and it takes a really long time. I have analysed and optimised this but the time is now just from the sheer volume of data being synced and the relationships between that data.
A normal sync (i.e. sending and getting updates) only takes about 5 seconds from start to finish including reads, uploads, downloads and writes.
Is there a way I can "plug into" a computer and just import a DB file into the app?
Is there any other way of doing this other than going through the sync process and downloading and installing all this stuff on the device?
I can't do it at build time as the app is independent of which project it is on and each project has a different set of data.
Import the sqlite db into your app, and link it in your core-data stack in the app delegate. This massive amount of data should be shipped with the app and then syncing would make changes to that initial dataset.
This is how you link the sqlite to your core-data stack
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"MyAppName.sqlite"];
//Connects to sqlite---------------------------------------
NSFileManager *fileManager = [NSFileManager defaultManager];
// If the expected store doesn't exist, copy the default store.
if (![fileManager fileExistsAtPath:[storeURL path]]) {
NSURL *defaultStoreURL = [[NSBundle mainBundle] URLForResource:#"MyAppName" withExtension:#"sqlite"];
if (defaultStoreURL) {
[fileManager copyItemAtURL:defaultStoreURL toURL:storeURL error:NULL];
}
}
//end sqlite connection ---------------------------------------
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}

Questions about CoreData and pre-populated models

To pre-populate CoreData in my app upon first launch, I have included a PreModel.sqlite file that was previously created by the app from data that it downloaded from a web service, which includes images.
To populate the model, I do this:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
NSLog(#"creating new store");
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"PreModel.sqlite"];
if(![[NSFileManager defaultManager] fileExistsAtPath:[storeURL path]]) {
NSString *sqlitePath = [[NSBundle mainBundle] pathForResource:#"PreModel" ofType:#"sqlite"];
if (sqlitePath) {
NSError *error = nil;
[[NSFileManager defaultManager] copyItemAtPath:sqlitePath toPath:[storeURL path] error:&error];
if (error) {
NSLog(#"Error copying sqlite database: %#", [error localizedDescription]);
}
}
}
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
It seems to work. But I have 2 questions:
If the sqlite file is just a database file and does not actually contain any images, how is the app finding and loading the images on first launch?
Even on subsequent runs of the app I see "creating new store" logged every time. Why is _persistentStoreCoordinator always nil? I am clearly setting it in the code.
It's possible to store images in a database file, usually as binary blobs (which look like instances of NSData in Core Data). If you can provide more info about your model or the code that stores/loads the images, we can be more specific.
"Creating new store" is expected to get logged every time the app is launched in this instance. Even though the SQLite file is persistent on disk, you can't expect data structures in your code to stick around when your app is terminated - you need to create a new persistent store object for your program every time it launches.
Think of it like assigning NSInteger x = 10, then expecting to be able to quit and relaunch your program while maintaining that x has the value 10. That's not how programs work - you'd need to reassign x = 10 before you can expect to read x and get 10 back. The variable _persistentStoreCoordinator works the same way here.

Resources