NSBatchUpdateRequest not saving Transformable attribute - ios

I have an entity (TestEntity) which contains a "Transformable" attribute which holds an object (MyObjectClass). On initial load, the transformable saves correctly; initialised as below:
TestEntity *test = (TestEntity *)[NSEntityDescription insertNewObjectForEntityForName:ENTITY[<Int>] inManagedObjectContext:temporaryContext];
test.transformableAttr = [[MyObjectClass alloc] initWithObject:obj];
However, when I fetch an object (I fetch as dictionary with NSDictionaryResultType) and update its "Transformable" attribute,
MyObjectClass *my_obj = ....
dict[#"transformableAttr"] = my_obj
it saves successfully but when I fetch it again I get nil for the "Transformable" attribute.
Now this only happens with "NSBatchUpdateRequest" because when I save using the MOC
TestEntity *test = ....
test.transformableAttr = updated_object
it saves successfully and I can access the updated attribute when fetched again.
Can anyone please explain? Does it mean that NSBatchUpdateRequest does not Transformable?
My NSBatchUpdateRequest code:
[context performBlock:^{
NSError *requestError = nil;
NSBatchUpdateRequest *batchRequest = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:entity];
batchRequest.resultType = NSUpdatedObjectIDsResultType;
batchRequest.propertiesToUpdate = properties;
NSBatchUpdateResult *result = nil;
SET_IF_NOT_NIL(batchRequest.predicate, predicate)
#try {
result = (NSBatchUpdateResult *)[context executeRequest:batchRequest error:&requestError];
if (requestError != nil){
// #throw
}
if ([result.result respondsToSelector:#selector(count)]){
__block NSInteger counter = [result.result count];
if (counter > 0){
[managedObjectContext performBlock:^{
for(NSManagedObjectID *objectID in result.result){
NSError *faultError = nil;
NSManagedObject *object = [managedObjectContext existingObjectWithID:objectID error:&faultError];
if (object && faultError == nil) {
[managedObjectContext refreshObject:object mergeChanges:YES];
}
counter--;
if (counter <= 0) {
// complete
}
else{
// Wait.
}
}
}];
}
else{
// No Changes
}
}
else {
// No Changes
}
}
#catch (NSException *exception) {
#throw;
}
}];

The documentation doesn't seem to call out this particular scenario, but I'm not surprised that it doesn't work. An NSBatchUpdateRequest is described as [emphasis mine]:
A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.
Transformables work by converting to/from Data in memory. If your class conforms to NSCoding, the coding/decoding happens in memory, because SQLite doesn't know about NSCoding or your classes.
Your original assignment works because Core Data converts the value of transformableAttr to Data in memory and then saves the bytes of the Data to the persistent store. In the batch update, the objects aren't loaded into memory, so the transformation can't run, so the update doesn't work as you'd expect.
It's disappointing that Core Data doesn't make this clearer. Look in the Xcode console to see if it warns you about this. If it doesn't, please file a bug with Apple, because though I don't expect this to work, it's also not good for it to fail silently.
If you want to use batch updates, you'll have to convert your value in code before running the update. I'm not 100% certain how this will work but if your value conforms to NSCoding you'll start with
let transformedData: Data = NSKeyedArchiver.archivedData(withRootObject:transformableAttr)
What you do then is where I'm not sure. You might be able to use transformedData as the new value. Or you might have to access its bytes and use them somehow-- maybe using withUnsafeBytes(_:). You'll probably run into trouble because transformableAttr is not a Data, so it may get messy. It seems that batch updates aren't designed to work well with transformables.

Related

Does core data consider a value as changed if it is set to the same value it was?

