28
votes

Say I have a custom NSManagedObject Department and this has a property representing a to-many relationship to employees i.e. NSSet *employees;.

For a given Department, I want to remove all objects in employees. What is the recommended/best way to do this, please?

So, hypothetically, my code would look like this:

Department.h

@interface Department: NSManagedObject {
}
@property (retain) NSString *departmentName;
@property (retain) NSSet *employees;
@end

Department.m

@implementation Department
@dynamic departmentName;
@dynamic employees;

Employee.h

@interface Employee: NSManagedObject {
}
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (retain) Department *worksIn;
@end

doCoreDataStuff

- (void)doCoreDataStuff:sender {
    //add a department, give it a couple employees, then try to remove those employees
    NSEntityDescription *deptEntity = [NSEntityDescription entityForName:@"Department"
                                                 inManagedObjectContext:self.managedObjectContext];
    Department *dept = [Department alloc] initWithEntity:deptEntity
                          insertIntoManagedObjectContext:self.managedObjectContext];
    NSError *error;

    dept.departmentName = @"Accounting";
    //save more often than normal to see more easily what causes error
    if (![self.managedObjectContext save:&error]) NSLog(@"\nError: %@", [error localizedDescription]);

    NSEntityDescription *empEntity = [NSEntityDescription entityForName:@"Employee"
                                                 inManagedObjectContext:self.managedObjectContext];
    emp.firstName = @"Steve";
    emp.lastName = @"Smith";
    emp.worksIn = dept;

    if (![self.managedObjectContext save:&error]) NSLog(@"\nError: %@", [error localizedDescription]);

    emp = [[Employee alloc] initWithEntity:empEntity
            insertIntoManagedObjectContext:self.managedObjectContext];
    emp.firstName = @"Natasha";
    emp.lastName = @"Johnson";
    emp.worksIn = dept;

    if (![self.managedObjectContext save:&error]) NSLog(@"\nError: %@", [error localizedDescription]);

    //all good so far! now will try to delete all employees for this department
    dept.employees = [NSSet set];
    if (![self.managedObjectContext save:&error]) NSLog(@"\nError: %@", [error localizedDescription]); //"Multiple validation errors occurred."

    //this also produces the same error
    [[dept mutableSetValueForKey:@"employees"] removeAllObjects];
    if (![self.managedObjectContext save:&error]) NSLog(@"\nError: %@", [error localizedDescription]); //"Multiple validation errors occurred."

The relationship employees is not optional so I'm guessing that removing employees from the department means that I am trying to "orphan" the employees i.e. keep employees in the persisted model without an associated department.

So, I think my original question should be reworded to: what is best/recommended way to remove all "child" objects of a "parent" when the children have a non-optional relationship with the parent?

I suspect that the answer is going to be "loop through and delete the employee objects one at a time".

UPDATE

According to an answer and a link to Apple's documentation, I should be able to set the delete rule to "Cascade" and then code like department.employees = [NSSet set]; will work. However, this does not work in my very simple project where I have set the delete rule accordingly.

Thanks

3
i wonder if you have found a solution to that problem? i'm having the same problem.. to stick to your example i have a synchronization process where i'm trying to delete all employees and afterwards create them from scratch (to not make my synchronization too complicated).. but currently the only way i got that to work was to remove the 'to many' relation ship for both sides - iterate employees calling [managedObjectContext deleteObject:employee] and afterwards doing a [[department mutableSetValueForKey:@"employees"] removeAllObjects]; which just seems wrong, but works.. - Herbert Poul
I never got the example above to work. In the end, I iterated through employees and deleted them, as in the response below I've just accepted as the answer - stifin
You misunderstand the documentation. When you DELETE the department object, then the delete rule will apply to employee objects in the employees set. Changing the relationship won't delete any objects. - Eric

3 Answers

20
votes

If you want to delete the employee elements for a specific department, then you could run a for-in loop like for

for (Employees * theEmployee in department.employees) {
  [self.managedObjectContext deleteObject:[self.managedObjectContext objectWithID:theEmployee.objectID]]; 
}

Then save your managed context. IF of course that's what you want, and not remove the relationship between employees and departement; in that case, assigning an empty set would work.

Variation on above:

for (Employee *employeeToDelete in department.employees) {
    [self.managedObjectContext deleteObject:employeeToDelete];
}
19
votes

Setting the department's employee relationship to an empty set WILL NOT delete the employees, regardless of the deletion rule. I believe you are misunderstanding the deletion rule. According the apple docs: "A relationship's delete rule specifies what should happen if an attempt is made to delete the source object". Thus, cascading will only take effect if we DELETE the department. By setting the relationship to an empty set, all we are doing is separating the employees from the department, not deleting them. And if that relationship is not set to optional for the employees, this will cause an error when saving. If you want to delete the employees from the department, you can iterate through them as listed above, or set the department relationship to cascade and then delete the department.

1
votes

I also had something like below but it didn't work...

- (BOOL)deleteCollection:(Collection *)collection {
    // Grab the context
    NSManagedObjectContext *context = [self managedObjectContext];
    NSError *error = nil;

    [collection removeSounds:collection.sounds];
    [context deleteObject:collection];

    // Save everything
    if ([context save:&error]) {
        return YES;
    }
    return NO;
}

Obviously the database layer can't delete the sounds and then the collection at once. Setting the delete rule on the relationship to 'cascade' nicely solved my problem and let me use just:

[context deleteObject:collection];

If you want to save some people detailed reading time then just mark this as the answer.

As Fervus stated above, this link may also be helpful for the pros: Propagate deletes immediately in Core Data