Why does my core data object not show up using fetch requests between related objects that were created in the same session? - objective-c

This is a really weird situation which I have been battling to resolve over the last couple of days.
I have a Card entity with a To-Many relationship on a Transaction entity. I am also using Magical Record to remove the boilerplate code around managing my data store.
My problem occurs in the following order:
Create a Card object
Then create a Transaction object in another view
When I inspect the transaction count with [card.transactions count] I get 1 as the size
However, if I perform a fetch request using the same context the transaction is not there. My predicate looks like this:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"card == %# && deleted == 0", card];
So logic says that I am perhaps not saving the context after saving the transaction, but the weird thing is that if I quit the app and then rerun it, the transaction is there. Steps 1-4 work perfectly too if I create the card, quit the app and then run the app and add a transaction. Then my save transaction code works fine.
So my question is, why aren't my objects showing up using fetch requests on a newly created parent object, where "newly created" means that it was created in the same session as the child object.
My code for creating the card looks like this:
GYCard *card = [GYCard MR_createInContext:[NSManagedObjectContext MR_defaultContext]];
[[NSManagedObjectContext MR_defaultContext] MR_save];
Then when I save the transaction my code looks like this:
NSManagedObjectContext *localContext = [NSManagedObjectContext MR_defaultContext];
NSNumber *transactionAmount = [self.numberFormatter numberFromString:self.transactionAmountLabel.text];
GYTransaction *transaction = [GYTransaction MR_createInContext:localContext];
transaction.amount = transactionAmount;
transaction.deleted = [NSNumber numberWithBool:NO];
transaction.card = self.card;
[localContext MR_save];
The code that is not working is this:
+ (int) countTransactions:(GYCard *) card {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"card == %# && deleted == 0", card];
return [GYTransaction MR_countOfEntitiesWithPredicate:predicate inContext:[NSManagedObjectContext MR_defaultContext]];
}
The countTransactions always returns 0 if the card was created in the same session. But it works 100% if I quit the app and relaunch. It also works for adding any new transactions to the card after quitting and relaunching the app.

This appears to be a Magical Record issue. I was using the 2.0 beta branch of Magical Record and after reverting to the tagged version:1.8.3 the issue disappears. So 2.0 seems to be the culprit. It would still be interesting to know however, why version 2.0 is causing this problem.
I would strongly advise anybody who is using Magical Record to avoid the 2.0 branch until it comes out of beta.
Update: After further investigation what the 2.0 beta of Magical Record was doing was generating 2 save notifications for 1 save, this led to my app inadvertently have 2 versions of the card. This resulted in causing the transaction that I was logging to be logged on the first card but the second card obviously did not have a transaction on it. My fetch request was then fetching the second card and returning zero transactions. Quitting and restarting the app then made my app load the correct transaction from the data store and thus worked as expected.

This could be related to the includesPendingChanges property on NSFetchRequest. However the default for this is YES which means any unsaved changes should be included in any queries.
I'm not sure what the MR_ methods are doing under the hood, but the setIncludesPendingChanges: documentation states:
Special Considerations
A value of YES is not supported in conjunction with the result type
NSDictionaryResultType, including calculation of aggregate results
(such as max and min). For dictionaries, the array returned from the
fetch reflects the current state in the persistent store, and does not
take into account any pending changes, insertions, or deletions in the
context.
So I would make sure the MR_countOfEntitiesWithPredicate method is not doing anything funky. Maybe try calling the standard - (NSUInteger)countForFetchRequest:(NSFetchRequest *)request error: (NSError **)error method on your context and see what that returns.

Related

Cloud Kit Core Data persistent history transactions not showing author even when set

