Fetch request returns old data - ios

I'm getting outdated results when executing a fetch request upon a NSManagedObjectContextObjectsDidChangeNotification.
As an example, consider directories with documents that can be deleted logically with a boolean attribute (softDeleted). The fetch request should return all directories that have at least one non-deleted document (directoriesWithDocuments).
The initial state is single directory with a single deleted document. directoriesWithDocuments returns an empty array.
The following code restores the document by setting the softDeleted boolean to NO.
[_context.undoManager beginUndoGrouping];
[_context.undoManager setActionName:#"undelete"];
document.softDeleted = #(NO);
NSError *error;
BOOL success = [_context save:&error]; // This triggers the notification
[_context.undoManager endUndoGrouping];
The save triggers a NSManagedObjectContextObjectsDidChangeNotification. I expected directoriesWithDocuments to return the directory, but instead it still returns an empty array.
- (void)objectsDidChangeNotification:(NSNotification*)notification
{
NSArray *objects = [self directoriesWithDocuments]; // Still empty!
}
Yet, if I execute directoriesWithDocuments after saving the context, either right away or in the next run loop, it returns the directory as expected.
This is the code of the fetch request:
- (NSArray*)directoriesWithDocuments
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY documents.softDeleted == NO"];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Directory"];
fetchRequest.predicate = predicate;
fetchRequest.includesPendingChanges = YES; // Just in case
NSError *error = nil;
NSArray *objects = [_context executeFetchRequest:fetchRequest error:&error];
return directories;
}
I suspect that the context has some kind of cache for fetch requests that is not cleared until the notification is handled. Is this how Core Data is expected to behave? Or am I doing something wrong?
Workaround
I'm currently delaying the execution of the fetch request like this:
- (void)objectsDidChangeNotification:(NSNotification*)notification
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// HACK: Give time to Core Data to process pending changes or invalidate caches
NSArray *objects = [self directoriesWithDocuments]; // Returns the directory as expected
}];
}
Experiments
Per #MikePollard's suggestion, I checked the returned value of directoriesWithDocuments in NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification. The results are (in order):
NSManagedObjectContextObjectsDidChangeNotification: empty (wrong)
NSManagedObjectContextWillSaveNotification: empty (wrong)
NSManagedObjectContextDidSaveNotification: 1 directory (correct)

