ndg
ndg

Reputation: 2645

Handling deletion of Core Data objects in to-many relationships

Using Core Data, I'm looking to setup a tagging system with the following to-many relationship:

article.tags <<------->> tag.articles

In this scenario an article can have numerous tags, and those same tags can be assigned to multiple articles. Whenever an article's tags are edited, I remove all previous tags associated with the article and re-add those that are still relevant, like so:

// Remove any existing tags from this article
if(article.tags.count)
{
    NSSet *existingEventTags = [article.tags copy];
    for (NSManagedObject *tag in existingEventTags)
    {
        [[article mutableSetValueForKey:@"tags"] removeObject:tag];
        [moc deleteObject:tag];
    }
}

// Now re-assign any applicable tags to this article
for(NSString *tag in tags)
{
    Tag *existingTag = [self existingTagWithString:tag];
    if(existingTag)
    {
        [article addTagsObject:existingTag];
    }
    else
    {
        NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Tag" inManagedObjectContext:moc];
        Tag *newTag = (Tag *)[[NSManagedObject alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:moc];
        newTag.name = tag;
        [article addTagsObject:newTag];
    }
}

My question is how best to handle the delete rules associated with the two relationships. Essentially the behaviour I'm looking for is that when I remove a tag from an article, I only want to delete it entirely if no other articles have been tagged with it.

To accomplish this I've set my tag.articles relationship to Nullify, and my articles.tags relationship to Cascade. However when deleting an article any tags associated with it (regardless of whether they're associated with other articles) are removed. To test this I've written a simple debug function which I'll include below:

- (void)debugTags
{
    NSFetchRequest *request = [[NSFetchRequest alloc] init];

    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Tag" inManagedObjectContext:moc];
    [request setEntity:entity];

    // Execute the fetch.
    NSError *error = nil;
    NSArray *objects = [moc executeFetchRequest:request error:&error];
    if (objects != nil && [objects count] > 0)
    {
        NSLog(@"Found %d tags", [objects count]);
        for(Tag *tag in objects)
        {
            NSLog(@"\t%@ (%d events)", tag.name, tag.articles.count);
        }
    }
    else
    {
        NSLog(@"No tags found!");
    }
}

For completeness, here's an example output (showing the creation of two articles with two shared tags: food and fastfood). Following the deletion of one article, I'd expect to still find food and fastfood in the store, but with a relationship count of 1.

Before deletion
---------------

Found 4 tags
   breakfast (1 events)
   food (2 events)
   fastfood (2 events)
   lunch (1 events)

After deletion
--------------

Found 1 tags
   lunch (1 events)

Upvotes: 1

Views: 666

Answers (2)

ndg
ndg

Reputation: 2645

The best solution I found to this problem was to set both delete rules to Nullify, and to handle the deletion in my article's NSManagedObject prepareForDeletion method:

- (void)prepareForDeletion
{
    [super prepareForDeletion];

    for (Tag *tag in self.tags)
    {
        if(tag.events.count == 1)
        {
            [self.managedObjectContext deleteObject:tag];
        }
    }
}

Upvotes: 5

Mark Kryzhanouski
Mark Kryzhanouski

Reputation: 7241

Everything is correct except delete rules. With this configuration CoreData do not wont save context with error:

 NSValidationErrorKey=events, NSLocalizedDescription=The operation couldn’t be completed. (Cocoa error 1600.), NSValidationErrorValue=Relationship 'articles' on managed object

To accomplish this you should set tag.articles relationship to Nullify, and my articles.tags relationship to Cascade. This will work as expected.

And second. Your code crashed with exception: Collection <_NSFaultingMutableSet: 0x841de10> was mutated while being enumerated. Make a copy of relationships to safely enumerate through it:

    NSSet* tags = [[object valueForKey:@"tags"] copy];
    for (NSManagedObject *tag in tags) {
        [[object mutableSetValueForKey:@"tags"] removeObject:tag];
        [context deleteObject:tag];
    }

Upvotes: 0

Related Questions