Storing JSON objects into Core Data - objective-c

I'm trying to sync my local core data database with a remote JSON API. I'm using RestKit to map JSON values into local managed objects. here is a piece of code:
- (IBAction)testButtonPressed:(id)sender {
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
RKManagedObjectStore *managedObjectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error = nil;
BOOL success = RKEnsureDirectoryExistsAtPath(RKApplicationDataDirectory(), &error);
if (! success) {
RKLogError(#"Failed to create Application Data Directory at path '%#': %#", RKApplicationDataDirectory(), error);
}
// - - - - - - - - Change the path !
NSString *path = [RKApplicationDataDirectory() stringByAppendingPathComponent:#"AC.sqlite"];
NSPersistentStore *persistentStore = [managedObjectStore addSQLitePersistentStoreAtPath:path
fromSeedDatabaseAtPath:nil
withConfiguration:nil
options:nil
error:&error];
if (! persistentStore) {
RKLogError(#"Failed adding persistent store at path '%#': %#", path, error);
}
[managedObjectStore createManagedObjectContexts];
// - - - - - - - - Here we change keys and values
RKEntityMapping *placeMapping = [RKEntityMapping mappingForEntityForName:#"Place"
inManagedObjectStore:managedObjectStore];
[placeMapping addAttributeMappingsFromDictionary:#{
#"place_id": #"place_id",
#"place_title": #"place_title",
#"site": #"site",
#"address": #"address",
#"phone": #"phone",
#"urating": #"urating",
#"worktime": #"worktime",
#"lat": #"lat",
#"lng": #"lng",
#"about": #"about",
#"discount": #"discount",
#"subcategory_title": #"subcategory_title",
#"subcategory_id": #"subcategory_id",
#"category_title": #"category_title",
#"image_url": #"image_url"}];
//RKEntityMapping *articleMapping = [RKEntityMapping mappingForEntityForName:#"Article" inManagedObjectStore:managedObjectStore];
//[articleMapping addAttributeMappingsFromArray:#[#"title", #"author", #"body"]];
//[articleMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"categories" toKeyPath:#"categories" withMapping:categoryMapping]];
NSIndexSet *statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful); // Anything in 2xx
// here we need to change too
RKResponseDescriptor *responseDescriptor =
[RKResponseDescriptor responseDescriptorWithMapping:placeMapping
pathPattern:nil // #"/articles/:articleID"
keyPath:#"data.place_list"
statusCodes:statusCodes];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://allocentral.api.v1.ladybirdapps.com/place/?access_token=19f2a8d8f31d0649ea19d478e96f9f89b&category_id=1&limit=10"]];
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request
responseDescriptors:#[responseDescriptor]];
operation.managedObjectContext = managedObjectStore.mainQueueManagedObjectContext;
operation.managedObjectCache = managedObjectStore.managedObjectCache;
[operation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *result) {
NSLog(#" successfull mapping ");
[self refreshContent];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failed with error: %#", [error localizedDescription]);
}];
NSOperationQueue *operationQueue = [NSOperationQueue new];
[operationQueue addOperation:operation];
}
- (void) refreshContent {
// perform fetch
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
// reload data
[self.tableView reloadData];
}
it works perfect and gets all the objects and stores them in core data, BUT if some objects are deleted on the server, and they are not in the JSON response, they stay in the detebase. how can i make restkit clear out objects that are not in the response? thx

Anytime you receive a new JSON response from your server, you should process it as normal, adding new entries into your Core Data objects.
Then iterate through your Core Data objects, and check to see if they're included in the JSON (using whatever method makes sense for your objects), and if not, delete them.
Alternatively, if you are passing some kind of ID in with the JSON, you could store each ID in an NSArray at the same time as you're adding objects to Core Data. Then do a predicate search for any Core Data objects that don't match the IDs in the array, and delete them.
Which is better depends on whether you have more new/existing items or more to-be-deleted items.

Related

Core Data update does not save and does not give any error, why?

I am using a very simple piece of code to update an NSManagedObject, but the save does not make it to the persistent store (SQLite). There are no error messages and the logs look ok so I am a little lost.
I have scaled down the code as much as possible to try and isolate the problem as shown below.
The logs tell me that the orderNumber and status are set correctly and also the debug output of the arrays are all correct, but my simple update still fails without any error at all.
+ (int) synchOrderWithStatusUpdate:(NSString *)orderNumber : (int)status {
if ([NWTillHelper isDebug] == 1) {
NSNumber *statusCheck = [NSNumber numberWithInt:status];
NSLog(#"WebServices:synchOrderWithStatusUpdate:ordernumber = %#, status = %d, status2 = %#", orderNumber, status, statusCheck);
}
synchOrderErrorCode = 0;
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication]delegate];
NSManagedObjectContext *context = appDelegate.persistentContainer.viewContext;
// *** The query for all tables uses orderNumber as selection so we set that up first for re use
NSPredicate *predicateOrderNumber =[NSPredicate predicateWithFormat:#"orderNumber like[cd] %#", [NWTillHelper getCurrentOrderNumber]];
NSFetchRequest *fetchRequestOh = [[NSFetchRequest alloc] initWithEntityName:#"OrderHead"];
NSFetchRequest *fetchRequestOrp = [[NSFetchRequest alloc] initWithEntityName:#"OrderRow"];
NSFetchRequest *fetchRequestTender = [[NSFetchRequest alloc] initWithEntityName:#"Tender"];
fetchRequestOh.predicate = predicateOrderNumber;
fetchRequestOrp.predicate = predicateOrderNumber;
fetchRequestTender.predicate = predicateOrderNumber;
fetchRequestOh.resultType = NSDictionaryResultType;
fetchRequestOrp.resultType = NSDictionaryResultType;
fetchRequestTender.resultType = NSDictionaryResultType;
NSError *errorOh = nil;
NSMutableArray *orderHeads = [[context executeFetchRequest:fetchRequestOh error:&errorOh] mutableCopy];
NSError *errorOrp = nil;
NSArray *orderRows = [[context executeFetchRequest:fetchRequestOrp error:&errorOrp] mutableCopy];
NSError *errorTender = nil;
NSArray *tenderRows = [[context executeFetchRequest:fetchRequestTender error:&errorTender] mutableCopy];
if ([NWTillHelper isDebug] == 1) {
NSLog(#"WebServices:synchOrderWithStatusUpdate:orderHeadsArray: %#", [orderHeads objectAtIndex:0]);
NSLog(#"WebServices:synchOrderWithStatusUpdate:orderRowsArray: %#", orderRows);
NSLog(#"WebServices:synchOrderWithStatusUpdate:tenderRowsArray: %#", tenderRows);
}
// *** Set the status before upload since this dictates what will happen in backend
// *** regardless if synch is successful or not
NSManagedObject *orderHeadObject = nil;
orderHeadObject = [orderHeads objectAtIndex:0];
[orderHeadObject setValue:[NSNumber numberWithInt:status] forKey:#"status"];
// Save the objects to persistent store
NSError *error = Nil;
[context save:&error];
NSLog(#"Jongel Error = %#", error);
if(error !=nil) {
if([NWTillHelper isDebug] == 1) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
synchOrderErrorCode = 99;
}
return 10101;
The orderHeads are fetched as dictionaries. The NSManagedObjectContext will not be aware of any changes made to a dictionary. Only NSManagedObjects will be managed by the NSManagedObjectContext, hence the name ;-)
Try to fetch the orderHeadObject as an NSManagedObject as this will allow you to save the changes back into the store. If you need the dictionary for JSON serialisation: Just fetch it again as a dictionary after you have made the changes. That second fetch will be very fast since all values of the object are already cached, so CoreData won't have to reload from the database.

why get data from dataTaskWithURL:completionHandler: to late

i have some misunderstands with completion Handler in
- dataTaskWithURL:completionHandler:.
TableViewController.m
- (IBAction)search:(id)sender {
if ([self.textField.text isEqual: #""]) {
[self textFieldAnimation];
} else {
[self.dataWork takeAndParseDataFromFlickrApiWithTag:self.textField.text];
[self.itemStore fillItemsStore:self.dataWork];
[self.tableView reloadData];
}
}
when i call takeAndParseDataFromFlickrApiWithTag: i want to download some data from Flickr Api and then parse it and make array with JSON objects in dictionaries.
DataWorkWithFlickrApi.m
- (void)takeAndParseDataFromFlickrApiWithTag:(NSString *)tag {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSString *prerareStringForUrl = [NSString stringWithFormat:#"https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=%#&tags=%#&format=json&nojsoncallback=1", self.apiKey, tag];
self.url = [NSURL URLWithString:prerareStringForUrl];
self.session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
[[self.session dataTaskWithURL:self.url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSDictionary *photos = dict[#"photos"];
self.array = photos[#"photo"];
// NSLog(#"%#", self.array);
}] resume];
}
when this method is finished i go to the next [self.itemStore fillItemsStore:self.dataWork]; but at this moment in my array i have 0 objects, and then when i used second time - (IBAction)search: just then my table view showed me a list with objects and i have in array 100 objects and in that time there is uploading a new hundred objects.
So questions is why loading data so late? why I just don't get the data as a method takeAndParseDataFromFlickrApiWithTag: finishes? How to fix it?
sorry for my English
The whole point of a completion handler is that it is called when the task is completed.
You need to trigger processing of the next item in your completion handler and not when -takeAndParseDataFromFlickrApiWithTag: returns.

RESTKit DELETE request not deleting local object on 2xx success

According to the docs for 0.20 RK:
RKManagedObjectRequestOperation adds special behavior to DELETE requests. Upon retrieving a successful (2xx status code) response for a DELETE, the operation will invoke deleteObject: with the operations targetObject on the managed object context. This will delete the target object from the local store in conjunction the successfully deleted remote representation.
I have been trying to delete an object with such a request but no matter what I try I can't seem to get it to work. I successfully perform a request for many objects which get mapped to appropriate class, and get stored in core data. When I attempt a delete request on one of the objects and get a 200 success back, it does not deleted from local store.
Here's some code where I am no doubt missing a trick.
AppDelegate.m
...
//
// Match Mapping
//
RKEntityMapping *matchMapping = [RKEntityMapping mappingForEntityForName:NSStringFromClass([Match class])
inManagedObjectStore:objectManager.managedObjectStore];
NSDictionary *matchAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
#"objectId", #"id",
#"score", #"matchScore",
#"date", #"matchDate",
nil];
matchMapping.identificationAttributes = #[#"objectId"];
[matchMapping addAttributeMappingsFromDictionary:matchAttributes];
// Response descriptor for GET
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:matchMapping
method:RKRequestMethodGET
pathPattern:#"match/"
keyPath:#"matches"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
// Response Descriptor for PUT
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:matchMapping
method:RKRequestMethodPUT
pathPattern:#"match/"
keyPath:#"match"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
// Request Descriptor for DELETE
[objectManager addRequestDescriptor:[RKRequestDescriptor requestDescriptorWithMapping:[matchMapping inverseMapping]
objectClass:[Match class]
rootKeyPath:nil
method:RKRequestMethodDELETE]];
MatchDetailVC.m
...
- (void)deleteMatch {
NSDictionary *requiredParameters = #{
#"APIKey": #"xxxxx"
};
NSMutableURLRequest *request = [[RKObjectManager sharedManager] requestWithObject:self.match
method:RKRequestMethodDELETE
path:#"match/"
parameters:requiredParameters];
RKManagedObjectRequestOperation *operation = [[RKObjectManager sharedManager]
managedObjectRequestOperationWithRequest:request
managedObjectContext:[RKManagedObjectStore defaultStore].mainQueueManagedObjectContext
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
//[[RKManagedObjectStore defaultStore].mainQueueManagedObjectContext save:nil]; // IS THIS NEEDED?
NSLog(#"Successfully deleted match.");
[self.navigationController popToRootViewControllerAnimated:YES];
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", [error localizedDescription]);
}];
NSOperationQueue *operationQueue = [NSOperationQueue new];
[operationQueue addOperation:operation];
}
...
Thanks in advance and if you need more code, let me know.
Andy
I know this is quite an old post, but here is what I found out after searching for ages...
The local delete will fail if there is no valid response mapping for the DELETE response.
The problem wen't away for me when I created an empty response mapping like this:
RKObjectMapping* nullMapping = [RKObjectMapping mappingForClass:[NSNull class]];
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:nullMapping method:RKRequestMethodDELETE pathPattern:#"mybase/something/:myid/" keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];

NSIncrementalStore: Object's persistent store is not reachable from this NSManagedObjectContext's coordinator

I am trying to implement an NSIncrementalStore which will access a server over an RPC API. It needs to cache all the data locally, and so I have started by creating a NSIncrementalStore to use another core data stack which is used as the cache.
So first I set up the metadata and set up the core data stack that the cache will use. The cache SQL file is the URL passed in when the store is initialised:
- (BOOL)loadMetadata:(NSError *__autoreleasing *)error {
NSMutableDictionary *mutableMetadata = [NSMutableDictionary dictionary];
[mutableMetadata setValue:[[NSProcessInfo processInfo] globallyUniqueString] forKey:NSStoreUUIDKey];
[mutableMetadata setValue:RTkCachedAPIStoreType forKey:NSStoreTypeKey];
//[mutableMetadata setValue:NSStringFromClass([self class]) forKey:NSStoreTypeKey];
[self setMetadata:mutableMetadata];
// Set up the Cache Stack
NSManagedObjectModel *model = [self.persistentStoreCoordinator.managedObjectModel copy];
self.cacheModel = model;
self.cachePersistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.cacheModel];
self.cacheContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
NSURL *storeURL;
if ([self.URL isFileURL]) {
storeURL = self.URL;
}
NSError *storeAddError = nil;
if (![self.cachePersistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&storeAddError]) {
NSLog(#"Fatal error while creating persistent store: %#", storeAddError);
abort();
}
[self.cacheContext setPersistentStoreCoordinator:self.cachePersistentStoreCoordinator];
return YES;
}
The execute request for a simple request for an entity look like the following code. Essentially I'm just taking the request, wrapping it up as a FetchRequest and performing the request on the Cache Core data stack. I then create the objects using the returned objectID's on the calling context:
- (id)executeRequest:(NSPersistentStoreRequest *)request withContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error {
if ([request requestType] == NSFetchRequestType) {
NSFetchRequest *fetchRequest = (NSFetchRequest *)request;
NSEntityDescription *entity = [fetchRequest entity];
NSFetchRequest *cacheFetchRequest = [[NSFetchRequest alloc] init];
cacheFetchRequest.entity = entity;
__block NSArray *fetchedObjectFromCache;
[self.cacheContext performBlockAndWait:^(){
fetchedObjectFromCache = [self.cacheContext executeFetchRequest:cacheFetchRequest error:error];
}];
NSMutableArray *fetchedObjects = [NSMutableArray arrayWithCapacity:[fetchedObjectFromCache count]];
for (NSManagedObject *anObject in fetchedObjectFromCache) {
NSManagedObjectID *objectID = anObject.objectID;
NSManagedObject *managedObject = [context objectWithID:objectID];
[fetchedObjects addObject:managedObject];
}
return fetchedObjects;
}
}
When I try this out it throws an exception with the error
<unknown>:0: error: -[] : Object's persistent store is not reachable from this NSManagedObjectContext's coordinator
at the lineNSManagedObject *managedObject = [context objectWithID:objectID]; I can't work out why this is - this is the exact way that apple suggest this is done in the docs!
This error seems to be associated with accessing a context across threads - but I'm not doing that.
Any suggestions as to what is wrong here?
You're using the wrong objectID.
The objectID from your cache will not match the objectID of your persistent store because your incremental store does not have the same identifier.

Threads Using Managed Objects Between Contexts

I am at that point where I am losing hair on this so I figured I'd reach out to the great minds here who have had experience using Objective C with Threads and core data. I am having issues with managed objects inserted in on thread in a NSPrivateQueue Context being accessed from the main thread. So at a high level I am using AFNetworking to generate a thread to make requests to retrieve JSON data from a server and then insert the values into my persistent store core data. After this is done I have another thread for downloading some binary data using AFNetworking as well. I have set up 2 managed contexts for this as shown below:
(NSManagedObjectContext *)masterManagedContext {
if (_masterManagedContext != nil) {
return _masterManagedContext;
}
NSPersistentStoreCoordinator *coord = [self coordinator];
if (coord != nil) {
_masterManagedContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_masterManagedContext.stalenessInterval = 0.0;
[_masterManagedContext performBlockAndWait:^{
[_masterManagedContext setPersistentStoreCoordinator:coord];
}];
}
return _masterManagedContext;
}
// Return the NSManagedObjectContext to be used in the background during sync
- (NSManagedObjectContext *)backgroundManagedContext {
if (_backgroundManagedContext != nil) {
return _backgroundManagedContext;
}
NSManagedObjectContext *masterContext = [self masterManagedContext];
if (masterContext != nil) {
_backgroundManagedContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroundManagedContext.stalenessInterval = 0.0;
[_backgroundManagedContext performBlockAndWait:^{
[_backgroundManagedContext setParentContext:masterContext];
}];
}
return _backgroundManagedContext;
}
As is shown above I am using a child context and the parent context. When I make I call to fetch the json data I have something like below:
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
//Initially delete all records in table. This will change
[[Singleton sharedInstance]removeEntityObjects:className];
for (int x=0; x < [JSON count]; x++) {
NSMutableDictionary *curDict = [JSON objectAtIndex:x];
[[CoreDatam sharedinstance] insertEmployeesWithDictionary:curDict];
}else {
/* do nothing */
}
}
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error,id JSON) {
[delegate performSelector:#selector(didNotCompleteSync:) withObject:className];
}];
[operations addObject:operation];
}
[self.AFClient enqueueBatchOfHTTPRequestOperations:operations progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"Currenlty downloaded table data %d of %d!",numberOfCompletedOperations,totalNumberOfOperations);
} completionBlock:^(NSArray *operations) {
if (_syncInProgress) {
[[CoreDatam sharedInstance]updateEmpForId];
[self downloadAllFiles];
}
}];
}`
for the insert function I have something like below:
insertEmployeesWithDictionary:curDict {
[[self backgroundManagedContext]performBlockAndWait:^{
Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:#"Employee"
inManagedObjectContext:[self backgroundManagedContext]];
/* Issues saving null into core data based on type.*/
[emp setFirst:[dictCopy objectForKey:#"first"]];
[emp setLast:[dictCopy objectForKey:#"last"]];
NSError *error = nil;
BOOL saved;
saved = [[self backgroundManagedContext] save:&error];
if (!saved) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
[self saveMasterContext];
}];
}
The issue is below where I am trying to access the managed objects in the method that is in the completion block above:
updateEmpId {
[self.backgroundManagedContext performBlockAndWait:^{
NSError *error = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Employee"];
[request setSortDescriptors:[NSArray arrayWithObject:
[NSSortDescriptor sortDescriptorWithKey:#"last" ascending:YES]]];
myEmps = [self.backgroundManagedContext executeFetchRequest:request error:nil];
for (Employee *moEmp in myEmps) {
[[self backgroundManagedContext]refreshObject:moEmp mergeChanges:YES];
moEmp.photo = #'default.pic';
}
NSError *saveError = nil;
if (![self.backgroundManagedContext save:&saveError]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
[self saveMasterContext];
}
The issue is that I am getting very inconsistent behavior when looking at the managed objects that are modified in the main thread. Is it still necessary to pass managed objectIds when using a parent child context relation? if so how can I do so for the above example? Any help greatly appreciated.
You should pass NSManagedObjectIDs or re-fetch in the main thread context, yeah. If you pass object IDs, get the IDs from the background context after saving the new Employee objects, and use existingObjectWithID:error: in the parent context to instantiate them there. Or just re-do the fetch request from your updateEmpId code block in the masterManagedContext.