For the purpose of ensuring I have chosen the correct NSMergePolicy I am curious as to whether a value being set to its current value is capable of causing a merge conflict across multiple contexts.
Specifically, in my case I want to ensure that a modified flag will conflict and be preserved if set at an inopportune moment.
Example:
//...
//on background thread, doing some work to an object because it's status was
//set to Status_Modified
[managedObjectContext performBlockAndWait:^{
object.status = Status_NotModified;
[managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[managedObjectContext save:&error];
}];
What if while this is going on, on the main thread, the status is set to Status_Modified and saved? Will the objects status stay as Status_Modified? I.e. will the 'status' property be considered to be changed and so cause a conflict and therefore trump our in memory change (according to the policy)?
So, I cannot find any decent documentation to answer this question, but I have done some tests and it seems that the property is considered to be changed. This was my suspicion and seems to agree with various references to key-value setting being wrapped by will/didSetValueForKey:
My test:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSManagedObjectContext * uiCtx = [self contextForUIWork];
NSEntityDescription * entityDesc = [NSEntityDescription entityForName:#"Entity" inManagedObjectContext:uiCtx];
Entity * entity = [[Entity alloc] initWithEntity:entityDesc insertIntoManagedObjectContext:uiCtx];
entity.testproperty = #(1);
NSError * error = nil;
[uiCtx save:&error];
if (error)
{
assert(0);
}
NSManagedObjectID * objID = entity.objectID;
[self doStuffToObjectWithIDOnAnotherThreadAndAnotherContext:objID];
entity.testproperty = #(2);
[uiCtx setMergePolicy:NSErrorMergePolicy];
error = nil;
[uiCtx save:&error];
if (!error)
{
//We do not hit this! Success!
assert(0);
}
}
-(void)doStuffToObjectWithIDOnAnotherThreadAndAnotherContext:(NSManagedObjectID*)objID
{
dispatch_barrier_sync(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSManagedObjectContext * bgCtx = [self contextForBackgroundWork];
Entity * bgEntity = (Entity*)[bgCtx objectWithID:objID];
[bgCtx performBlockAndWait:^{
//set to same value
bgEntity.testproperty = bgEntity.testproperty;
NSError * bgError = nil;
[bgCtx save:&bgError];
if (bgError)
{
assert(0);
}
}];
});
}
(Full test code uploaded here: https://github.com/samskiter/CoreDataValueChangingTest )
A citation from the docs confirming this would be far better than just some test that shows it works on this particular version of iOS.

Core Data multithreading--what am I doing wrong

I'll try to keep this brief but basically, I have an app that, in a certain mode, can near-continuously log location and other data, and snap photos (using AVFoundation) and store it all in Core Data. I discovered, as suspected, that all of this would need to be threaded...otherwise the UI gets extremely sluggish.
I have never attempted to combine Core Data with concurrency before so I read up on it as best I could. I feel like I understand what I'm supposed to do, but for someone reason it's not right. I crash with this error: "Illegal attempt to establish relationship "managedDataPoint" between objects in different contexts. I know what this means, but I thought what I have below would avoid this (I'm following what I've read)...since I get an Object ID reference from the main context, and use that to grab a new reference to the object and pass it to the "temp" context...but that isn't working as Core Data still claims I'm attempting to create a relationship across contexts (where?). Appreciate any help. Thank you!
-(void)snapPhotoForPoint:(ManagedDataPoint*)point
{
if (!_imageCapturer)
{
_imageCapturer = [[ImageCapturer alloc] init];
}
if (!_tempContext) {
_tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_tempContext.parentContext = self.managedObjectContext;
}
__block NSManagedObjectID* pointID = [point objectID];
[_tempContext performBlock:^{
NSError *error = nil;
Photo *newPhoto = [NSEntityDescription insertNewObjectForEntityForName:#"Photo" inManagedObjectContext:_tempContext];
UIImage *image = [_imageCapturer takePhoto];
newPhoto.photoData = UIImageJPEGRepresentation(image, 0.5);
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
newPhoto.managedDataPoint = tempPoint; // *** This is where I crash
if (![_tempContext save:&error]) { // I never get here.
DLog(#"*** ERROR saving temp context: %#", error.localizedDescription);
}
}];
}
Shouldn't
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
not be
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[_tempContext objectWithID:pointID];
Otherwise you are working with different contexts! Also you should check if objectID is a temporary ID and acquire a "final" one in case of.

MagicalRecord does not save data

I am trying my hand at some very basic implementation of MagicalRecord to get the hang of it and run into the following.
When I save an entry and then fetch entries of that type it will come up with the entry I just saved. However, when I save the entry, close the app, start it again, and then fetch, it comes up empty.
Code for saving:
- (void)createTestTask{
NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];
Task *task = [Task createInContext:localContext];
task.tName = #"First Task";
task.tDescription = #"First Task created with MagicalRecord. Huzzah!";
NSError *error;
[localContext save:&error];
if (error != Nil) {
NSLog(#"%#", error.description);
}
}
Code for fetching: (all I want to know here if anything is actually saved)
- (void) fetchTasks{
NSArray *tasks = [Task findAll];
NSLog(#"Found %d tasks", [tasks count]);
}
I am sure I am missing something here, but not anything I can seem to find on stackoverflow or in the Tutorials I looked at.
Any help is welcome.
I have to ask the obvious "Is it plugged in" question: Did you initialize the Core Data Stack with one of the +[MagicalRecord setupCoreDataStack] methods?
Did your stack initialize properly? That is, is your store and model compatible? When they aren't, MagicalRecord (more appropriately, Core Data) will set up the whole stack without the Persistent Store. This is annoying because it looks like everything is fine until it cannot save to the store...because there is no store. MagicalRecord has a +[MagicalRecord currentStack] method that you can use to examine the current state of the stack. Try that in the debugger after you've set up your stack.
Assuming you did that, the other thing to check is the error log. If you use
[localContext MR_saveToPersistentStoreAndWait];
Any errors should be logged to the console. Generally when you don't see data on a subsequent run of your app, it's because data was not saved when you thought you called save. And the save, in turn, does not happen because your data did not validate correctly. A common example is if you have a required property, and it's still nil at the time you call save. "Normal" core data does not log these problems at all, so you might think it worked, when, in fact, the save operation failed. MagicalRecord, on the other hand, will capture all those errors and log them to the console at least telling you what's going on with your data.
When i have started with magical record I was also facing this problem, problem is context which you are using to save data. here is my code which might help you
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *userInfoArray = [UserBasicInfo findByAttribute:#"userId" withValue:[NSNumber numberWithInt:loggedInUserId] inContext:localContext];
UserBasicInfo* userInfo;
if ([userInfoArray count]) {
userInfo = [userInfoArray objectAtIndex:0];
} else {
userInfo = [UserBasicInfo createInContext:localContext];
}
userInfo.activeUser = [NSNumber numberWithBool:YES];
userInfo.firstName = self.graphUser[#"first_name"];
userInfo.lastName = self.graphUser[#"last_name"];
userInfo.userId = #([jsonObject[#"UserId"] intValue]);
userInfo.networkUserId = #([jsonObject[#"NetworkUserId"] longLongValue]);
userInfo.userPoint = #([jsonObject[#"PointsEarned"] floatValue]);
userInfo.imageUrl = jsonObject[#"Picturelist"][0][#"PictureUrL"];
userInfo.imageUrlArray = [NSKeyedArchiver archivedDataWithRootObject:jsonObject[#"Picturelist"]];
} completion:^(BOOL success, NSError *error) {
}];
Use this when your done
[[NSManagedObjectContext MR_defaultContext]saveToPersistentStoreAndWait];

Core Data not firing fault for an NSManagedObject instance as property on app delegate

I import the logged in user's data from server into a Core Data Entity called "User". I also keep a reference of this specific User object onto my AppDelegate (as a property) so I can access it elsewhere in my app. The problem I am facing is, when I push another view controller and try to access appdelegate.loggedInUser.id , I see that "id" is nil. Debugger shows this for the object :
$24 = 0x0b28ad30 <User: 0xb28ad30> (entity: User; id: 0xb261160 <x-coredata:///User/tC48E8991-B8A6-4E68-9112-93F9F21DB5382> ; data: <fault>)
My understanding was that the Core Data framework would fire the fault the moment I try to access one of the properties of this object. I am confused as to why me accessing the "id" property of the user is not firing a fault in this case?
EDIT:
This is how create and use the loggedInUser object :
//method to get bgContext
+(NSManagedObjectContext *)getContextOnBgWithParentSetToMainMOC
{
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[tmpContext setParentContext:[Utils getAppDelegate].managedObjectContext];
return tmpContext;
}
//in App Delegate
NSManagedObjectContext *bgContext = [NSManagedObjectContext getContextOnBgWithParentSetToMainMOC];
self.loggedInUser = [User importFromObject:loggedInUserData inContext:bgContext completionBlock:^(NSManagedObjectContext *theContext, NSManagedObject *theManagedObjectWithValuesImported) {}];
//In User.m file
+ (User *)importFromObject:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context completionBlock:(TemporaryContextImportReturnBlock)block {
if ( !context ){
context = [NSManagedObjectContext getContextOnBgWithParentSetToMainMOC];
}
NSManagedObjectContext *localContext = context;
User *newUserEntity = [NSEntityDescription insertNewObjectForEntityForName:#"User" inManagedObjectContext:localContext];
NSArray *emailsArray = [dictionary objectForKey:#"emails"];
NSString *emailsString = #"";
if ([emailsArray count] > 0){
emailsString = [emailsArray componentsJoinedByString:#","];
}
newUserEntity.emails = emailsString;
newUserEntity.id = [dictionary objectForKey:#"id"];
newUserEntity.n = [dictionary nonNullObjectForKey:#"n"];
return newUserEntity;
}
//Access in one of the view controllers
User *loggedInUser = [Utils getAppDelegate].loggedInUser;
// loggedInUser.id /*nil*/
I have the same problem. It turns out, according to this answer, which references the Apple docs, that an NSManagedObject does not hold a strong reference to its NSManagedObjectContext as you might expect. I suspect that if you inspect your object when it doesn't fire the fault properly that [myObject managedObjectContext] == nil.
I don't know what best practices are here. The obvious (but potentially difficult) solution is to find out why your MOC is being deallocated. As an alternative, although I'm unsure whether it's safe to do, you could retain the MOC from each NSManagedObject instance. (I have question about that open here.)
make sure, that you do not call
[managedObjectContext reset];
somewhere. From Apple doc:
Returns the receiver to its base state.
All the receiver's managed objects are “forgotten.” If you use this method, you should ensure that you also discard references to any managed objects fetched using the receiver, since they will be invalid afterwards.
Those "orphaned" managed object's managedObjectContext property will change to nil and they will not be able to fire faults anymore.

I have an issue in saving individual Object, rather than saving all the objects in NSMangedObject stack ??

I am new to the IOS programming, currently learning core data, I went into running the code where i need to save only specific objects in core data. So how can i do that?
According to the scenario, i have data from server as well as local storage (core data), but when user close the app (went to background) I want to store the data in the server(if net available) if not then in the local storage (but selected only - means specific data should be stored, there are objects which came from online server which i dont want to store on local).
Please let me know any solution if possible.
Regards
Nisar Ahmed
I see two ways to achieve this:
Iterate through inserted and updated objects and revert those you do not wont to save. Inserted objects should be deleted, updated should be refreshed:
for (NSManagedObject* obj in [self.managedObjectContext insertedObjects]) {
if (/*Shouldn't be saved*/) {
[self.managedObjectContext deleteObject:obj];
}
}
for (NSManagedObject* obj in [self.managedObjectContext updatedObjects]) {
if (/*Shouldn't be saved*/) {
[self.managedObjectContext refreshObject:obj mergeChanges:NO];
}
}
Create separate managed object context. Recreate objects that you want to save in new context and then save it.
NSManagedObjectContext* newContext = [[NSManagedObjectContext alloc] init];
[newContext setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];
for (NSManagedObject* obj in objectsWantToSave) {
NSEntityDescription* entity = [obj entity];
NSDictionary* valuesByKeys = [obj dictionaryWithValuesForKeys:[[entity attributesByName] allKeys]];
NSManagedObject* objCopy = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:newContext];
[objCopy setValuesForKeysWithDictionary:valuesByKeys];
}
[newContext save:NULL];
The second approach is better for my opinion.
Have a look into UIManagedDocument - http://developer.apple.com/library/ios/#documentation/uikit/reference/UIManagedDocument_Class/Reference/Reference.html
It takes care of a lot of the boilerplate involved in using core data.

Resources