I'm trying to avoid a dreadful hack while implementing Core Data with CloudKit using Objective-C. I've gotten the basics to work ok but what I want to do is trap for when a 'foreign' (ie not local) update to the Core Data database comes in, to indicate in an iOS app via badge on a tab item entry or bold a button on the OSX part of the app to tell the user that the entries they made on one part of the app need to be applied (say on a Mac) need to be refreshed into the UI on the iOS app, as an example.
I can not get the author or name properties to come through to the monitored NSPersistentHistoryTransactions (s) or associated NSPersistentHistoryChange (s) at all. I set the name on the managed object context (and the chid managed object contexts used in various update routines) and that name shows up in the debug messages just fine so SOMETHING is being set but that nor the author properties show up as hoped. I do find entries in the author of the NSPersistentHistoryChange transactions but they show the generic CloudKit delegate (dot) mirror.
Partially for learning but partially out of desperation, I set up the following code in the notification handler and it fires reliably and just as expected.
NSDate *date = [NSDate date];
NSDate *newDate = [date dateByAddingTimeInterval:-3600*4];
NSFetchRequest *histdatefetch = NSPersistentHistoryTransaction.fetchRequest;
NSPersistentHistoryChangeRequest *fetchHistoryDateRequest =
[NSPersistentHistoryChangeRequest fetchHistoryAfterDate:newDate];
NSPersistentHistoryResult *fetchHistoryDateResult = [localAppDelegate.persistentContainer.newBackgroundContext executeRequest:fetchHistoryDateRequest error:&error];
NSArray *tempArrayDate = fetchHistoryDateResult.result;
unsigned long arrayDateCount = [tempArrayDate count];
unsigned long fetchlastDateArrayEntry = arrayDateCount - 1;
NSPersistentHistoryTransaction *tempHistoryTransaction = [tempArrayDate objectAtIndex: fetchlastDateArrayEntry];
long long checkRow = tempHistoryTransaction.transactionNumber;
if ([localAppDelegate.localUpdate isEqualToString:#"YES"]) {
localAppDelegate.lastLocalRowCount = (int) checkRow;
localAppDelegate.localUpdate = #"NO";}
In the update routines just at the save, I set the local update string to #"YES" and assume that if the notifications fire up, the first one at the top is my local update.
I get the expected history transactions and they build as updates are applied to the database, etc... I then check the row number of the current last persistent history transaction and if any of the subsequent notifications do not have row greater than that it doesn't light up the badge or do the button highlight.
If something comes in and no local updates have been done, the last local row will be zero and the indicator(s) will light up. If a new remote update happens after a local update, the remote update will have a row number greater than what I have saved. It works because it really is a single user application and mostly people don't use more than one device at a time and even if it doesn't work, the worst that happens is that you get redundant indications that you need to press the refresh key. Still, yuck.
I could REMOVE all of this nonsense if I could tell local notifications from remote notifications and I can't seem to be able to do it. The token that comes in with the notification didn't help me and neither do the author or name properties of anything I can see.
I haven't worked out the purge logic yet but I could do that easily enough by doing one when I detect a local transaction is being applied and delete everything more than four hours old.
Any help anybody can supply would be tremendous.

Cannot use removeObjectForKey in Parse (using Back4App on Objc)

I'm using Back4App's service to host Parse server and I can't seem to successfully remove a field from a row. The field in question is a pointer to another object, and calling removeObjectForKey followed by a save does not work. Other updates work in the same batch, and I have seen that 4 times out the 36 that I have tried, it did successfully delete the object.
[self.myProfile removeObjectForKey:#"partnership"];
[self.myProfile saveEventually:^(BOOL succeeded, NSError * _Nullable error) {
NSLog(#"success %d", succeeded); //always returns true
}];
Is this a known problem with Back4App? Or Parse itself? I tried the same code in swift and it worked.
Any ideas?
When you need to run a callback, to confirm when is deleted, is recommended the deleteInBackgroundWithBlock: or deleteInBackgroundWithTarget:selector: methods. You can delete a single field from an object with the removeObjectForKey method:
// After this, the playerName field will be empty
[classScore removeObjectForKey:#"customName"];
// Saves the field deletion to the Parse Cloud
[classScore saveInBackground];
About the saveEventually, most save functions execute immediately, and inform your app when the save is complete. If you don’t need to know when the save has finished, you can use saveEventually instead. The advantage is that if the user currently doesn’t have a network connection.
Content from Parse =D

Can't find mapping model for migration - UIManagedDocument Core Data Migration

I have two versions of my model Model001.xcdatamodel and Model002.xcdatamodel. These two are in the Model.xcdatamodeld bundle.
I also have a Model001to002.xcmappingmodel which is not part of the Model.xcdatamodeld. I checked: both the xcmappingmodel and the xcdatamodeld get copied into the .app bundle.
My managed object context is initialized like this:
NSURL *documentModel = [bundle URLForResource:#"Model"
withExtension:#"momd"]; managedObjectModel = [[NSManagedObjectModel alloc]
initWithContentsOfURL:documentModel]; return managedObjectModel;
I also set these properties on my overridden initWithFileURL: in my UIManagedObject subclass.
NSMutableDictionary *options = [NSMutableDictionary dictionaryWithDictionary:self.persistentStoreOptions];
[options setObject:#YES forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:#YES forKey:NSInferMappingModelAutomaticallyOption];
self.persistentStoreOptions = [options copy];
But when I try to open a documet, I get the following error:
Can't find mapping model for migration
-- UPDATE --
Even if I do a manual migration
[NSMappingModel mappingModelFromBundles:#[[NSBundle mainBundle]]
forSourceModel:sourceObjectModel
destinationModel:self.managedObjectModel];
this returns nil. Although I double checked that the Model001to002.cdm is in the app bundle. It has to be in the app bundle right?
A "gotcha" with mapping models is that you are not allowed to make any changes to the models after you created the mapping. If you do, you will also get this error.
OK, solved the problem by removing all core data files from Xcode, reading them and setting the source and destination of the mapping model again.
Damn you Xcode!
You are not allowed to make any changes to the source/destination model after you have created the mapping models.
If you do make some changes,
mappingModelFromBundles:forSourceModel:destinationModel: will not be able to find the mapping model file
addPersistentStoreWithType:configuration:URL:options:error: with {NSInferMappingModelAutomaticallyOption: #NO} will report an error "Can't find mapping model for migration"
migrateStoreFromURL:type:options:withMappingModel:toDestinationURL:destinationType:destinationOptions:error: will report an error "Mismatch between mapping and source/destination models"
So, just recreate the mapping model and copy every change you made in the old one.
TL;DR
At least as of Xcode 8/9, open the mapping model then from the Editor menu select Refresh data models. Usually it seems you need to restart Xcode. If that doesn't do it you might try re-selecting the destination at the bottom of the model editor.
More Tips
Definitely NEVER change a model after it has been distributed in an app build.
For this example, let's say you have published Data Model 1 (DM1) and are making a migration to DM2. If you set DM2 as the active version then run your app, a migration will run on your persistent store. If you then make another change to DM2, run your app... Boom!
The issue is that your store has already been migrated to "DM2" but the data in the store doesn't fit into the model anymore. And, we can't migrate from DM2 to DM2 again.
It may seem like an obvious solution to go ahead and create DM3. It is
usually a good idea though to minimize the number of models and
migrations while you are developing.
So... now you have a persistent store that has been migrated to a defunct DM2. How do you test the migration again? You could revert your app and generate some data with DM1 but I prefer to use backups
Creating a backup
Before you run your app with DM2 you can copy the existing store (with DM1) to use for later test migrations. On macOS you can easily do this manually. The code below should do the trick as well. Typically you wouldn't want to ship this, rather you could just put it somewhere before your normal CD stack opens, run the app, then stop the app (maybe place a breakpoint just after then end the run via Xcode).
let fm = FileManager.default
let url = // The store URL you would use in ↓
// try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
let dir = url.deleteLastPathComponent().appendingPathComponent("Backup", isDirectory: true).appendingPathComponent("DM1", isDirectory: true)
print("Saving DB backup for DM1")
if !fm.fileExists(atPath: dir.path) {
do {
// Create a directory
try fm.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil)
let backupURL = dir.appendingPathComponent(url.lastPathComponent)
try fm.copyItem(at: url, to: backupURL)
}
catch {
print("Failed to save DB backup")
}
}
Oops, I need to make another change...
If you run your migration to DM2 then realize you need to make another change, you'll want to re-test your migration from DM1 -> DM2. This is where the backup comes in.
Same way you made the backup, run this code.
let fm = FileManager.default
let url = // The store URL you would use to add the store
let dir = url.deleteLastPathComponent().appendingPathComponent("Backup", isDirectory: true).appendingPathComponent("DM1", isDirectory: true)
let backupURL = dir.appendingPathComponent(url.lastPathComponent)
if fm.fileExists(atPath: backupURL.path) {
do {
fm.removeItem(at: url.path)
try fm.copyItem(at: backupURL, to: url)
}
catch {
print("Failed to restore DB backup")
}
}
You now have a restored DM1 store and have made changes to DM2. If you run the app the migration might succeed but it won't use your custom mapping model.
Remember if you are using a custom mapping, you will still need to use the Refresh Data Models technique before the mapping model will work.
This can happen if your test device's store is from a version of the data model that no longer exists.
For example I had Data Model Version 7, then I made Data Model Version 8. I made a mapping model to go from 7 to 8. Then I ran it on my test device and everything was happy.
Then I made some more changes to 8.
The thing to realize is that in Core Data, every model has a hash identifier that the system creates by taking a checksum of the xcdatamodel file. So if you make even a slight change, even if you didn't create a new version, it sees it as a different version. These versions' identifiers are NSStoreModelVersionHashes (see documentation here).
So in other words, I ended up with:
Data Model 7 (release) - 0plcXXRN7XHKl5CcF+fwriFmUpON3ZtcI/AfK748aWc=
Data Model 8 (beta) - qeN1Ym3TkWN1G6dU9RfX6Kd2ccEvcDVWHpd3LpLgboI=
Data Model 8 (release) - EqtMzvRnVZWkXwBHu4VeVGy8UyoOe+bi67KC79kphlQ=
Instead of making a version 9, and saving the original version 8 in the data model history, I just updated 8, figuring automatic migration could take care of me. Well, it couldn't, and I couldn't make a mapping between the two, because the old (beta) version of 8 was gone.
I did it that way because it was an intermediary internal build (not a release) so it wasn't a big deal, but it did throw me for a loop!
If it wasn't an internal build and I needed to make this work, I could go back to the (beta) commit and pull out that xcdatamodel file for 8 (beta), rename the (release) version to 9, then stick it into the release build and make a mapping model between 8 and 9.
However since it was just an internal beta build, we just erased and reinstalled the app on test devices. We did verify that, when going from 7 (release) to 8 (release), the migration went smoothly.
Removing Coredata files from its path an re - run project is worked for me

RestKit Object Mapping: difficulty using setObjectMapping:forResourcePathPattern:withFetchRequestBlock

My setup
The following all works fine:
RKManagedObjectMapping* chanMapping = [RKManagedObjectMapping mappingForClass:[Channel class] inManagedObjectStore:objectStore];
chanMapping.primaryKeyAttribute = #"chanId";
[chanMapping mapKeyPathsToAttributes:
#"id",#"chanId",
#"name", #"chanName",
nil];
[objectManager.mappingProvider setMapping:chanMapping forKeyPath:#"Channels.channel"];
I can call
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/channels" delegate:self];
and I get my channel's from the server and they're stored locally by Core Data. Perfect.
The issue
However, I now wan't to have RestKit automatically delete Channels from the Core Data store that have been removed from the server the next time a GET is performed. I understand this is supported by adding the following:
[objectManager.mappingProvider setObjectMapping:chanMapping forResourcePathPattern:#"/channels" withFetchRequestBlock:^ (NSString *resourcePath) {
return [Channel fetchRequest];
}];
However with this all the Channels get deleted whenever there is anything new on the server.
Things I've tried [UPDATED]
I've debugged using the steps in the answer below. It looks as though the mapping isn't working / is not being found (i.e. I haven't properly associated the mapping with the resource path).
In deleteCachedObjectsMissingFromResult the cachedObjects array looks good, has all the objects that should be there from the last time but the results array is empty which obviously results in [results containsObject:object] always being NO and all the objects being deleted.
Do I need to change something to do with the resource path mapping?
I looked at your updated description. Give this a try:
Switch back to the setObjectMapping:forResourcePathPattern:withFetchRequestBlock
Set the rootKeyPath on the object mapping you register to Channels.channel
Then give it another try. There is some API work in progress to provide URL and keyPath based mapping configuration in a single line, but its not merged to development yet.
Two things to check out to determine why you are seeing the described behavior:
Open up RKManagedObjectLoader and put a breakpoint within isResponseMappable. This method checks if the response was loaded from the cache and performs a load of the objects using the objects returned by the managed object cache if it returns YES. This is probably where you are seeing the return of the cached objects from.
As for the deletion of cached objects, put a breakpoint within deleteCachedObjectsMissingFromResult and see what is going on in there (if you are even making it into the routine).
The scenario to expect automatic pruning would be:
GET /channels returns 2xx status code with new payload
RKManagedObjectLoader performs pruning

If you are using naitive identities, are objects persistant instantly?

In my asp.net application, I open and close/flush the session at the beginning/ending of each request.
With this setup, I thought it would result in things like:
Entity e = EntityDao.GetById(1);
e.Property1 = "blah";
EntityDao.MakePersistant(e);
e = EntityDao.GetById(1);
e.Property1 // this won't be blah, it will be the old value since the request hasn't flushed
But I noticed that the value returned was the most recent updated value.
Someone responded that because of they way I have my identity setup?
Can someone explain this behaviour? So I don't need to call flush to ensure it is persisted to the db?
I belive (but could be mistaken) that nHibernate's caching and change-tracking is the reasons for this.
Since this Session instance is still active, nHibernate is tracking the changes you made to 'e'. When you ask for it again it includes those changes in the object returned. What happened when you called MakePersistant (which I assume calls Session.SaveOrUpdate(e)) is that it told that Session instance you are going to save those changes, thus it knows about them and will still show them when you call Session.Get(id) again.
If you instead started another Session and then called Session.Get(id) you wouldn't see those changes if you had not called Flush (or closed the Transaction, since you should be using a Transaction here) as it knows nothing of the changes made in the other Session.
To answer your other question, yes you still need to call Flush or close a Transaction to ensure changes are written to the database. One thing that is neat, is that you don't actually need to call SaveOrUpdate(e). For example this code will cause an Update to the database:
using (var session = SessionFactory.OpenSession())
using (var trans = session.BeginTransaction()){
var e = session.Get(id);
e.Name = "New Name";
trans.Commit();
}
nHibernate knows to update 'e' since it was tracking the changes that were made to during that Session. When I commit that transaction they are written. Note, this is the default behavior and I believe it can be changed if you want to require that .SaveOrUpdate() is called.