I'm using three-leveled multithreaded Core Data introduced by Marcus Zarra in his book. tempMOC for dealing with difficult tasks, mainMOC for UI management and writerMOC for writing data to persistant store.
I'm trying to integrate this model with my UITableView.
Everytime user pull-to-refresh I process downloading, parsing and loading this data. In this process there is one extra step - deleting previos entries for entit. I want this to be smooth so, the current UITableView (MOC as well) will wait to be cleaned until the last moment so this gap between deleting and loading new data wouldn't last for c.a 4 sec.
Here is my method which is called everytime I pull-to-refresh (I've removed parsing to keep the code cleaner):
- (void)loadTimetableToCoreData:(id)timetable
{
[self.pullToRefresh finishLoading];
// Initializing temporary context
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tempContext.parentContext = self.moc;
[tempContext performBlock:^{
// Parsing JSON data
}];
[self deleteAllObjects:#"Timetable"];
NSLog(#"Finished loading to temp MOC");
[tempContext performBlock:^{
// Saving procedure with multithreading
NSError *error;
if (![tempContext save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Finished saving to temp MOC");
[self.moc performBlock:^{
// Save groups to presistant store
NSError *error;
if (![self.moc save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Finished saving to main MOC");
[self.writer performBlock:^{
// Save groups to presistant store
NSError *error;
if (![self.writer save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
NSLog(#"Finished saving to writer MOC");
}];
}];
}];
}
I've also put some logs and if you run this code it's like:
2014-04-08 09:55:28.349 devPlan[21125:1803] Finished loading to temp MOC
2014-04-08 09:55:33.145 devPlan[21125:1803] Finished saving to temp MOC
2014-04-08 09:55:33.650 devPlan[21125:60b] Finished saving to main MOC
2014-04-08 09:55:33.652 devPlan[21125:60b] Finished saving to writer MOC
So as you can see there is this gap between loading and saving to temp MOC. It's okey because there is a lot of work going on but I would like to wait with [self deleteAllObjects:#"Timetable"]; until this work is done. When this is executed it wipes all data and when it's realoaded again it's displayed in UITableView - but still with time gap in which UITableView is empty...
What should I do to resolve thing thing? Below is the list of what I've tried so far:
Putting this delete method in various places across download method.
Messing around with MOCs performBlock: and performBlockAndWait:.
Fetching and initializing deleteMOC with NSFetchRequest, waiting for data being processed and later call delete method.
I must tell you, I'm stuck with this one... And it bothers me so much but I think there must be a logical explanation to this!
Edit
Here is the code of the method responsible for deleting:
- (void)deleteAllObjects:(NSString *)entityDescription
{
// Initializing temporary context
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tempContext.parentContext = self.moc;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:entityDescription inManagedObjectContext:tempContext];
[fetchRequest setEntity:entity];
NSError *error;
NSArray *items = [tempContext executeFetchRequest:fetchRequest error:&error];
[tempContext performBlockAndWait:^{
for (NSManagedObject *managedObject in items) {
[tempContext deleteObject:managedObject];
}
NSError *error;
if (![tempContext save:&error]) {
NSLog(#"Error deleting %# - error:%#", entityDescription, [error localizedDescription]);
}
}];
}
First the easy answer; I would put the -deleteAllObjects after the save of the tempContext.
But I would question the idea of deleting everything from the table. Could you not just remove the objects that need to be removed, add the objects that need to be added and update the ones that need to be updated? By capturing the notification from NSManagedObjectContextDidSaveNotification you can resolve that data and present (imho) the data more cleanly.
Update
I am not concerned with the parsing (insert, update, delete) part of it as who cares how long it takes. It is human perceivable no matter what so we are not going to do magic there. I am thinking more of the UX of the table view disappearing and then reappearing as opposed to individual cells updating and resorting themselves. I personally feel that is a far cleaner, slicker looking experience.
The doubling up sounds like there is something wonky in your NSFetchedResultsController delegate methods or in your -deleteAllObjects. Care to post that?
Update
Ok so you are literally deleting everything from that table. Is there a reason you are deleting vs. merging?
Now that I understand a bit more, I would do this order of events:
Process the data
Block the UITableView from updating with a -beginUpdates
Delete all data
Save both contexts
Unblock the UITableView with a -endUpdates
That should give you a more "instantaneous" refresh of the tableview.
I am not a fan of the delete/insert way of handling data but it may be appropriate in your case.
With a little help of my older code I've came onto this:
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:#"Timetable"];
[r setIncludesPendingChanges:NO];
NSArray *existingTimetables = [tempContext executeFetchRequest:r error:nil];
for (Timetable *table in existingTimetables) {
[tempContext deleteObject:table];
}
So I'm fetching Timetable and not messing it with pending changes inside tempContext. With this code everything works like it should but is blocking main thread! But I guess this qualifies to be another question.
Related
I have a relatively simple entity. When I create it, set its attributes, and save it, it saves successfully. I can later retrieve it and it is not nil and I get a successful save message from MagicalRecord.
When I retrieve it and try to access any attribute though the attribute is nil. The entity itself is fine but the attributes are all nil. I have checked they are all definitely set correctly before I save.
I haven't encountered this problem before. Why could it be occurring?
NB: This doesn't happen every time. Most times I call the method to create and save this entity it can later be retrieved without any issues. The problem is intermittent but possible to replicate on every run.
Code:
Entity1 *entity1 = [Entity1 MR_createEntityInContext:localContext];
[entity1 setUpEntity:myobject];
EntityChild *entityChild=[EntityChild MR_createEntityInContext:localContext];
[entityChild setUpEntityChild:entity.child withContext:localContext];
[entityChild setEntity1:entity1];
[localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
}];
Update:
If I look in the sqlite database and search for the entity it actually doesn't exist at all. So MagicalRecord tells me it saves, CoreData lets me retrieve a non-nil object (albeit with nil attributes) but no record exists in the database.
I did not understand ur code standards. As I am new to IOS Development. I Used below code for retrieving.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entityRef = [NSEntityDescription entityForName:#"Entity1" inManagedObjectContext:appDelegate.managedObjectContext];//localContext
[fetchRequest setEntity:entityRef];
NSError *error=nil;
NSArray *detailsArray = [appDelegate.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(#"Unable to execute fetch request.");
NSLog(#"%#, %#", error, error.localizedDescription);
}
Saving the data
NSManagedObjectContext *context = [appDelegate managedObjectContext];//localContext
NSManagedObject *objectRef = [NSEntityDescription
insertNewObjectForEntityForName:#"Entity1" inManagedObjectContext:context];
[objectRef setValue:#"IOS" forKey:#"Name"];
[objectRef setValue:#"positive" forKey:#"Attitude"];
NSError *error;
if (![context save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
Hope it helps you...!
Ok, I got to the bottom of this. It wasn't a problem with the code when I did the save. It was actually a problem with some code in another class that was retrieving the data from the wrong context. When I changed the context it worked correctly.
I'm still not sure why this only happened occasionally and not every time the code was run but it's working now.
Thanks for your help anyway everyone.
I have followed a variety of posts here in SO to delete all the data from an app so I can start over. I have tried:
A) Deleting all the data:
NSArray *entities = model.entities;
for (NSEntityDescription *entityDescription in entities) {
[self deleteAllObjectsWithEntityName:entityDescription.name
inContext:context];
}
if ([context save:&error]) {
...
- (void)deleteAllObjectsWithEntityName:(NSString *)entityName
inContext:(NSManagedObjectContext *)context
{
NSFetchRequest *fetchRequest =
[NSFetchRequest fetchRequestWithEntityName:entityName];
fetchRequest.includesPropertyValues = NO;
fetchRequest.includesSubentities = NO;
NSError *error;
NSArray *items = [context executeFetchRequest:fetchRequest error:&error];
for (NSManagedObject *managedObject in items) {
[context deleteObject:managedObject];
NSLog(#"Deleted %#", entityName);
}
}
B) Delete the physical data store:
NSError *error;
NSPersistentStore *store = [[self persistentStoreCoordinator].persistentStores lastObject];
NSURL *storeURL = store.URL;
NSPersistentStoreCoordinator *storeCoordinator = store.persistentStoreCoordinator;
[self.diskManagedObjectContext reset]; // there is a local instance variable for the disk managed context
[storeCoordinator removePersistentStore:store error:&error];
[[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error];
_diskManagedObjectContext = nil;
C) Perform step A and then step B
In all combinations it appears to run with no errors, but whenever I receive new data (via my HTTP service) and start adding it to the re-initialized data store I get all kinds of duplicate data and various data issues. I usually have to delete and reinstall the app to get the data clean enough to re-initialize.
It should be fairly straightforward. The user logs in. App data is downloaded and saved in the store. User logs out and logs in again or as different ID and new data is brought down.
Any ideas why the above methods are not working?
UPDATE:
I edited my code above to show that I am saving the context and removing the data store file. I still end up with bad leftover data. Could the problem be the multiple contexts we use? We have three contexts we use in the app: a UI-managed context, a background context and a disk-managed context. A notification listener takes care of merging changes in the background context with the disk managed context.
I have tried altering the above code to loop through the objects in all three contexts and we set them all to nil. The authentication code takes care of reinitializing the contexts. Still banging my head on what seems like a simple issue.
After
for (NSEntityDescription *entityDescription in entities) {
[self deleteAllObjectsWithEntityName:entityDescription.name
inContext:context];
}
Save your context
[context save:&error];
(B) doesn't delete the physical store, it just dissociates it from your app for the time being. No doubt you just attach it again shortly thereafter or upon next launch.
Use [[NSFileManager defaultManager] removeItemAtURL:... error:...] actually to delete the file from your disk.
As the other posters have said, you fail to NSManagedObjectContext -save: in (A) so you affect what's in that one context but not in the persistent store. Contexts are just in-memory scratch pads so as soon as you create a new context it'll be able to find everything in the persistent store again unless or until you save the one with the modifications.
I am trying to add objects to a Core Data Database using the following code:
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] init];
managedObjectContext.persistentStoreCoordinator = [[CoreDataController sharedCoreDataController] persistentStoreCoordinator];
Feed *feed = [NSEntityDescription insertNewObjectForEntityForName:#"Feed" inManagedObjectContext:managedObjectContext];
feed.feedID = dictionary[#"feed_id"];
feed.siteURL = dictionary[#"site_url"];
feed.title = dictionary[#"title"];
NSError *error;
if (![managedObjectContext save:&error]) {
NSLog(#"Error, couldn't save: %#", [error localizedDescription]);
}
Feed is a subclass of NSManagedObject. The persistentStoreCoordinator returned from the sharedCoreDataController (a singleton) is the persistentStoreCoordinator from a UIManagedDocument (created or opened when the app launches). As far as I can tell, the document is being created or opened successfully. I am running this code in the simulator, and I'm looking in the directory in which I am saving the Database (the apps Documents directory), but the persistentStore file is not being updated to reflect the new objects being added. Am I doing something wrong? I should also point out that the above code is being executed multiple times on a concurrent, asynchronous queue.
Any help would be much appreciated, thanks in advance.
Update: After the suggestions from Alexander and Duncan, the code above has been updated to reflect the changes. Sadly, however, I haven't noticed any difference (the new data is not appearing in the persistentStore file).
Have you called the line to save your managedObjectContext? How about this:
NSError *error;
if (![managedObjectContext save:&error]) {
NSLog(#"Error, couldn't save: %#", [error localizedDescription]);
}
Try using something like this
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] init];
managedObjectContext.persistentStoreCoordinator = [[CoreDataController sharedCoreDataController] persistentStoreCoordinator];
for (NSDictionary *dictionary in arrayOfData) {
Feed *feed = [NSEntityDescription insertNewObjectForEntityForName:#"Feed" inManagedObjectContext:managedObjectContext];
feed.feedID = dictionary[#"feed_id"];
feed.siteURL = dictionary[#"site_url"];
feed.title = dictionary[#"title"];
}
NSError *error;
if (![managedObjectContext save:&error]) {
NSLog(#"Error, couldn't save: %#", [error localizedDescription]);
}
I would avoid creating a managedObjectContext for each object you insert.
First, using UIManagedDocument is not recommended. It is not intended to be your single app wide context. It is meant for document style applications.
Second, the NSManagedObjectContext that is exposed from the UIManagedDocument doesn't have a NSPersistentStoreCoordinator attached to it. It is a child context of a private NSManagedObjectContext that then has a NSPersistentStoreCoordinator. I suspect that if you used the debugger you may find that your NSManagedObjectContext is missing its NSPersistentStoreCoordinator.
In any event, you are using UIManagedDocument and then trying to attach another NSManagedObjectContext to the same NSPersistentStoreCoordinator. That is a bad design and you should at a minimum remove the UIManagedDocument or stop creating a new NSManagedObjectContext.
What is the point of the new context? What are you trying to solve with this code?
I have a Core Data stack with a main managed object context with NSMainQueueConcurrencyType.
The user can initiate a task on a managed object that can take a long time, so it is performed on a separate context:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
Person *samePerson = (Person *)[context objectWithID:person.objectID];
[context performBlock:^{
// BLOCK 1
// do lots of work
// then update the managed object
samePerson.value = someCalculatedValue;
// save the private context
NSError *error;
if (![context save:&error]) {
NSLog(#"Error: %#", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(#"Error saving: %#", error);
}
}];
}];
This works fine, and the main MOC gets updated properly, NSFetchedResultsController hooked up to it perform properly, etc.
The problem is with deleting. I have this setup to delete objects:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
[context performBlock:^{
// BLOCK 2
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Person"];
NSError *error;
NSArray *all = [context executeFetchRequest:request error:&error];
if (!all) {
NSLog(#"Error fetching: %#", error);
} else {
for (NSManagedObject *person in all) {
[context deleteObject:person];
}
NSError *error;
if (![context save:&error]) {
NSLog(#"Error saving: %#", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(#"Error saving: %#", error);
}
}];
}
}];
Now if I do this delete operation (Block 2) during the time it takes to perform the long-duration task (Block 1), then the delete operation finishes quickly, and saves to main context. After a while Block 1 finishes, and when it saves the mainMOC at its end, we get a seemingly obvious crash:
CoreData could not fulfill a fault for ...
My question is: how do I perform a task such as Block 1 with the possibility of its object being deleted?
If these are both background tasks that cannot run at the same time, try using semaphores to protect access to them.
eg. for an instance variable:
dispatch_semaphore_t _backgroundProcessingSemaphore;
Lazily initialised using something like:
- (dispatch_semaphore_t)backgroundProcessingSemaphore
{
if (!_backgroundProcessingSemaphore) {
_backgroundProcessingSemaphore = dispatch_semaphore_create(1);
}
return _backgroundProcessingSemaphore;
}
Surround the critical code with:
dispatch_semaphore_wait(self.backgroundProcessingSemaphore, DISPATCH_TIME_FOREVER);
// Critical code
dispatch_semaphore_signal(self.backgroundProcessingSemaphore);
Only one critical section of code can then run at any point in time. The block that calls dispatch_semaphore_wait will block if the semaphore is already taken, until it is freed up.
You also probably want to think about splitting your long-duration task up so that it will run in discrete batches if you're not already doing so - this is useful if the long running background task timer is about to expire while you still have work to do - you can stop and restart from the appropriate point on next launch.
Other options would involve forcing a save on block 1 before block 2 saves itself, but this starts to get messy. Much easier to ensure the two competing blocks cannot overlap.
My app simply add some users informations (name, birthdate, thumbnail, ...) with Core Data.
I noticed that if I delete a user right after created it, my app just stop working (not a crash, xCode returns no crash log, nothing).
I'm using asynchronous nested context for saving my users informations so I guess that behavior is due to the fact that my delete statement is executing before my save statement.
But since i'm a total beginner with Core Data, i don't really know how to handle that. I don't even know if i declared nested contexts the right way.
Here's my save codes :
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tmpContext.parentContext = self.backgroundManagedObjectContext;
BSStudent *newStudent = (BSStudent *)[NSEntityDescription insertNewObjectForEntityForName:kBSStudent inManagedObjectContext:tmpContext];
newStudent.firstname = firstname;
newStudent.lastname = lastname;
newStudent.birthdate = birthdate;
newStudent.thumbnail = thumbnail;
newStudent.createdAt = [NSDate date];
[self dismissViewControllerAnimated:YES completion:nil];
[tmpContext performBlock:^{
[tmpContext save:nil];
[self.backgroundManagedObjectContext performBlock:^{
NSError *error;
if (![self.backgroundManagedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
[self.managedObjectContext performBlock:^{
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
}];
}];
}];
For precision, self.managedObjectContext is a NSPrivateQueueConcurrencyType and self.backgroundManagedObjectContext is a NSMainQueueConcurrencyType. And self.backgroundManagedObject is a child of self.managedObjectContext.
Here's my delete codes :
BSStudent *student = objc_getAssociatedObject(alertView, kDeleteStudentAlertAssociatedKey);
// on supprimer l'objet et on sauvegarde le contexte
[self.managedObjectContext deleteObject:student];
NSError *error;
if(![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
Can someone know how to handle this situation properly ?
Your delete is probably using the BSStudent created by a different context than you are deleting with. The following code will fix that.
NSManagedObjectContext * deleteContext = student.managedObjectContext;
[deleteContext deleteObject:student];
If you really want to use the other context, refetch the student using ObjectID
NSManagedObject * studentToDelete = [self.managedObjectContext objectWithID:student.objectID];
[self.managedObjectContext deleteObject:studentToDelete];
Nested contexts tips
Your contexts are probably okay, but I see a lot of people throwing around performBlock unnecessarily. With nested contexts, the QueueConcurrencyType refers to the thread it will do Core Data operations on, not the thread it was created on. So doing an operation like save on itself inside its performBlock is unnecessary and can lead to deadlocks.
When you save a child context, the parent is automatically synced with the changes. If you want to save upwards to the next higher parent automatically, I would recommend registering the parent for NSManagedObjectContextDidSaveNotification of the child saves. You can make this easier by having your AppDelegate have a factory method for creating the child contexts.
- (NSManagedObjectContext *)createChildContext
{
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tmpContext.parentContext = self.managedObjectContext;
//Register for NSManagedObjectContextDidSaveNotification
return tmpContext;
}
if you wrap your delete in a performBlock call it can't execute at the same time as the saving performBlock.
e.g.:
BSStudent *student = objc_getAssociatedObject(alertView, kDeleteStudentAlertAssociatedKey);
// on supprimer l'objet et on sauvegarde le contexte
[self.managedObjectContext performBlock:^{
[self.managedObjectContext deleteObject:student];
NSError *error;
if(![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
}];
This is the "preferred" way of dealing with contexts as it serializes access to the context and keeps all those operations on the contexts thread,
I assume you are getting the crash because the objectID is becoming invalid or changing before the save completes, near the top of the call stack you'll see something about "hash64" or such