NSBatchDeleteRequest does not delete relationship - ios

I have a problem with NSBatchDeleteRequest seems that is not possible to delete relationship references.
I have two entities:
News
Categories
where a category can have multiple news.
Now, when I try to delete all the objects in the core data using NSBatchDeleteRequest with the following code, then looking into the sqlite file seems that all categories are deleted, all news are deleted, but the relationship between categories and news persists, and this cause faults.
Here the delete function:
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName];
NSBatchDeleteRequest *delete = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fetchRequest];
[delete setResultType:NSBatchDeleteResultTypeCount];
NSError *error;
NSBatchDeleteResult *results = [deleteContext executeRequest:delete error:&error];
Any idea on how to fix this?

You can probably do [manageObjectContext reset];

Set shouldDeleteInaccessibleFaults: to YES and inaccessible/unfulfillable faults will be deleted. This solves the immediate problem.
The WWDC 2015 session What's New in Core Data talks about this a little bit. Both NSBatchDeleteRequest and NSBatchUpdateRequest modify the persistent store without the participation of the NSManagedObjectContext - which will result in the context's view of the data being inconsistent with the store.
The in-memory copy of the deleted object needs to be updated in the NSManagedObjectContext - have the batch delete request return the object IDs of the deleted objects and tell the NSManagedObjectContext to refresh those IDs.
This would look something like this:
[managedObjectContext performBlock:^{
NSBatchDeleteRequest batchDeleteRequest = [NSBatchDeleteRequest alloc] initWithFetchRequest:fetchRequest];
NSBatchDeleteResult result = nil;
result = [managedObjectContext executeRequest:batchDeleteRequest error:&error];
if ([[result result] count] > 0){
[managedObjectContext performBlock:^{
NSArray<NSManagedObjectID *> *objectIDs = (NSArray<NSManagedObjectID *>)[result result];
[objectIDs enumerateObjectsUsingBlock:^(NSManagedObjectID *objID, NSUInteger idx, BOOL *stop) {
NSError *error = nil;
NSManagedObject *obj = [managedObjectContext existingObjectWithID:objID error:&error];
if (![obj isFault] ) {
[managedObjectContext refreshObject:obj mergeChanges:YES];
}
}];
}];
}
}];
When the batch delete runs, relationships will be deleted or nullified, but a cascading set of delete rules may not be executed, and validation rules will not be executed - it is up to your application to ensure data integrity when using any of the batch change requests.
Your data model may require you to issue multiple delete requests to prevent related objects from being orphaned but still findable. For example, you may need a second batch delete to locate previously related entities that now have empty relationships. The predicate for such a request may look like:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"toMany.#count == 0"];
Or may use a subquery, etc.

I think the best solution might be to first delete the categories in the object graph, thus nullifying all relationships.
After that you could proceed with the NSBatchDeleteRequest for the news items.

Related

how to merge two different instances of NSManagedObjects in child contexts into single instance in parent context

