Question

I have a Core Data model with a Container and Item entities. A Container can have have zero or more Items in it. An Item must belong to at least one Container (but it can be in more than one.)

The relationships look like this:

Container:
  Relationship: items, Destination: Item, Inverse: itemContainers
  Optional, To-Many Relationship
  Delete Rule: Nullify

Item:
  Relationship: itemContainers, Destination: Container, Inverse: items
  Not-Optional, To-Many Relationship
  Delete Rule: Cascade

Problems arise when a Container is deleted. The Item objects in that container are updated, but if the item existed in only one container, the itemContainers property is a set with no objects. Saving the object graph fails because that empty set violates the Item's not-optional setting for itemContainers.

Of course, it's easy enough find the Item objects with empty itemContainers using an NSPredicate like "itemContainers.@count == 0", but it seems like there ought to be a way to configure the model to do this automatically.

So is there an easier/better way?

Was it helpful?

Solution

I don't think you can specify this behavior in your model, butI instead of making that fetch, you could validate the count of itemContainers in your Container's

 - (void)removeItemObject:(Item *)value
{...
if(![[value itemContainers]count])
  [context deleteObject:value];
...
}

OTHER TIPS

I tried Tony Arnold's answer above for a similar problem, but found issues when deleting several "Containers" at once (this is on OS X 10.8.2). Containers aren't removed from [item itemContainers] until the managed object context is saved, so count remains above 1 and item never gets deleted.

I came up with the following solution using -[NSManagedObject isDeleted] and category methods on NSManagedObject.

File NSManagedObject+RJSNondeletedObjects.h

#import <CoreData/CoreData.h>

@interface NSManagedObject (RJSNondeletedObjects)

- (NSSet *)RJS_nondeletedObjectsForToManyKeyPath:(NSString *)keyPath;
- (BOOL)RJS_hasOtherNondeletedObjectsForToManyKeyPath:(NSString *)keyPath;

@end

File NSManagedObject+RJSNondeletedObjects.m

#import "NSManagedObject+RJSNondeletedObjects.h"

@implementation NSManagedObject (RJSNondeletedObjects)

- (NSSet *)RJS_nondeletedObjectsForToManyKeyPath:(NSString *)keyPath
{
    NSSet * result = nil;

    id allObjectsForKeyPath = [self valueForKeyPath:keyPath];

    if ( ![allObjectsForKeyPath isKindOfClass:[NSSet class]] ) return result;

    result = [(NSSet *)allObjectsForKeyPath objectsPassingTest:^BOOL(id obj, BOOL *stop)
    {
        BOOL testResult = ![obj isDeleted];
        return testResult;
    }];

    return result;
}

- (BOOL)RJS_hasOtherNondeletedObjectsForToManyKeyPath:(NSString *)keyPath
{
    BOOL result = NO;

    // self will be in the set of nondeleted objects, assuming it's not deleted. So we need to adjust the test threshold accordingly.
    NSUInteger threshold = [self isDeleted] ? 0 : 1;
    NSSet * nondeletedObjects = [self RJS_nondeletedObjectsForToManyKeyPath:keyPath];
    result = ( [nondeletedObjects count] > threshold );

    return result;
}

@end

Container class

...
#import "NSManagedObject+RJSNondeletedObjects.h"
...
- (void)prepareForDeletion
{
    NSSet *childItems = [self items];

    for (Item *item in childItems) {
        if ([item RJS_hasOtherNondeletedObjectsForToManyKeyPath:@"containers"]) {
            continue;
        }

        [managedObjectContext deleteObject:item];
    }
}

I know it's not as clean as a configuration option offered by Core Data, but I've deployed a few projects where the Container object cycles through it's child Item entities when it is deleted, checking if they have 0 itemContainers (inside 'Container.m'):

- (void)prepareForDeletion
{
    NSSet *childItems = [self items];

    for (Item *item in childItems) {
        if ([[item itemContainers] count] > 1) {
            continue;
        }

        [managedObjectContext deleteObject:item];
    }
}

In my app, I make the item's containers relationship optional, and give access to those containerless items via a 'smart container'.

If you don't want that, I suspect you will just have to handle the save failure, and delete the violating objects.

More and more I am changing my approach to core data to a defensive one: assuming validation will fail, and being prepared to handle it. Becomes even more important when you integrate iCloud sync.

I like doing it this way:

- (void)didChangeValueForKey:(NSString *)inKey withSetMutation:(NSKeyValueSetMutationKind)inMutationKind usingObjects:(NSSet *)inObjects
{
    [super didChangeValueForKey:inKey withSetMutation:inMutationKind usingObjects:inObjects];

    if ([inKey isEqualToString:@"YOURRELATIONSHIP"] && self.YOURRELATIONSHIP.count == 0) {
        [self.managedObjectContext deleteObject:self];
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top