What are the best practices on where to update my Core Data?
The first guy who worked on this project I'm working right now created all the Core Data related functions inside the ViewController, but I wanted to declare them inside the model classes (NSManagedObject subclass) to separate concerns.
The main function is a AFNetworking postPath that calls a web service and returns an array of objects to add/edit/delete. What I did was create a class method and do this AFNetwork call inside it:
+ (void)updateEbooksListWithSuccessBlock:(void (^)())successBlock andFailureBlock:(void (^)())failureBlock {
NSURL *url = urlSchema (urlWebServices, #"");
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSString *postPath = [NSString stringWithFormat:#"ws-ebooks-lista.php"];
[httpClient postPath:postPath parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([operation isKindOfClass:[AFHTTPRequestOperation class]]) {
NSDictionary *result = [[responseObject objectFromJSONData] retain];
bool success = statusDoRetornoDoWebService(result); //Function that checks if the return was successful
//Configura o Core Data
NSError *error = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSManagedObjectContext *localManagedObjectContext = [[NSManagedObjectContext alloc] init];
[localManagedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Ebooks" inManagedObjectContext:localManagedObjectContext];
NSPredicate *filterPredicate;
[request setEntity:entity];
if (success) {
NSArray *ebookInfos = [result objectForKey:#"saida"];
Ebooks *ebook;
NSManagedObject *objectInsert;
for (NSDictionary* ebookInfo in ebookInfos) {
filterPredicate = [NSPredicate predicateWithFormat:#"ebooks_id == %#",[ebookInfo valueForKey:#"id_ebook"]];
[request setPredicate:filterPredicate];
ebook = [[localManagedObjectContext executeFetchRequest:request error:&error] lastObject];
objectInsert = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:localManagedObjectContext];
if (ebook) {
if (![[ebookInfo valueForKey:#"excluido"] isEmpty]) {
//Delete Ebook
} else {
//Update Ebook
}
} else {
//Add Ebook
}
if (![localManagedObjectContext save:&error]) {
//Log Error
}
[objectInsert release];
}
}
[request release];
[localManagedObjectContext release];
}
[successBlock invoke];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//Failure
[failureBlock invoke];
}];}
And it works fine while the app is running, but if I close it (through Xcode) and open it again, the changes aren't saved. I tried not using the "parent context" way and just using the AppDelegate managed object context (since AFNetworking callbacks always runs on the main queue) but no success: the data is not persisted. Why is that? Am I doing something wrong? Is it bad practice? Should I leave everything in the View Controller the way it was?
Thanks!
I think it is a bad idea to have too much logic that ultimately relates to your data model into your entity classes. These tasks simply do not belong there. The entity classes should focus only on what they encapsulate: the entity instances themselves.
To illustrate: think of a class that represents a number (like NSNumber). It think it is not convenient to extend it to give you, say, an array of all even numbers within a certain limits, or the nth member of the Fibonacci series. It seems unsound to have a number class be responsible for saving itself to a file, or retrieving information from the web.
For these and similar reasons, I believe the fetching and saving of Core Data entities belongs into controllers, not entity classes. Remember, one of the basic ideas behind the MVC (model-view-controller) pattern is that the controller manipulates the model or asks it for information, not that the model manipulates itself.
I speculate that your troubles are derived mainly from not separating the various functional aspects of your application sufficiently (data model, persistence, network operations, user interactions).
ugh... what I would do is make very naked NSManagedObject subclasses... then extend them with categories, that way when you regenerate your classes from the updated model you don't have to try to merge in all of your custom logic.
also the custom logic belongs in the model, the model contains the category or class extension.
so take that crap out of the View Controllers and put it in an easily maintainable category or several categories if it is warranted.
Related
I'm making some app and I want to provide offline functionality to it.
Problem is with getting new data from backend as temporary objects not saved in persistent store. Why I want this? Because I want to check whether data from backend is newer than offline one (by update date) If yes then update, otherwise, send it to the backend.
For now I'm trying something like this:
NSMutableURLRequest *apiEmailRequest = [[RKObjectManager sharedManager] requestWithObject:#"ApiEmail" method:RKRequestMethodGET path:pathToContent parameters:nil];
RKObjectRequestOperation *apiEmailOperation = [[RKObjectManager sharedManager] managedObjectRequestOperationWithRequest:apiEmailRequest managedObjectContext:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
*********************CHECK FOR BACKEND EMAILS AND OFFLINE ONE **********************
NSArray *backendEmails = [mappingResult array];
for (ApiEmail *backendEmail in backendEmails) {
if ([backendEmail isKindOfClass:[ApiEmail class]]) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"ApiEmail"];
NSPredicate *filterByApplication = [NSPredicate
predicateWithFormat:#"emailId == %#", backendEmail.emailId];
[fetchRequest setPredicate:filterByApplication];
NSArray *persistentEmails = [[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil];
*HERE PUT IT INTO mainQueueManagedObjectContext and
saveToPersistentStore else POST it to the backend*
}
}
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
*ERROR*
}];
return apiEmailOperation;
[[RKObjectManager sharedManager] enqueueObjectRequestOperation:apiEmailOperation];
Is there any way to do it without creating new RKObjectManager?
Best regards, Adrian.
UPDATE
-(void)willSave {
[super willSave];
NSDictionary *remoteCommits = [NSDictionary dictionaryWithDictionary:[self committedValuesForKeys:#[#"updateDate"]]];
NSDate *updateDate = [remoteCommits valueForKey:#"updateDate"];
NSComparisonResult result = [self.updateDate compare:updateDate];
if(result == NSOrderedDescending) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
} else {
[self.managedObjectContext refreshObject:self mergeChanges:YES];
}
}
After such modification I'm getting Failed to process pending changes before save. The context is still dirty after 1000 attempts.
The below is unlikely to work in your situation actually, specifically because of the way discardsInvalidObjectsOnInsert works.
You may be able to do this by following the below process but additionally implementing willSave on the managed object and checking the status there. If you decide to abandon the updates you can try using refreshObject:mergeChanges: with a merge flag of NO.
With KVC validation you have 2 options:
edit individual pieces of data as it is imported
abandon the import for a whole object
Option 2. requires that you use the Core Data validation to prevent the import. That means doing something like making the date stamp on the object non-optional (i.e. required) and in your KVC validation setting it to nil when you determine that the import should be aborted.
Note that for this to work you need to set discardsInvalidObjectsOnInsert on the entity mapping.
After big help from #Wain, I finally got it working. Without this brave men I would still be in the sandbox. Solution:
-(BOOL)validateUpdateDate:(id *)ioValue error:(NSError **)outError {
NSComparisonResult result = [self.updateDate compare:(NSDate *)*ioValue];
if (result == NSOrderedDescending) {
self.updateDate = nil;
return NO;
}
return YES;
}
-(void)willSave {
[super willSave];
if (self.updateDate == nil) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
}
}
Thank You so much for your time and help.
Best regards, Adrian.
I am using RestKit 0.2.x with Core Data and following the standard tutorials, ie:
Create Core Data model and use mogenerator to make code
Instantiate object manager with base URL
Create managed object context and persistent store
Create entity mappings for all entities returned by my web service
Create response descriptors for all web service endpoints and entities
Add response descriptor to object manager
Everything seems to be "working" just fine ... I can call
[[RKObjectManager sharedManager] getObjectsAtPath:_requestPath parameters:_requestParameters success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self requestSuccess];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
[self requestError:error];
}];
... all day long, and I then I keep handling with (as shown in the tutorials)
- (void)requestSuccess {
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:_entityName];
fetchRequest.sortDescriptors = #[_defaultSortDescriptor];
NSError *error = nil;
requestData = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
[_delegate handleRequestSuccess:self withData:requestData];
//[self cleanupRequestBeforeSuccessWithData:requestData];
[self completeRequest];
}
Now the problem is that at least by default, RestKit+CoreData actually persists your GET'ted objects to its own persistence store, or something like that. I'll explain the "cleanupRequest..." in a moment.
That kind of defeats the purpose of trying to allow the users to specify parameters at the level of the web service client, because all of the objects seem to end up in the same place anyway.
For instance, let's say I have a method /api/endpoint?queryString and I call it with two different sets of parameters:
[[RKObjectManager sharedManager] getObjectsAtPath:#"/api/endpoint" parameters:PARAMS_ONE success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self requestSuccess];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
[self requestError:error];
}];
[[RKObjectManager sharedManager] getObjectsAtPath:#"/api/endpoint" parameters:PARAMS_TWO success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self requestSuccess];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
[self requestError:error];
}];
If I then blindly follow the tutorials about how to retrieve my objects, my callbacks are then identical!
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"EndpointDataTransferObject"];
fetchRequest.sortDescriptors = #["endpointDataTransferObjectID"];
NSError *error = nil;
requestData = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
The result, of course, is that the my delegate gets sent (pseudocode) requestData WHERE PARAMS_ONE on the first call, and then requestData WHERE PARAMS_ONE UNION requestData WHERE PARAMS_TWO on the second call.
Now all I really want is to be able to conduct the NSFetchRequest on only those items mapped from the web service. I think this is a totally reasonable expectation, so clearly I am missing something because whoever wrote this library is much more clever than I.
For instance, if I could somehow get an NSArray of all the objects from the two parameters it provides in the success block (RKRequestRequestOperation *o, RKMappingResult *m) - and if I can, please tell me how!!! - then my problem would be solved, and I could enjoy the caching without having to worry about whether my filters are being ignored.
What I do not want to do, however, is this:
Call getObjectsAtPath: parameters: success: failure: with parameters and/or path representing a sort of "server-side" predicate
On success, create a NSFetchRequest and a client-side predicate that mirrors my server-side predicate
This approach seems really really dumb, and yet, I don't know any better. But I refuse to do that. It is error-prone, redundant, and potentially resource-intensive.
So instead, I've opted to add a little method cleanupRequestBeforeSuccessWithData at the end of my success callback before calling completion:
- (void)cleanupRequestBeforeSuccessWithData:(NSArray *)managedObjects {
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
for (NSManagedObject *o in managedObjects) {
[managedObjectContext deleteObject:o];
}
NSError *error = nil;
[managedObjectContext save:&error];
}
This is ugly but it sure gets the job done. Now it totally empties the cache, but I'd rather have to make requests over and over again than to form "server-side" predicates with URL's and then form client-side NSPredicates.
What am I missing about how this is supposed to work? Clearly, I'm missing something big.
If I then blindly follow the tutorials
Never a good idea, you need to take what you've learnt from the tutorials and apply it to your problem space.
The items are indeed persisted as you're using Core Data. You don't need to but it does help with memory management. Technically you don't need to run a fetch because the mapping result (RKMappingResult) contains all the mapped objects, so you can just extract them and pass them on.
The other approach you shun of running a local fetch with filters is actually perfectly acceptable, and I'd say it's the usual approach as it's how a fetched results controller based approach works... To facilitate that you would add the query parameters to your mapping so that the mapped objects are updated. You do need to be cautious though as multiple requests returning the same objects could overwrite the data (assuming you're using unique identifiers).
My question is similar to the following one but has differences
I use Core Data. The problem is if I use the current NSManagedObjectContext then I may get an incorrect result. For example:
From the beginning the db is filled.
My app deletes all the entities via the current context.
I get check if db filled and get NO but in reality the db is still filled because the context should be saved to apply the changes.
And example of my solution:
- (BOOL)isDBFilled {
//separate context to
NSManagedObjectContext *context = [[[NSManagedObjectContext alloc] init] autorelease];
[context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[context setPersistentStoreCoordinator:coordinator];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:... inManagedObjectContext:context];
[request setEntity:entity];
[request setFetchLimit:1];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
if (!results) {
LOG(#"Fetch error: %#", error);
abort();
}
if ([results count] == 0) {
return NO;
}
return YES;
}
So is it normal and safe to create another context just to check if db is filled? Or Is there better ways to perform this checking except of creating a separate BOOL variable which will be changed before db changes and after them?
It is fine to use a separate context to check if the database is populated or not. The overhead is minimal, so I would have no objections.
Please note that Apple does not necessarily agree. From the "Core Data Snippets":
Neither should a view controller create a context for its own use (unless it’s a nested context).
That being said, I do not see the need for this. If you delete all entities, save and then check should get an empty database. If you delete all entities and do not save and then check, you should get a non-empty database. You can easily - and more easily - do this with one context.
PS: autorelease seems to indicate that you are using a very old SDK. How about upgrading?
I have to fetch data from multiple entities which are associated to different screens of an iPhone App, now the scenario is as follows, when user press a Sync button i will have to fetch data from all these entities (i have about 12 entities) and send all this data to a server via a web service, and all these entities have no relationship among them, now my question is what is the best approach to do this task, should i write 12 different fetch requests in a single method, or is there some other better approach, if any one can guide with some tutorial link, that will me most appreciated, thanx in advance.
You can use a for loop to accomplish your task like this. I've used this code to delete all entries of the Database.
NSArray *allEntities = [[[[UIApplication sharedApplication] delegate] managedObjectModel] entities];
NSError *error;
for (NSEntityDescription *entityDescription in allEntities)
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entityDescription];
fetchRequest.includesPropertyValues = NO;
fetchRequest.includesSubentities = NO;
NSArray *items = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(#"Error requesting items from Core Data: %#", [error localizedDescription]);
}
//Do whatever you need to do here
}
I have a core data setup that has 2 database entities. For the sake of names I'll call them Primary and Secondary. Secondary only belong to one Primary (relationship is setup). In my main view that lists the Primary objects in a table I retrieved them and put them in an PriObject Class which stores it's properties (including the managed object ID). The PriObject is then added to a mutable array (priArray), which is then used to fill the table with the data. All works ok so far. When I then click on the row I can log the PriObject.moID.
I can't figure out how to lookup that object in the database so I can then add Secondary objects to it. I can't do it by name because some Primary might have the same name.
I need to work out how to get an object back from either the URI or the ID. I have the ID so I can generate the URI if I need to.
Can't get my head around it at all and any examples I have found while looking don't cover what I need. What options are there?
EDIT: I am currently getting all my objects with the following.
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:#"Primary"
inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
NSError *error;
NSArray *objects = [context executeFetchRequest:request
error:&error];
if ([objects count] == 0) {
NSLog(#"Nothing found");
} else {
NSLog(#"Something found");
}
How can I change this for just the one using:
ObjectWithID:
Call the objectWithID: method on your NSManagedObjectContext instance to retrieve the instance.
As an aside, it seems like you are making things harder on yourself with this PriObject class, it seems to be a wrapper around your NSManagedObject instances, is that right? I'd just use NSManagedObject subclasses directly, personally.