What are the delete semantics for RestKit when using it with Core Data?
For example, assume I correctly set a primaryKeyAttribute in RestKit for Organization entities. If I do a GET on, say, /organizations/ I get entries for /organizations/1/, /organizations/2/, and /organizations/3/ back. Let's say I do a GET on /organizations/ a bit later and only get entries for /organizations/1/ and /organizations/3/ back. `/organizations/2/ has been deleted on the server.
I would expect RestKit to delete my Core Data record for /organizations/2/. Is this what RestKit does or do I have to implement this behavior? Does this change in any way if I am using the reboot-networking-layer branch? Are there any settings in RestKit I should be aware of that affect this behavior?
You need to implement FetchRequests in order for those objects to be deleted. Here is a simple example:
[objectManager addFetchRequestBlock:^NSFetchRequest *(NSURL *URL) {
RKPathMatcher *pathMatcher = [RKPathMatcher pathMatcherWithPattern:#"/organizations"];
NSDictionary *argsDict = nil;
BOOL match = [pathMatcher matchesPath:[URL relativePath] tokenizeQueryStrings:NO parsedArguments:&argsDict];
if (match) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Organization"];
return fetchRequest;
}
return nil;
}];
I do this in the ApplicationDelegate(the same place I setup RestKit). After you do it there, all other calls will automatically delete orphaned objects.
I have a Mac document-based app, using NSPersistentDocument for the document model.
When new document is created, the app adds some default data (several sport objects and user data) to the document in the initiWithType method.
- (id)initWithType:(NSString *)typeName error:(NSError **)outError {
self = [super initWithType:typeName error:outError];
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
[[SportManagement sharedManager] addDefaultSports:managedObjectContext];
[[UserManagement sharedManager] addDefaultUser:managedObjectContext];
[managedObjectContext processPendingChanges];
return self;
}
The app has an import function that imports data from some hardware, which runs in a thread, which I set up as follows (managedObjectContext is that of the NSPersistentDocument):
dispatch_async(dispatch_get_global_queue(0, 0), ^ {
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[moc setPersistentStoreCoordinator:[managedObjectContext persistentStoreCoordinator]];
Data is imported from the hardware into a number of NSManagedDataObject items. Each ManagedObject has a 'Sport' field, which needs to be populated with one of the sport objects created when the document was created.
However, none of the sport objects that were added in the - (id)initWithType:(NSString *)typeName error:(NSError **)outError exist in the new ManagedObjectContext in the thread (moc).
If I run the app, create a new document, then let the app sit idle for a minute-or-so, then try the import, then the Sport objects DO exist in the thread Managed Object Context.
How do I sync the new ManagedObjectContext in the thread with the main one from the NSPersistantDocument?
I've tried: [managedObjectContext processPendingChanges]; and [managedObjectContext setStalenessInterval];, but neither seem the solve this problem.
Interestingly, this doesn't appear to happen in Mac OS X 10.8, only in 10.7
Setup your "main" MOC to receive NSManagedObjectContextDidSaveNotification notifications, and merge the changes when the background MOC saves with -mergeChangesFromContextDidSaveNotification:.
EDIT
OK, it looks like you have made your changes in the MOC, but it is just a scratchpad. Until the data is actually saved to the persistent store, the persistent store does not know about the new data changes.
Thus, when you create your other MOC and connect it to the PSC, it does not know about those changes.
You can tell when autosave kicks in, because "after a while" it works.
I would try a manual save of the document after you create the initial content.
I have an NSPersistentDocument subclass using NSManagedObject subclasses for my data.
When a new document is opened, I do some initializing of data structures (trivial amount of populating fields). What I've noticed is that the Untitled document gets autosaved, and when the application re-opens, that document gets loaded. If the application quits, the user doesn't (by default) get prompted with the save dialog. If the window closes, the user does.
First question:
I want to call up the save dialog when the user quits the application. I don't want this Untitled document hanging around (under normal circumstances). I either want it saved or trashed.
I attempted to fill out:
- (void)applicationWillTerminate:(NSNotification *)aNotification
In order to trigger the document to be saved. Calling save: on the context at this point gives an error. From what I can tell, this is because the user hasn't yet saved the file on their own. In addition, calling [self close]; or [[self windowForSheet] close]; close the window without saving.
How can I force the save dialog to come up? How can I trash the untitled document?
Second question (no, I can't count):
Since when the application starts, there may or may not be an Untitled document to deal with, I'm trying to keep track of the state in another model. I've already found that the initial data (to which I referred earlier) is present when the Untitled document came up. My other model has some metadata, including a success flag/state for the populated data. Once the populated data is all in place and correct, the state indicates as such. Unfortunately, while my populated data is being loaded when the app starts with a pre-existing Untitled document, the metadata class is not.
Please excuse the roughness of the code, at this point, I'm mucking it up until I can see that it's working how I want before I polish it back off:
- (bool) createGameState {
NSEntityDescription* description = [NSEntityDescription entityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:description];
NSError *error = nil;
NSArray *array = [[self managedObjectContext] executeFetchRequest:req error:&error];
[req release];
req = nil;
GameState* result = nil;
if (array) {
NSUInteger count = [array count];
if (!count) {
// Create the new GameState.
DebugLog(#"Creating GameState");
result = [NSEntityDescription insertNewObjectForEntityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
[result setIsLoaded:[NSNumber numberWithBool:NO]];
} else {
if (count > 1) {
NSLog(#"WARNING: Potentially Corrupt Game State. found: %lu", count);
}
result = [array objectAtIndex:0];
if ([result isLoaded]) {
[self variantLoaded];
} else {
// In this case, we have an aborted set-up. Since the game isn't
// playable, just refuse to create the GameState. This will
// force the user to create a new game.
return NO;
}
}
} else {
DebugLog(#"error: %#", error);
}
[game setState:result];
return result;
}
Note that array is always present, and count is always zero. No, I'm not explicitly calling save: anywhere. I'm relying on the standard auto-save, or the user performing a save.
EDIT:
I installed the Core Data Editor app. It turns out the issue isn't on saving the data, but on loading it. (Note: Due to another issue, the app saves as binary when instructed to save as XML, which causes much head banging.)
I've broken it down to the simplest code, which should pick up all objects of type GameState in an array. It retrieves none, despite there clearly being objects of the appropriate type in the saved file:
NSManagedObjectContext* moc = [self managedObjectContext];
NSEntityDescription* entity = [NSEntityDescription entityForName:#"GameState" inManagedObjectContext:moc];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:entity];
NSError *error = nil;
NSArray *array = [moc executeFetchRequest:req error:&error];
Array is not null, but [array count] is 0.
At this point, I'm guessing it's something simple that I'm overlooking.
Second EDIT:
I added -com.apple.CoreData.SQLDebug 5 and saved as SQLite. The call to executeFetchRequest does not generate any debug logs. I do see the INSERT INTO ZGAMESTATE entry show up in the logs. It seems that executeFetchRequest is not getting passed to the backend.
Third EDIT (this one burns):
I created a new xcode project, using core data (as I had with the other). I copied just this one function (stubbing where necessary) and plopped a call to it in windowControllerDidLoadNib. In this new project, the code above works.
Found the problem.
I errantly was loading objects in Document's - (id) init call. Moved to windowControllerDidLoadNib (which is what I did in the test version) and it worked fine.
I have a one to many relationship in my core data model. I need to create a new entity and save it. The entity has a one to many relationship which generated the following code:
- (void)addRelationshipEvent1:(NSSet *)values;
- (void)removeRelationshipEvent1:(NSSet *)values;
.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
ApplicationRecord *newManagedObject = (ApplicationRecord*)[NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
newManagedObject.startDate = [NSDate date];
newManagedObject.stopDate = [[NSDate date] dateByAddingTimeInterval:120];
//keep adding individual dynamic properties
is it correct to set the -toMany relationship sets to nil initially? Or do I need to initialize an (empty?) set here and assign it? Would I be able to add extra objects later if I set the initial set to nil?
newManagedObject.relationshipEvent1 = nil;
newManagedObject.relationshipEvent2 = nil;
//...
// Save the context.
NSError *error = nil;
if (![context save:&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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
Alex,
You don't need to intialize your relationships. Just use the supplied accessors or helper functions and Core Data takes care of it. IOW, only worry about the property/relationship when you need to actually use it.
Andrew
This question is probably a long shot. I can't figure out the errors I'm getting on my core data project when I save after I delete an entity.
I have two main entities that I work with, an Outfit, and an Article. I can create them with no problem but when I delete them I get the follow error log:
For the Outfit:
2009-09-22 20:17:37.771 itryiton[29027:20b] Operation could not be completed. (Cocoa error 1600.)
2009-09-22 20:17:37.773 itryiton[29027:20b] {
NSLocalizedDescription = "Operation could not be completed. (Cocoa error 1600.)";
NSValidationErrorKey = outfitArticleViewProperties;
NSValidationErrorObject = <Article: 0x12aa3c0> (entity: Article; id: 0x12b49a0 <x-coredata://7046DA47-FCE1-4E21-8D7B-E532AAC0CC46/Article/p1> ; data: {
articleID = 2009-09-22 19:05:19 -0400;
articleImage = 0x12b4de0 <x-coredata://7046DA47-FCE1-4E21-8D7B-E532AAC0CC46/ArticleImage/p1>;
articleType = nil;
attributeTitles = "(...not nil..)";
color = nil;
comment = nil;
dateCreated = 2009-09-22 19:05:19 -0400;
designer = nil;
imageView = "(...not nil..)";
location = "(...not nil..)";
outfitArticleViewProperties = (
0x12b50f0 <x-coredata://7046DA47-FCE1-4E21-8D7B-E532AAC0CC46/OutfitArticleViewProperties/p1>
);
ownesOrWants = 0;
pattern = nil;
price = nil;
retailer = nil;
thumbnail = "(...not nil..)";
washRequirements = nil;
wearableSeasons = nil;
});
NSValidationErrorValue = {(
<OutfitArticleViewProperties: 0x1215340> (entity: OutfitArticleViewProperties; id: 0x12b50f0 <x-coredata://7046DA47-FCE1-4E21-8D7B-E532AAC0CC46/OutfitArticleViewProperties/p1> ; data: {
article = 0x12b49a0 <x-coredata://7046DA47-FCE1-4E21-8D7B-E532AAC0CC46/Article/p1>;
articleViewPropertiesID = nil;
outfit = nil;
touch = nil;
view = "(...not nil..)";
})
)};
}
And if I delete an Article I get:
2009-09-22 18:58:38.591 itryiton[28655:20b] Operation could not be completed. (Cocoa error 1560.)
2009-09-22 18:58:38.593 itryiton[28655:20b] DetailedError: {
NSLocalizedDescription = "Operation could not be completed. (Cocoa error 1600.)";
NSValidationErrorKey = articleImage;
NSValidationErrorObject = <Article: 0x12aa340> (entity: Article; id: 0x12b3f10 <x-coredata://05340FA6-B5DC-4646-A5B4-745C828C73C3/Article/p1> ; data: {
articleID = 2009-09-22 18:58:26 -0400;
articleImage = 0x12b4d00 <x-coredata://05340FA6-B5DC-4646-A5B4-745C828C73C3/ArticleImage/p1>;
articleType = nil;
attributeTitles = "(...not nil..)";
color = nil;
comment = nil;
dateCreated = 2009-09-22 18:58:26 -0400;
designer = nil;
imageView = "(...not nil..)";
location = "(...not nil..)";
outfitArticleViewProperties = (
0x12b5010 <x-coredata://05340FA6-B5DC-4646-A5B4-745C828C73C3/OutfitArticleViewProperties/p1>
);
ownesOrWants = 0;
pattern = nil;
price = nil;
retailer = nil;
thumbnail = "(...not nil..)";
washRequirements = nil;
wearableSeasons = nil;
});
NSValidationErrorValue = <ArticleImage: 0x12ad600> (entity: ArticleImage; id: 0x12b4d00 <x-coredata://05340FA6-B5DC-4646-A5B4-745C828C73C3/ArticleImage/p1> ; data: {
article = 0x12b3f10 <x-coredata://05340FA6-B5DC-4646-A5B4-745C828C73C3/Article/p1>;
image = "(...not nil..)";
});
}
A 1600 error is:
NSValidationRelationshipDeniedDeleteError
Error code to denote some relationship
with delete rule NSDeleteRuleDeny is
non-empty.
Available in Mac OS X v10.4 and later.
Declared in CoreDataErrors.h.
But I can't see for the life of me which relationship would be preventing the delete. If some Core Data wizard can see the error of my ways, I would be humbled.
I can't mark this solved, because I didn't really solve it, but I do have a working work-around. In the .m for each of my managedObjects I added a method that looks like:
-(void) deleteFromManangedObjectContext{
self.outfit = nil;
self.article = nil;
[[self managedObjectContext] deleteObject:self];
}
So you can see, first I manually nil out the relationships, and then I have the object delete itself. In other objects, instead of nil-ing, my delete method is called on some of the objects relationships, to get a cascade.
I just had the problem of delete fail, and landed on this question. And I've figured out my problem and thought that I'd share that too and maybe someone will have the same problem as well.
The mistake I made is that the object (A) I am trying to delete have a relationship to another object (B) with NULL as delete rule. However, object B also have a relationship to A and it's non-optional. Therefore, when I delete A, B's relationship of A becomes null which is not allowed. When I change the delete rule to cascade and it worked.
Do you happen to implement some of the accessor to the relationship yourself?
I once had a code like
-(NSSet*)articles
{
re-calculates properties....
return [self primitiveValueForKey:#"articles"];
}
in a subclass of NSManagedObject and had a save error.
What happened was that, when this object is deleted from the ManagedObjectContext, the CoreData calls the accessor "articles" to deal with the delete propagation. This re-calculation of articles occurred during the delete propagation, which re-surrected the nullified "articles" in my case.
I can't mark this solved, because I didn't really solve it, but I do have a working work-around. In the .m for each of my managedObjects I added a method that looks like:
-(void) deleteFromManangedObjectContext{
self.outfit = nil;
self.article = nil;
[[self managedObjectContext] deleteObject:self];
}
So you can see, first I manually nil out the relationships, and then I have the object delete itself. In other objects, instead of nil-ing, my delete method is called on some of the objects relationships, to get a cascade.
I'm still interested in the "right" answer. But this is the best solution I have, and it does allow for some fine-grained control over how my relationships are deleted.
Check your xcdatamodel file for a Deny delete rule. Click on each relationship until you find it. You'll need to change this rule or adjust how you delete managed objects to anticipate the rule's application to the relationship.
I recently encountered this error because I had code in the - (void)willSave method which updated some of the properties of the delete managed object after - (BOOL)isDeleted already returned true.
I fixed it by:
- (void)willSave {
if (![self isDeleted]) {
//Do stuff...
}
}
I had a similar problem where it turned out the problem was in the .xib file. When I switched on the check box for "Deletes Objects on Remove" (under Bindings->Content Set) of the relevant Array Controller, the problem went away.
Don't know if this will help in your case, but I've had a lot of hairs go gray over problems that turned out be hidden away somewhere inside Interface Builder.
In my case I have innocently created custom method in my subclass of NSManagedObject: isDeleted. I was encountering strange save exceptions until I removed / renamed it.
After losing my sanity, I read documentation again more through-fully this time.
It turned out I overridden one of the NSManagedObject methods one MUST NOT OVERRIDE.
Check if this excerpt from docs helps you:
Methods you Must Not Override
NSManagedObject itself customizes many features of NSObject so that
managed objects can be properly integrated into the Core Data
infrastructure. Core Data relies on NSManagedObject’s implementation
of the following methods, which you therefore absolutely must not
override: primitiveValueForKey:, setPrimitiveValue:forKey:,
isEqual:, hash, superclass, class, self, isProxy, isKindOfClass:,
isMemberOfClass:, conformsToProtocol:, respondsToSelector:,
managedObjectContext, entity, objectID, isInserted, isUpdated,
isDeleted, and isFault, alloc, allocWithZone:, new, instancesRespondToSelector:, instanceMethodForSelector:,
methodForSelector:, methodSignatureForSelector:,
instanceMethodSignatureForSelector:, or isSubclassOfClass:.
Besides - there are other methods you can override but you MUST CALL super implementation like or call: willAccessPrimitiveForKey, didAccessPrimitiveForKey in accessors and willChangevalueForKey, didChangeValueForKey in setters....
I was encountering a very similar issue with cascading deletes, on non optional parent-child relationships. It was very confusing because I thought the parent relationship delete rule was set to cascade. It turns out that the data model editor in Xcode was not saving the delete rule. I would set it to Cascade, go to a different view and come back and it would be set to nullify again. I had to restart Xcode and set the delete rule to cascade. After I did this everything worked.
So if anyone else encounters this issue double check that Xcode is saving your delete rules before delving into more complicated solutions.
By the way I'm using core data on iOS with Xcode 5's data model editor.