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.
Related
I'm downloading often identical data on multiple calls to different URLs in RESTKit. The first call maps fine, but the subsequent calls then replace the first call's entities.
The behaviour that I want is to have the objects unique to within their parent entity, so even if the data looks the same I still want a new object to be created, but at the moment it looks like RESTKit wants them to be unique across the entire database. There's no unique key to do this in the data I'm downloading, the objects are literally identical. Below is the code I'm using to create the operation. How do I set this up to allow duplicates?
NSMutableURLRequest *request = [self requestWithURL:URL];
[request setHTTPMethod:#"GET"];
RKHTTPRequestOperation *requestOperation = [[RKHTTPRequestOperation alloc]initWithRequest:request];
RKResponseDescriptor *responseDescriptor = [INKResponseDescriptorFactory journeySegmentDescriptorForPath:URL.path inStore:[RKObjectManager sharedManager].managedObjectStore];
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:requestOperation
responseDescriptors:#[responseDescriptor]];
operation.managedObjectContext = [self.objectManager context];
operation.managedObjectCache = self.objectManager.managedObjectStore.managedObjectCache;
operation.savesToPersistentStore = NO;
[operation setCompletionBlockWithSuccess: ^(RKObjectRequestOperation *requestOperation, RKMappingResult *mappingResult)
{
success();
} failure: ^(RKObjectRequestOperation *requestOperation, NSError *error) {
failure(error);
}];
[self.objectManager enqueueObjectRequestOperation:operation];
Figured out a way to do this.
Each time I'm updating the data, even though the entities I get contain identical data, I'm retrieving them from a different url.
RESTKit allows you to retrieve metadata from you calls and map this into your managed objects properties.
So what I've done is map the URL I used for the request into a property and used that as well as an identifier, which is unique within this call's returned objects, to make objects which will be unique throughout the database.
So my mapping now looks like this:
RKEntityMapping *seatMapping = [RKEntityMapping mappingForEntityForName:#"Seat" inManagedObjectStore:store];
[seatMapping addAttributeMappingsFromDictionary:#{ #"designator" : #"designator",
#"status" : #"status",
#"#metadata.HTTP.request.URL" : #"requestURL"}];
seatMapping.identificationAttributes = #[#"requestURL", #"designator"];
I would like to use the item that has been just been saved in the completion block of Magical Record saveWithBlock method. For example:
//Get the ID of an existing NSManagedObject to use in the save block (if it exists)
NSManagedObjectID *objectRef = [self.object objectID];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
//This method either loads an existing object and makes changes or creates a new entity in localContext
NSManagedObject *itemToSave = [self prepareItemInContext:localContext WithID: objectRef];
} completion:^(BOOL success, NSError *error) {
if (success) {
//here I want to get at the object 'itemToSave' that was either created in the save block (with a new objectID) or updated (with the ID objectRef)
Well, you need to have a reference to your external context to load the object with that ID:
NSManagedObjectContext *outsideContext = //...
NSManagedObjectID *objectID = //...
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
} completion:^(BOOL success, NSError *error) {
NSManagedObject *newlySavedObject = [outsideContext existingObjectWithID:objectID];
//...do stuff here
}];
Generally, however, I would discourage this usage. I would instead recommend keeping any predicates or means of reloading your data set handy, and dump and refetch fresh data from the store. This will give you proper object references. Another, more precise way of updating objects in other contexts is to listen to the NSManagedObjectContextDidSaveNotification and merge this updates into your context. From there, your data will be "refreshed" and as long as you're KVO'ing a property, or using a NSFetchedResultsController with a delegate, your updates will propagate to the UI (or other destination).
Either just use self.object or, if you create a new object and insert it (presumably because objectRef is nil) then you should get the corresponding new object from the main thread context and use that.
How you do that shuffle is the interesting part. It isn't exactly clear why you're using a background context at the moment so you can also consider changing that, which removes all of the complexity.
If you need to keep the background context then you need to decide on how to get that data back to the main thread. Generally, you could use performBlockAndWait: inside your current block to get the new object from the main context and then store it into a property on your class so you can use it in the completion block. This would be setting the self.object property.
I'm using AFNetworking and MagicalRecord (the current develop branch) and I'm trying to figure out how to import a lot of objects which are dependent on each other. Each resource/entity has multiple pages worth of downloads. I have a class managing the downloads for a given entity and saving them using MagicalDataImport (which has been amazing).
I believe my issue is that the imports aren't happening on the same thread. So I think what is happening is:
In one thread, EntityA is getting saved properly and propagated to the parent entity.
Then in another thread, EntityB is being saved, and along with it it's relationship to EntityA is built. That means a blank (fault?) object is being created. Then when it gets propagated to the parent entity, I believe EntityA is overwriting the EntityA that is there. Thus I'm left with some objects that don't have all of the attributes.
At least, I think that is what is happening. What I'm seeing via the UI is actually that the relationships between entities aren't always built correctly.
My end goal is to get the entire download/import process to be done in the background, not effecting the UI at all.
Here is my AFJSONRequest:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self saveResources:[JSON objectForKey:#"data"]];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON)
{
DLog(#"%#",error.userInfo);
[self.webService command:self didFail:error.localizedDescription];
}];
[operation setQueuePriority:self.priority];
And it calls saveResources::
- (void)saveResources:(NSArray*)resources {
BOOL stopDownloads = [self stopDownloadsBasedOnDate:resources];
if ([resources count] > 0 && !stopDownloads){
self.offset = #([offset intValue] + [resources count]);
[self send];
}
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *blockLocalContext) {
[self.classRef MR_importFromArray:resources inContext:blockLocalContext];
} completion:^(BOOL success, NSError *error) {
if (error){
// ... handle errors
}
else {
// ... handle callbacks
}
}];
}
This kicks off another download ([self send]) and then saves the objects.
I know by default AFNetworking calls the callback in the main queue, and I've tried setting the SuccessCallbackQueue/FailureCallbackQueue to my background thread, but that doesn't seem to solve all the issues, I still have some relationships going to faulted objects, though I think I do need to do that to keep everything going in a background thread.
Is there anything else I need to call in order to properly propagate these changes to the main context? Or is there a different way I need to set this up in order to make sure that all the objects are saved correctly and the relationships are properly built?
Update
I've rewritten the issue to try to give more clarification to the issues.
Update
If you need more code I created a gist with (I believe) everything.
I ended up having this exact same issue a few days ago. My issue was I had received a customer record from my API with AFNetworking. That customer could have pets, but at this point I didn't have the petTypes to correspond to the customers pet record.
What I did to resolve this was create a transformable attribute with an NSArray which would temporarly store my pets until my petTypes were imported. Upon the importation of petTypes I then triggered an NSNotificationCenter postNotification (or you can just do the pet import in the completion).
I enumerated through the temporary transformable attribute that stored my pet records and then associated the with the petType
Also I see you are doing your import inside of a save handler. This is not needed. Doing your MR_importFromArray will save automatically. If you are not using an MR_import method then you would use the saveToPersistentStore methods.
One thing is I don't see where you are associating the relationships. Is EntityB's relationship to EntityA being sent over via JSON with the EntityA objecting being in EntityB?
If so then this is where the relationship is getting messed up as it is creating / overwriting the existing EntityA for the one provided in EntityB. My recommendation would be to do something like this.
NSArray *petFactors = [responseObject valueForKeyPath:#"details.items"];
NSManagedObjectContext *currentContext = [NSManagedObjectContext MR_context];
Pets *pet = [Pets MR_findFirstByAttribute:#"id" withValue:petId inContext:currentContext];
pet.petFactors = nil;
for (id factor in petFactors) {
[pet addPetFactorsObject:[PetFactors MR_findFirstByAttribute:#"id" withValue:[factor valueForKey:#"factorId"]]];
}
[currentContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"SAVED PET FACTORS");
[[NSNotificationCenter defaultCenter] postNotificationName:kPetFactorsSavedSuccessfully object:nil];
} else {
NSLog(#"Error: %#", error);
}
}];
I'm putting this as an answer, though I'm not 100% sure if this is your issue or not. I think the issue stems from your localContext. Here is a sample web request method from an app we wrote that uses data importing, you may be able to use it as an example to get yours working.
Note that the AFNetworking performs its completion block on the main thread, then the MagicalRecord saveInBackground method switches back to a background thread to do the importing and processing, then the final MR completion block performs the handler block on the main thread again. The localContext that's used to import is created/managed by the saveInBackground method. Once that method is complete the context is saved and merged with the app's main context and all the data can then be accessed.
- (void)listWithCompletionHandler:(void (^)(BOOL success))handler{
[[MyAPIClient sharedClient] getPath:#"list.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject){
NSString *statusString = [responseObject objectForKey:#"status"];
// Handle an error response
if(![statusString isKindOfClass:[NSString class]] || ![statusString isEqualToString:#"success"]){
// Request failure
NSLog(#"List Request Error: %#", statusString);
NSLog(#"%#", [responseObject objectForKey:#"message"]);
if(handler)
handler(NO);
return;
}
NSArray *itemsArray = [responseObject objectForKey:#"items"];
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext){
// Load into internal database
NSArray *fetchedItems = [Item importFromArray:itemsArray inContext:localContext];
NSLog(#"Loaded %d Items", [fetchedItems count]);
} completion:^{
if(handler)
handler(YES);
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
NSLog(#"Fail: %#", error);
if(handler)
handler(NO);
}];
}
I have an entity called Practice and I use a View Controller called SelectorViewController to select one of the practices, selectedPractice. I then return selectedPractice to a view Controller called RegularViewController where I display some of the selectedPractice attributes. All of this works fine. However the app has a number of other View Controllers which can be reached by modal segues from instances of RegularViewController. As a result, if I leave and then come back to RegularViewController, selectedPractice is reset as null. I would also like to save selectedPractice so that it is available at app initialisation if it has previously been set in SelectorViewController. How do I achieve this by making selectedPractice persistent across the app, and available at runtime?
Regards
Thanks to the post above, which was great, I managed to sort it. Here is my code, which may be very clumsy, but it works.
Firstly, as I loaded the fetchedObjects into a PickerView in SelectorView Controller, I set an attribute "isSelectedPractice" to "NO" with the following code:
for (Practice *fetchedPractice in [self.fetchedResultsController fetchedObjects]) {
[fetchedPractice setValue:#"NO" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
I then identified for the selected Practice:
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
Practice *practice = [[self.fetchedResultsController fetchedObjects] objectAtIndex:row];
self.selectedPractice = practice;
NSLog(#"The '%#' practice was selected using the picker", self.selectedPractice.name);
}
as the view Segue'd back to RegularViewController I set the isSelectedPractice attribute for selectedPractice to YES. I kept it this late as I didn't want more than one selection in the PickerView to result in multiple objects with isSelectedPractice YES.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"SavedPractice Segue"])
{
[self.selectedPractice setValue:#"YES" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
NSLog(#"Setting SelectedPractice as '%#' in RegularViewController with isSelectedPractice as '%#'",self.selectedPractice.name,self.selectedPractice.isSelectedPractice );
RegularViewController *rvc= segue.destinationViewController;
rvc.delegate = self;
rvc.selectedPractice = self.selectedPractice;
}
else {
NSLog(#"Unidentified Segue Attempted!");
}
}
I then set the following Predicate in the setupFetchedResultsController method of RegularViewController:
request.predicate = [NSPredicate predicateWithFormat:#"isSelectedPractice = %#", #"YES"];
Many thanks for the help
Without seeing your actual project, one way I know will work but might be a little too round a bout would be to add an attribute "isSelectedPractice" to your entity. You could make it a BOOL, but I've had mixed results with BOOL's in Core Data, I prefer to just leave it as a NSString and set it to "yes" or "no". Then when you pull it down, modify it or add it to core Data as a entity with isSelectedPractice set to "yes". Then in your other controllers, do a
if (self.managedObjectContext == nil) {
self.managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
then do a fetch request to get entities with a predicate which is looking for isSelectedPractice equaling "yes". If you need actual code samples on how to do this let me know and I'll edit them in.
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.