I'm having a race condition problem in trying to create or update managed objects in child contexts of a common parent.
Assume the following setup:
a save context with NSPrivateQueueConcurrencyType. This is also the only context with a persistent store coordinator pointing to the backing SQL.
a main with NSMainQueueConcurrencyType
as many child contexts of the main context as needed, which are dynamically created as I fetch data from a server and use it to create or update objects in the graph.
Some model entity, say, User. Assume User has 2 attributes: id and name. Also, User has a to-many relationship friends.
The flow for retrieving a user along with their attributes and relationships would go something like this (I'm simplifying and using a bit of pseudo code cause the actual implementation is lengthy and complex, but this should get the point across):
You ask for user with id 1
I branch into two child contexts of the main context: one will fetch attributes and the other the relationship.
The context in charge of dealing with attributes does this:
[childContext1 performBlock:^{
// id jsonData = GET http://myserver.com/users/1
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"id = %i", [jsonData[#"id"] intValue]];
NSArray *results = [childContext1 executeFetchRequest:fetchRequest error:&error];
NSManagedObject *newOrExistingObject = nil;
BOOL existsInContext = (BOOL)[results count];
if (!existsInContext) {
newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext1];
}
else {
newOrExistingObject = results[0];
}
// at this point, the id and name of the user would be set.
[newOrExistingObject setValuesForKeysWithDictionary:jsonData];
[childContext1 save:nil];
[mainContext performBlock:^{
[mainContext obtainPermanentIDsForObjects:mainContext.insertedObjects.allObjects error:nil];
[mainContext save:nil];
[saveContext performBlock:^{
[self.saveContext save:nil];
}];
}];
}};
The context in charge of dealing with the relationship would do this:
[childContext2 performBlock:^{
// id jsonData = GET http://myserver.com/users/friends/
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"id = %i", [jsonData[#"id"] intValue]];
NSArray *results = [childContext2 executeFetchRequest:fetchRequest error:&error];
NSManagedObject *newOrExistingObject = nil;
BOOL existsInContext = (BOOL)[results count];
if (!existsInContext) {
newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext2];
}
else {
newOrExistingObject = results[0];
}
// at this point, the 'friends' relationship would be set
[newOrExistingObject setValuesForKeysWithDictionary:jsonData];
[childContext2 save:nil];
[mainContext performBlock:^{
[mainContext obtainPermanentIDsForObjects:mainContext.insertedObjects.allObjects error:nil];
[mainContext save:nil];
[saveContext performBlock:^{
[self.saveContext save:nil];
}];
}];
}};
The problem with the above approach is that in order to "create or update", I need to figure out if the object in question already exists (which I attempt to do so by doing a fetch by id since this is the attribute I base equivalence on).
As it stands, sometimes one of the child contexts will be done doing its thing and by the time the other context attempts fetching an object with the id in question, it'll get the object previously created by the other context, update it and everything will go fine. But sometimes, whichever child context executes second does not find the object previously created by the child who executed first. So it creates another new object with the same id. By the time the whole thing is done and both children propagated their objects to the main context and the main context in turn to the save context, I end up with two objects with different objectID but same id: one will have the attributes and the other the relationship. My guess is this is because by the time the second context tries to fetch the object, either:
A: the child context which created the object is not done propagating it back to the main context by the time the other child context attempts to retrieve it.
B: the main context is not done assigning permanent IDs to the objects it just got from the first child context.
Basically what I need is a way to either synchronize each child context's fetch, so that this cannot happen until whichever child executes first and creates the object has saved and in turn the object has been propagated to the main context. Or, perhaps better yet, a way for the main context to know that any number of objects of the same entity class and with the same id attribute value are equivalent (despite their objectIDs being different since they were created in different contexts in parallel) and so should be merged into a single one.
Any thoughts?
dispatch_semaphore_t
:p
I wasn't aware that I could use GCD semaphores inside an NSManagedObjectContext's -performBlock.
I ended up creating a mutex (a binary semaphore), so that the child blocks do:
[childContext performBlock:^{
// id jsonData = GET http://myserver.com/<whateverData>
dispatch_semaphore_wait(_s, DISPATCH_TIME_FOREVER);
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"id = %i", [jsonData[#"id"] intValue]];
NSArray *results = [childContext2 executeFetchRequest:fetchRequest error:&error];
NSManagedObject *newOrExistingObject = nil;
BOOL existsInContext = (BOOL)[results count];
if (!existsInContext) {
newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext];
}
else {
newOrExistingObject = results[0];
}
[newOrExistingObject setValuesForKeysWithDictionary:jsonData];
[childContext obtainPermanentIDsForObjects:childContext.insertedObjects.allObjects error:nil];
[childContext save:nil];
dispatch_semaphore_signal(_s);
[mainContext performBlock:^{
[mainContext save:nil];
[saveContext performBlock:^{
[self.saveContext save:nil];
}];
}];
}};
And this did the trick

Does Core Data persist the order of objects in NSMutableArray?

I basically operate on objects in a NSMutableArray, which is fetched from Core Data. All changes to those objects save in Core Data with the exception of the order of objects in the array. When, for instance, an object in the array is moved to the end of it, that does not seem to save in Core Data. Is it the case that Core Data does not persist order of objects in arrays? If so, what can be done to work around it? Sorting?
This is how I fetch data:
NSError *error;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Item" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
self.items = [[managedObjectContext executeFetchRequest:fetchRequest error:&error] mutableCopy];
And this is how I save changes:
- (void)saveCoreData{
NSManagedObjectContext *context = [self managedObjectContext];
NSError *error;
if (![context save:&error]) {
NSLog(#"Could not save data: %#", [error localizedDescription]);
}
}
If you're saving the array in a transform able property on an entity then the order will be saved - if you set the array back onto the managed object instance.
If the array is a fetched list of managed object instances then the order of that array means nothing and won't be saved. If you want to save this order then you need to add (and update) some other data in the context. This could be an order attribute on the entity or another entity with an ordered relationship perhaps.
If you're not using ordered relationships, then there is no guarantee of the order.
You can either set your relationships to be ordered. In this case you will have to deal with NSOrderedSet and different accessory methods. This feature is available in iOS5 and later.
Here is a great article of Ash Furrow (great developer, had a privilege to meet him) that covers ordered relationships in Core Data.
On the other hand, you can order your data once you access it. In my case I had an NSArray property that, once accessed, would get all objects in NSSet and order them. The disadvantage of this approach is every time you add new NSManagedObject to a relationship, mentioned NSArray will become outdated and must be recreated.

core data do all relation objects get deleted too when deleting an entity's objects

In core data do all relation objects get deleted too when deleting an entity's objects
there is an another entity Details which has one to many relationship. Do I need to delete its objects or the following code takes care of it?
my code so far:
NSFetchRequest * allClients = [[NSFetchRequest alloc] init];
[allClients setEntity:[NSEntityDescription entityForName:#"Client" inManagedObjectContext:[NSManagedObjectContext defaultContext]]];
[allClients setIncludesPropertyValues:NO]; //only fetch the managedObjectID
NSError * error = nil;
NSArray * clients = [[NSManagedObjectContext defaultContext] executeFetchRequest:allClients error:&error];
//error handling goes here
for (NSManagedObject * client in clients) {
[[NSManagedObjectContext defaultContext] deleteObject:client];
}
NSError *saveError = nil;
[[NSManagedObjectContext defaultContext] save:&saveError];
//more error handling here
That depends on what settings you have configured. For each relationship you can set it to:
Do nothing
Cascade (delete the other item too)
Nullify
Deny (don't allow the deletion)
You specify this for each relationship in the model (and both ends of each relationship).

Any way to optimize simple NSFetchRequest for single object?

I'm dong data processing in a child moc in a background queue. I need to query the database by ID so that I can differentiate updating-existing-object from creating-new-object. I found most of the time(the total processing time is about 2s for 50 items) is consumed by executeFetchRequest:error:. The NSPredicate is of the simplest form — only to match a single ID attribute(ID attribute is already indexed), and the NSFetchRequest should return one or none(ID is unique). Is there any way to optimize this kind of NSFetchRequest?
Here is my current code:
+ (User *)userWithID:(NSNumber *)ID inManagedObjectContext:(NSManagedObjectContext *)context {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"User"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ID == %#", ID];
[fetchRequest setPredicate:predicate];
[fetchRequest setFetchBatchSize:1];
NSError *error = nil;
NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
if (error) {
abort();
}
if ([users count] == 1) {
return [users objectAtIndex:0];
} else if ([users count] > 1) {
// Sanity check.
…
} else {
return nil;
}
}
As #ChrisH pointed out in comments under the question, doing a fetch for every ID is no good. So I changed my processing flow to this:
Enumerate data the first time to extract IDs.
Do a single fetch to fetch all existing users matching IDs and put them in a dictionary keyed by ID(named as existingUsers).
Enumerate data the second time to do the real processing: in each iteration, either update one existing user found in existingUsers or create a new user, add it into existingUsers if it is new.
The code is almost doubled, but so is the performance. Really good tradeoff!
To expand on my comment to the original question, it's not efficient to repeatedly perform fetch requests with Core Data when importing data.
The simplest approach, as #an0 indicated, is to perform one fetch of all the existing objects you will be checking against, and then constructing an NSDictionary containing the objects with the attribute you will be checking as keys. So sticking with the original User and userID example:
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"User"];
NSError *error = nil;
NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
if (error) {
//handle appropriately
}
NSMutableDictionary *userToIdMap = [NSMutableDictionary dictionary];
for (User *user in users){
[userToIdMap setObject:user forKey:user.ID];
}
Now in your method that processes new data you can check the userToIdMap dictionary instead of making fetch requests.
A more sophisticated approach, suited to larger data sets, is outlined in the Core Data Programming Guide's Efficently Importing Data. Take a look at the section called 'Implementing Find-Or-Create Efficiently'. The approach suggested by Apple here misses out some code regarding how to walk the arrays you create, and my solution to that problem is in this SO question: Basic array comparison algorithm

Checking for duplicates when importing to CoreData

I'm importing data into a Core Data store using RestKit and need to check for duplicates. If the item is already in the store, I'd like to update it with the latest attributes. If it's a new item, I'd like to create it.
The import was slow so I used Instruments and saw that the longest part of importing was checking to see if the item already exists (with a fetch request)
So I'd like to know if checking to see if the item is already in the store, is it faster to:
use countForFetchRequest to see if the item already exists, then executeFetchRequest to return the item to update or
just executeFetchRequest to get the item to update
or is there a better way to do this?
I thought countForFetchRequest would be faster since the entire NSManagedObject isn't returned and only execute the fetch request if I know there's going to be a NSManagedObject.
Thanks
- (Product *)productWithId:(int)productID {
NSManagedObjectContext *context = [Model sharedInstance].managedObjectContext;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"product_id == %d", productID];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Product" inManagedObjectContext:context];
request.predicate = predicate;
request.fetchLimit = 1;
NSError *error = nil;
NSUInteger count = [context countForFetchRequest:request error:&error];
if (!error && count == 1) {
NSArray *results = [context executeFetchRequest:request error:&error];
if (!error && [results count]) {
return [results objectAtIndex:0];
}
return nil;
}
return nil;
}
As far I know, the best way to find and/or import objects within Core Data is described in Implementing Find-or-Create Efficiently.
The documentation describes a find or create pattern that it's based on sorting data: the data you download from the service and the data you grab form the store.
I really suggest you to read the link I provided. You will see a speed up on your performances.
Obviously you should do the work in background, preventing the main thread to freeze, using thread confinement or new iOS Core Data queue API.
Hope that helps.

Resources