I'm seeing a situation where a NSFetchRequest returns a different number of objects depending on whether it's executed directly through the NSManagedObjectContext or as part of building a NSFetchedResultsController.
Sample code:
- (void)setupResultsController {
NSError *error = nil;
NSManagedObjectContext *ctx = [[DataManager sharedInstance] mainObjectContext];
// Create a fetch request and execute it directly
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [Song entityInManagedObjectContext:ctx];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"section" ascending:YES];
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor, nameDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSArray *debugResults = [ctx executeFetchRequest:fetchRequest error:&error];
NSLog(#"Count from context fetch: %lu", (unsigned long)debugResults.count);
// Use the request to populate a NSFetchedResultsController
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:ctx
sectionNameKeyPath:#"section"
cacheName:#"Detail"];
[aFetchedResultsController performFetch:&error];
NSLog(#"Count from results controller fetch: %lu", (unsigned long)[[aFetchedResultsController fetchedObjects] count]);
_songResultsController = aFetchedResultsController;
}
Executing the above results in log messages:
2020-01-10 11:05:07.892772-0500 asb7[12985:105052] Count from context fetch: 10
2020-01-10 11:05:07.893259-0500 asb7[12985:105052] Count from results controller fetch: 9
The difference between the two fetches is that the NSFetchedResultsController is missing the most recently added object. An extreme oddity about this is that, after running the application some seemingly random number of times, the counts start to agree and the new object is fetched.
Edit:
The results become consistent if I pass nil as the cache name or if I remove the second sort descriptor. Obviously these cause undesirable behavior changes but may be clues.
It seems that the NSFetchedResultsController is seeing a stale cache as being valid. Changing a sort descriptor invalidates the cache however updating the persistent store file should invalidate it but apparently does not in this case.
After a bit more experimenting, I have an explanation...if not a solution. Adding new objects does not change the modification date of my .sqlite file. It updates the .sqlite-shm and .sqlite-wal but I'll guess those aren't considered when judging whether to use the cache. Using touch from a terminal session makes the problem go away for the next launch.
(Xcode 10.1, macOS 10.13.6, deployment target 10.3, iOS 12.1 simulator and 10.3.2 device)
Another edit:
I've uploaded a zipped project directory that demonstrates the problem at https://github.com/PhilKMills/CacheTest
What I get is: first run, 3 records for both fetches; second run, 6 and 3. I see it as entirely possible that this depends on my particular software versions but I'm not in a position to upgrade at the moment. Other people's results would be most interesting.
Note: without a FRC delegate being assigned, the problem does not appear.
I suspected that the issue related to the use of the FRC cache, though I was uncertain why that might be. One workaround would therefore be to abandon using the FRC cache. However, that's not ideal.
After further experimentation by the OP, it seems the problem relates to the timestamp on the .sqlite file associated with the persistent store. By inference(*), if the FRC detects that the timestamp has changed, it realises that its cache might be out of date, and rebuilds it - thereby detecting the newly added objects. However, because CoreData by default uses SQLite's "WAL journal mode" (see here and here), database updates are written not to the .sqlite file, but instead to a separate .sqlite-wal file: the timestamp on the .sqlite file is consequently not changed, even though the database has been updated. The FRC continues to use its cache, unaware of the additional objects (or indeed other changes).
A second workaround is therefore to abandon using SQLite's WAL journal mode. This can be achieved by specifying the required journal mode when loading the persistent store, using
NSSQLitePragmasOption:#{#"journal_mode":#"DELETE"}
see here.
(*) In fact, this is documented here:
“the controller tests the cache to determine whether its contents are still valid. The controller compares the current entity name, entity version hash, sort descriptors, and section key-path with those stored in the cache, as well as the modification date of the cached information file and the persistent store file.”
Related
I am deleting CoreData objects using this method:
NSManagedObjectContext *theContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:nameEntity];
[fetchRequest setIncludesPropertyValues:NO]; //only fetch the managedObjectID
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray *fetchedObjects = [theContext executeFetchRequest:fetchRequest error:&error];
for (NSManagedObject *object in fetchedObjects)
{
[theContext deleteObject:object];
}
error = nil;
if(![theContext save:&error]){
NSLog(#"Couldn't save: %#", error);
}
What I don't understand is that for instance I download data and store it, and in the Settings I can see that my app uses 5MB of disk space.
Once I delete the data using this method, it says my app uses 6.3MB of data.
That makes absolutely no sense at all. What am I doing wrong? Why isn't the data being deleted correctly?
You can open the core data store with the SQLite vacuum option set to reclaim disk space. E.g.
NSDictionary *storeOptionsDict=#{NSSQLiteManualVacuumOption : #YES};
[persistentStoreCoordinator addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: sourceStoreURL options: storeOptionsDict error: &error];
See this older question: How to VACUUM a Core Data SQLite db?
When using the sqlite store, the storage is managed by sqlite, not the core data framework itself. When you delete a managed object, the row is deleted from the appropriate table. In a typical database, this does not result in freeing any disk space. As for why the space used might actually increase, this speculation on my part, but I think it's because of the transaction logging. Even though the transaction is complete, the log is not immediately purged.
I would recommend that you not worry about this. The sqlite library will have internal management to compact tables (reclaim space from deleted rows) and purge old transaction records.
I've kinda implemented a today view extension with CoreData sharing in my app, I have multiple problems (like only one object showing when I have three) and a big one, "Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xd0000000001c0004". Now, this only happens when I have the application open in the background, which leads me to believe that my app is leaving the store in a bad state, but that really shouldn't be happening. I'm using Persistent Stack, external 'library' to manage all of the CoreData (instead of AppDelegate) which is readable on https://gist.github.com/mluisbrown/7015953
CoreData Fetching from TodayView Extension:
-(void)viewDidLoad{
self.persistentStack = [[PersistentStack alloc] initWithStoreURL:self.storeURL modelURL:self.modelURL];
self.managedObjectContext = self.persistentStack.managedObjectContext;
}
- (NSURL*)storeURL
{
//NSURL* documentsDirectory = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
// return [documentsDirectory URLByAppendingPathComponent:#"WhatIOwe.sqlite"];
NSURL *directory = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com.bittank.io"];
NSURL *storeURL = [directory URLByAppendingPathComponent:#"WhatIOwe.sqlite"];
return storeURL;
}
- (NSURL*)modelURL
{
return [[NSBundle mainBundle] URLForResource:#"WhatIOwe" withExtension:#"momd"];
}
- (NSFetchedResultsController *)fetchedResultsController {
self.persistentStack = [[PersistentStack alloc] initWithStoreURL:self.storeURL modelURL:self.modelURL];
self.managedObjectContext = self.persistentStack.managedObjectContext;
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"OweInfo" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"details.date" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
As suggested, I've tried a merge policy in my persistent stack:
[self.managedObjectContext.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:self.storeURL
options:#{ NSPersistentStoreUbiquitousContentNameKey : #"WhatIOwe",
NSMigratePersistentStoresAutomaticallyOption : #YES,
NSInferMappingModelAutomaticallyOption : #YES,
NSMergeByPropertyObjectTrumpMergePolicy : #YES}
error:&error];
Another observation, on configuring my NSManagedObjectContext, passing:
[psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.storeURL options:nil error:&error]; allows the extension to read the store (but still throw the error I'm having), but passing
[psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.storeURL options:#{ NSPersistentStoreUbiquitousContentNameKey : #"iCloudStore",
NSMigratePersistentStoresAutomaticallyOption : #YES,
NSInferMappingModelAutomaticallyOption : #YES,
} error:&error]; will result in the extension not reading any data whatsoever.
Side-note: psc is passed as
__weak NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
self.persistentStoreCoordinator = self.managedObjectContext.persistentStoreCoordinator;
What is happening here is that you have two different processes accessing the same Core Data store, each with it's own NSPersistentStoreCoordinator.
One process has modified the store - most likely a delete. The other process has no way of knowing that this delete occurred, and already had an object in memory that pointed to the now deleted data. When that process tries to access that object it must go to the store to get the data ("firing a fault"), which no longer exists.
Core Data is not designed for this kind of use, and the capabilities of extensions are very different than applications. If your extension is able to write to the same data that the application can you may reconsider your approach and make the extension only able to read the data, and never hold the data in memory for long. This will at least mitigate the most common ways to run into these problems.
Replying to above answer and the question:
"Core Data is not designed for this kind of use"
It totally is. The assessment is correct: Something has likely been deleted in the actual app, and the extension is not aware. Fortunately CoreData provides a way to deal with this case. Check out the stalenessInterval property of NSManagedObjectContext. It defines how long your in memory cache is good for. If you're having problems because your in memory cache goes out of date from disk store change because an external process is changing them, simply set the staleness interval to 0 in your extension, and that will tell CoreData to always fetch new values from the store and ignore the in memory cache.
If you're holding a reference to an object in memory, and that object is deleted in the store, you still may have issues, so always check to make sure the object you are accessing has not been deleted.
There are a few other options if you want to get more detail. You could send notifications from your main app to your extension when things get saved to provide a manual trigger for reloading your data. You could also send specific object ids across that have been deleted or modified and use the refreshObject... method to manually refresh. Or check out mergeChangesFromContextDidSaveNotification:. You might be able to manually serialize the dictionary that expects and then send it across.
You could also have the parent app handle all database accesses and send results back via notifications. This might be unnecessary.
All of this requires a bit of work, but you're going to run into that with any
database system where the database is being accessed across multiple processes, and there is caching.
There are multiple issues with your code which are likely leading to a confused Core Data state. I can't be certain that they're causing your problem, but at the moment things are messed up badly enough that trying to debug this specific error is getting ahead of things. These problems include:
Confusion about how you're sharing data between your app and the extension.
You can do this using iCloud, or you can do it by using an app group to share the persistent store directly. You appear to be attempting both, which is not just unnecessary but also likely to cause problems keeping the extension up to date.
If you use iCloud to share data, you do not need the app group, because both the app and the extension will get their data from iCloud. You don't share a local persistent store in this case, instead the data is transferred via iCloud.
If you use an app group to share the persistent store file, you have no need of iCloud. The app and the extension both access the same file, which is located in the group container. Each can both read and write it.
Conflicting iCloud setups. You're using different values for NSPersistentStoreUbiquitousContentNameKey in different places. Even assuming that iCloud is working properly, this is going to prevent you from sharing data. If the app and extension are going to share via iCloud, they need to access the same cloud store, but you seem to be directing them to use separate cloud data stores.
You're not actually using the merge policy you're aiming for. You're passing NSMergeByPropertyObjectTrumpMergePolicy as one of the options when adding the persistent store file, but this isn't actually a valid option there. I would have expected at least a console message about this, but if there isn't one then it means Core Data is just silently ignoring that key. You set a merge policy by setting the value of the mergePolicy attribute on your managed object context. With no merge policy, you're falling back on the default NSErrorMergePolicy.
Unusual, suspicious code when adding the persistent store. In most cases with Core Data you'd add the persistent store and then later on create one or more managed object contexts. You appear to be creating the context first and only later adding a persistent store. That's not necessarily wrong but it's very unusual. If I were working on this code, it'd be a red flag to look over the life cycle of the Core Data stack very carefully. Especially if, as your code comments indicate, you're not getting any data at all in some cases.
Again I'm not sure if the above is directly causing this specific error, but they're going to be causing problems of various types and it wouldn't be surprising if the resulting confused state leads to this error.
I have an app which asynchronously downloads a JSON file and then it should insert those objects within Core Data for persistent storage. Regarding the insert, is it a good idea to do it from the main thread? What if there are thousands of objects? Should I do the inserts on a different thread? Could you provide me with some snippets regarding this matter? Regarding the fetching of the objects after I've saved them, should I also use a different thread?
My code for inserting into Core Data is:
- (void) insertObjects:(NSArray*)objects ofEntity:(NSString *)entityName
{
NSString *key;
NSManagedObject *managedObject;
NSError *error;
for(NSDictionary *dict in objects){
managedObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:_managedObjectContext];
for(key in dict){
[managedObject setValue:dict[key] forKey:key];
}
}
[_managedObjectContext save:&error];
}
PS: The objects are of the same entity. The project runs on iOS 7.0 or higher.
Since I can't comment yet..
What iOS Versions do you plan to support? If 5 and higher, this might help Concurrency stack
Summary of the link:
you create a context of private concurreny type to access your physical data
based on this you create a context of main concurreny type
on top of this you use private concurrency type stores again.
Don't forget so save in every store, otherwise, the data seems to be saved while the app is running, but after restart it is lost.
And yes, you want to do it an extra thread, since it would otherwise block the UI if there are to many items.
This question already has an answer here:
Checking for duplicates when importing to CoreData
(1 answer)
Closed 8 years ago.
I have a bunch of NSManagedObjects that are created from a JSON file online. Currently, I am creating them all each time the app launches (not ideal).
What is the best way to check to see if the objects are already there before I try to create them?
if I do [self saveContext] it seems to work, but as I don't know how to check if they are already loaded, it ends up duplicating everything.
Obviously, I am relatively new to Core Data and seem to be missing a key concept.
[EDIT] After reading more and more about where and when to load this many objects into Core Data, it looks like pre-loading the data is the best option for me (the data is static and will likely only be update a few times per year).
I chose not to use the "find or create pattern" as I assumed it would be more expensive given the number of objects that need to be checked/created and would like to save learning about background queues for next time ;)
I was then having trouble getting the sqlite file to work, and solved it by saving the context after each object was created, rather than once after all the objects were loaded.
The way this is handled usually in my experience is via one of the two options:
You first check if the item exists, and if it does, then you update it, else insert it. Here's a sample of what I have used in the past for a vouchers model:
Voucher *newObject = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Voucher"];
request.predicate = [NSPredicate predicateWithFormat:#"voucher_id = %#",[dictionary objectForKey:#"voucher_id"]];
NSError *error = nil;
NSArray *matches = [context executeFetchRequest:request error:&error];
if ([matches count] == 0 ){
newObject = [NSEntityDescription insertNewObjectForEntityForName:#"Voucher" inManagedObjectContext:context];
newObject.number = [json_dictionary objectForKey:#"number"];
newObject.valid_from = [json_dictionary objectForKey:#"valid_from"];
newObject.valid_to = [json_dictionary objectForKey:#"valid_to"];
}
else {
newObject = [matches lastObject];
newObject.number = [json_dictionary objectForKey:#"number"];
newObject.valid_from = [json_dictionary objectForKey:#"valid_from"];
newObject.valid_to = [json_dictionary objectForKey:#"valid_to"];
newObject.voucher_id = [json_dictionary objectForKey:#"voucher_id"];
}
return newObject;
The other way is to select all, put into an NSOrderedSet, and then run a comparison, and only insert if not in the set.
If you look at "Core Data Performance Optimization and Debugging" on this page https://developer.apple.com/wwdc/videos/ , it's got a great explanation of this
If you haven't worked on it before, the learning curve might be a bit steep. But one good way is to use RestKit.
https://github.com/RestKit/RestKit/wiki/Object-mapping#core-data
Ray Wenderlich has a detailed tutorial on Core Data that show you how to do it step by step: (make sure to turn on Google Translate)
In response to your question under comments, here it is:
create a new file and choose to create datamodel (under Core Data)
add your entities - entities are what you declared as class data models. Note that I have Location, Marker, and Village because I have created those as classes (Location.m/.h, etc)]
Add attributes (properties) associated with those entities.
http://i.stack.imgur.com/wOUvF.png
http://i.stack.imgur.com/5AJGZ.png
Frankly, I'm a little embarrassed to even ask this question for the perceived absurdity and without being able to personally reproduce it, but...
In my iOS 7 application, I've received many complaints from users that certain portions of the app "don't work." By "don't work," they mean that a UITableView being populated by a NSFetchRequestDelegate's methods has zero rows. The only thing in common about the error reports are the specific NSFetchRequests that return zero objects and the physical location of the users. The users are all in Nordic countries (Norway, Sweeden, Finland, etc.). This could be a coincidence, or just that the Nordic are more vocal when there is a bug, but this bug makes me nervous.
I've tried setting my phone to use Nynorsk as the default language and using Nordic international units (which my app does happily support with NSDateFormatter and friends). The app works fine for me when I do this, but I am physically in the USA. I've even gone so far as to use a user account with my app whose server-side data was generated in Finland (all sorts of diacritics in strings that will be saved with CoreData), and it still works as expected.
I don't see anything abnormal with my instantiation of my NSFetchedResultsController, but for completeness, here is one that returns no results:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
return (_fetchedResultsController);
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:kTTAEntityCheckinDetail inManagedObjectContext:MOC];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:kTTACheckinDetailBatchSize];
NSSortDescriptor *checkinIDSort = [NSSortDescriptor sortDescriptorWithKey:kTTACheckinDetailAttributeCheckinID ascending:NO];
[fetchRequest setSortDescriptors:#[checkinIDSort]];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:MOC sectionNameKeyPath:#"sectionIdentifier" cacheName:nil];
_fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if ([self.fetchedResultsController performFetch:&error] == NO)
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
return (_fetchedResultsController);
}
It's possible that performFetch is failing, but I do not have access to the user's logs. Perhaps I should detect the user's location and log this to a remote server?
Solution: CoreData failed to save the objects because a date field was nil. This field wasn't populated because the locale of a NSDateFormatter was not set. See the comments of #flexaddicted's accepted solution for details.
This could be a shot in the dark.
First, what type of objects do you retrieve? Based on dates?
Then, Yes, I would log both locally and server-side. To log locally you could ask users in Nordic Countries to send a feedback (based on email) with Core Data store or just file logs (the logs will be inserted in the right points). The latter could be achieved by CocoaLumberjack.
An example of this can be found at CocoaLumberjack and Mailing logs by NSScreencast.
TestFlight could be an alternative way but I guess you are in production.