First of all, it looks like you have a boolean attribute called "deleted" defined in your model.
I recall that doing so can be a significant problem because it conflicts (at a KVC level) with NSManagedObject isDeleted. You might want to change that just to make sure it's not the culprit.
Edit
Thanks for replying. I used deleted as a simple example; it's not the
actual attribute I'm using. Will change it to softDeleted to avoid
confusion
I've updated my suggestion below to match the softDeleted in your example.
That said, I think what's at work here boils down to the question of what constitutes a "change" for includesPendingChanges = YES.
Before the context has completed its save, the only 'change' is to Document entities, not Directory entities.
So when the fetch request includes pending changes, there are no Directory entities with any pending changes so you end up with the previous results.
To test that theory, give this a shot:
[_context.undoManager beginUndoGrouping];
[_context.undoManager setActionName:#"delete"];
document.softDeleted = #(NO);
[document.directory willChangeValueForKey:#"documents"] // any attribute will do really
[document.directory didChangeValueForKey:#"documents"]
NSError *error;
BOOL success = [_context save:&error];
[_context.undoManager endUndoGrouping];
What you're doing with the fake will/did changeValueForKey is to "dirty" the associated Directory object. My guess is that it will then be considered "changed" and, as such, included the fetch results.

It strikes me that the predicate is going to be translated into a SQL statement so until the save hits the DB you're not going to get the result you want ... (Not that'd you'd image that from just reading the NSFetchRequest documentation.)
So try doing the fetch as a result of the NSManagedObjectContextDidSaveNotification.
I bet if you turn on -com.apple.CoreData.SQLDebug 1 you'll see the SQL statement.

Related

Core Data, Get Sum Of Certain Boolean Value Attribute

I'm logging a Core Data attribute "passed" (Boolean value)
for (Circuit *object in self.distributionBoard.circuits) {
NSLog(#"Core Data Value = %d", object.passed);
}
This logs fine. What's the most efficient way to count the number of times the saved boolean value == 1?
Using NSFetchReques or NSExpression did not yield the desired result so far. Looked here: Core Data sum of all instances attribute and similar, with the usual searches
Since your property is a boolean, you can make it a lot simpler than the methods described in that answer. Use a predicate to match the value of passed and then get the count of the result instead of the fetched objects. Something like:
NSFetchRequest<Event *> *fetchRequest = MyEntity.fetchRequest;
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"passed = true"];
NSError *error = nil;
NSUInteger count = [self.managedObjectContext countForFetchRequest:fetchRequest error:&error];
Then count has the number of instances where passed is true.

Using predicates to filter out sources when doing a query

I'm trying to make an HKStatisticsQuery on step count, with a predicate that dismisses user-entered values.
I found out there were 2 differences between user-entered values and passively-logged values:
Source:
-passively logged values have "A's iPhone" for the source
-user-entered values have "Health" for the source
'Was User Entered' metadata key:
-passively logged don't have the metadata key at all
-user-entered values have a 'Was User Entered' metadata key set to true
I've tried this predicate, in an attempt to only get data that had 'Was User Entered' set to false:
HKQuery.predicateForObjectsWithMetadataKey(HKMetadataKeyWasUserEntered, allowedValues: [false])
I got no results, and someone helped me realize that the metadata key doesn't even exist if it's passively logged (kind of redundant...)
Given the above differences, does anyone know of any other way to extract passively-logged data? I was thinking along the lines of
NSPredicate(format: "%K != %#", HKPredicateKeyPathSource, "Health")//crash
This predicate did not work and crashed my program when I executed my query.
Does anyone know why it crashes, or if there is a better way to achieve my goal of weeding out the data that is user-entered?
Thanks.
Your crash could be probably due to the fact that currently, only the "=" and "IN" operators are supported for predicates filtering HKSamples by source
You could use a source query first to pull out all the sources and ignore the health app source (which has a bundle identifier of com.apple.Health), by using the following code:
- (void)fetchSources
{
NSMutableArray *dataSources = [[NSMutableArray alloc] init];
HKQuantityType *stepsCount = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
HKSourceQuery *sourceQuery = [[HKSourceQuery alloc] initWithSampleType:stepsCount
samplePredicate:nil
completionHandler:^(HKSourceQuery *query, NSSet *sources, NSError *error)
{
for (HKSource *source in sources)
{
if (![source.bundleIdentifier isEqualToString:#"com.apple.Health"])
{
[dataSources addObject:source];
}
}
}];
[self.healthStore executeQuery:sourceQuery];
}
Once you have all the other sources other than the Health App, use a source predicate to pull data for all the other sources:
NSPredicate *sourcesPredicate = [HKQuery predicateForObjectsFromSources:[NSSet setWithArray:self.dataSources]]
Hope this helps.

Core Data returns nil for relationship with valid object

I have a weird error. I have a relationship between a call object and status object. the relationship name from the call object is statusForCall.
the data is imported into core data programmatically from JSON. it will not create a call without a status. the user has no way of changing the statusForCall to nil.
however after a period of time (seems to be days) when accessing calls via a fetch request into an array, when I access aCall.StatusForcall.statusID it starts returning nil. There is no way the code can have updated the statusForCall to nil.
Any ideas what might be causing this and where to start looking?
the cachename is nil throughout the application.
interestingly, if the user redownload the application the problem is solved. The code doesn't change and the data doesn't change (there is no migration - it's the same version) - but for some reason this ALWAYS fixes the issue.
i'm really struggling to know what to look at to get to the bottom of this.
code for setting the status below (edited down for conciseness). As i say the job initially has a status but seems to lose the relationship after a period od of time (i can't tell how long this is as the user is unreliable on this)
Call *theCall;
//see if we have the call on the device - if not create a new one, if we do update that one
BOOL contentIsInCoreData = NO;
for (Call *thisCall in existingCalls)
{
if ([thisCall.callID isEqualToNumber:[checkDic objectForKey:#"callID"]])
{
theCall=thisCall;
contentIsInCoreData = YES;
}
}
NSError * error = nil;
NSDateFormatter *dateFormat =[[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"dd-MMM-yyyy"];
if (contentIsInCoreData==NO)
{
[self.postDelegate updateGetJobsAndTimesProgress:[NSString stringWithFormat:#"Adding new job: %#",[checkDic objectForKey:#"woRef"]]];
//new call add bits that will not change
theCall = (Call*)[NSEntityDescription insertNewObjectForEntityForName:#"Call" inManagedObjectContext:self.managedObjectContext];
}
//set various properties from the JSON dictrionary – these have been cut down for the example code
theCall.woRef=[checkDic objectForKey:#"woRef"];
theCall.shortAddress=[checkDic objectForKey:#"shortAddress"];
theCall.address=[[checkDic objectForKey:#"address"] stringByReplacingOccurrencesOfString:#"\\n" withString:#"\n"];
theCall.postCode=[checkDic objectForKey:#"postCode"];
theCall.callID=[checkDic objectForKey:#"callID"];
//****THIS IS WHERE THE STATUS IS SET – PLEASE NOTE THE STATUS WILL EXIST IN CD ALREADY and the JSON WILL have a valid statusID
NSFetchRequest *request2=[NSFetchRequest fetchRequestWithEntityName:#"CallStatus"];
request2.predicate=[NSPredicate predicateWithFormat:#"callStatusID ==%#",[checkDic objectForKey:#"callStatusID"]];
error = nil;
NSArray * existingStatus = [self.managedObjectContext executeFetchRequest:request2 error:&error];
CallStatus *selectedStatus;
selectedStatus=[existingStatus firstObject];
theCall.statusForCall = selectedStatus;
error = nil;
if ([self.managedObjectContext save:&error])
{
//NSLog(#"saved job");
}
else
{
NSLog (#"***BROKEN GET JSON!!!\r\r%#",error);
}
turns out that a relationship property had been accidently changed - it was cascading a delete from the many end - which resulted in the one end being deleted.

iOS Core Data <fault after searching

All, I am having problems with Core Data.
I have a method that queries all data which matches a jobId
- (JobSummary*)summaryForJobId:(NSInteger)jobId {
NSFetchRequest* request = [[NSFetchRequest alloc] initWithEntityName:[JobSummary entityName]];
request.predicate = [NSPredicate predicateWithFormat:#"jobId = %D", jobId];
JobSummary* summary = [[self.context executeFetchRequest:request error:nil] lastObject];
NSLog(#"DB Summary: %#", summary);
[request setReturnsObjectsAsFaults:NO];
return summary;
}
When called and I log out it works perfect, however when I call it from a seperate view controller like so;
JobSummary *retrievedDictionary = [[FSScheduleDatabaseTransaction new] summaryForJobId:jobid];
When I log out retrievedDictionary it spits out this;
<JobSummary: 0x12de24a0> (entity: JobSummary; id: 0xb3c19b0 <x-coredata://7E9F6C6E-B4A0-4450-8905-184C6C8FB60D/JobSummary/p169> ; data: <fault>)
Any help much appreciated!
It is giving you the log correctly. When ever you try to print the ManagedObject you'll only see a fault in logs.
A fault is a placeholder object that represents a managed object that has not yet been fully realized, or a collection object that represents a relationship:
A managed object fault is an instance of the appropriate class, but its persistent variables are not yet initialized.
A relationship fault is a subclass of the collection class that represents the relationship.
So, in short, unless you try to access those properties, CoreData wont fill property values. It will have a fault. Just trying to log the ManagedObject, without accessing its properties previously, will only show fault.

Programmatically update duplicate values in coredata?

I am new in Core Data. I want to update duplicate values. For example my table looks like this
id | Name
============
1 | Joseph
2 | Fernandez
3 | Joseph
4 | James
Say that I want to update Joseph corresponding to id 1 and 4 to "myName". When I tried to update this it only updates the 4th row. I can't find any way to do this in any of the documentation. Can anyone suggest me a solution?
One more question, how can I print all name values?
you will have to read over the documentation to know how to update record
http://www.appcoda.com/core-data-tutorial-update-delete/
James,
I'll try to reply to both your questions with sample code.
To update specific objects you need to se up a new NSFetchRequest with a predicate, grab the objects (of type NSManagedObject), update the values you are interested in and save the context.
So, for example:
NSFetchRequest* fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"YourEntityName"];
// set the predicate (it's equal to set a WHERE SQL clause) filtering on the name for example
// use camel case notation if possible, so instead of Name use name (for this you have to changes your model, if you don't want to do it use Name)
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"name == %#", #"Joseph"]];
NSError* error = nil;
NSArray* results = [context executeFetchRequest:fetchRequest error:&error];
// do some error checking here...
for (NSManagedObject resultItem in results) {
// use KVC (for example) to access your object properties
[resultItem setValue:#"myName" forKey:#"name"];
}
// save your context here
// if you don't save, changes are not stored
To print you need to se up a new NSFetchRequest, grab the objects (of type NSManagedObject) and use NSLog.
So for example:
NSFetchRequest* fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"YourEntityName"];
NSError* error = nil;
NSArray* results = [context executeFetchRequest:fetchRequest error:&error];
// do some error checking here...
for (NSManagedObject resultItem in results) {
NSLog(#"%#", [resultItem valueForKey:#"name"]);
}
P.S. The code I provided is quite simple and the predicate I used to specific values check against the name. Since this could be error prone, I would modify the model and using a sort of guid for each objects you need to use (I don't know if id is for that but I would change its name to another one, for example userId). Once done you can check against it.
Hope that helps.
It's as simple as retrieving the NSManagedObject and changing the Name property. You can retrieve the NSManagedObject with a fetch request. Once you changed the property and you want to keep it changed even when you close the application you'll have to do a save on the managedObjectContext.
You'll have to read over the documentation to get up to speed on core data:
http://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/Articles/cdBasics.html#//apple_ref/doc/uid/TP40001650-TP1
Edit: just NSLog whatever you want to know, for example log you fetch request results.

Resources