Skip to content

Instantly share code, notes, and snippets.

@irace
Created November 21, 2014 14:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save irace/509314c46718951137e0 to your computer and use it in GitHub Desktop.
Save irace/509314c46718951137e0 to your computer and use it in GitHub Desktop.
How to prevent Core Data objects from being orphaned when their relationship to another object is severed

Say you have a one-to-many relationship (modeled via Core Data) between something like a tweet and a bunch of photos:

@implementation Tweet

- (void)updateWithDictionary:(NSDictionary *)JSONDictionary {
    self.photos = [JSONDictionary["photos"] transformedArrayUsingBlock:^Photo *(NSDictionary *photoJSON) {
      NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Photo" inManagedObjectContext:self.managedObjectContext];
      Photo *photo = [[Photo alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.managedObjectContext];
      [photo updateWithDictionary:photoJSON];
      return photo;
    }];
}

This update method could theoretically be called on the same Tweet object multiple times, as the user refreshes their timeline. The code above would result in Photo objects being orphaned as new ones are created to take their place.

I can think of three decent ways to avoid this:

1) Properly merge photo data with what is already on disk

This would entail:

  • Creating identifiers for each photo
  • Updating photos that already exist
  • Creating photos that don't already exist
  • Deleting photos that exist on disk but not in the JSON response

Seems like more work than it's worth to be honest

2) Manually delete photos before creating new ones

- (void)updateWithDictionary:(NSDictionary *)JSONDictionary {
    [self.photos enumerateObjectsUsingBlock:^(Photo *photo, NSUInteger photoIndex, BOOL *stop) {
        [self.managedObjectContext deleteObject:photo];
    }];

    self.photos = [JSONDictionary["photos"] transformedArrayUsingBlock:^Photo *(NSDictionary *photoJSON) {
      NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Photo" inManagedObjectContext:self.managedObjectContext];
      Photo *photo = [[Photo alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.managedObjectContext];
      [photo updateWithDictionary:photoJSON];
      return photo;
    }];
}

Would result in some unnecessary deleting and re-creating of the same data, but would be far simpler than #1.

3) Have orphaned photos delete themselves

@implementation Photo

- (void)willSave {
  if (!self.tweet) {
    [self.managedObjectContext deleteObject:self];
  }
}

This is slightly cleaner than manually deleting the photos (in my opinion), but doesn't scale particularly well. It requires me to create the tweet inverse relationship (with approaches 1-2, the Photo doesn't actually need to have a reference to the Tweet).

Additionally, Photo could be generic and used in contexts other than tweets. Maybe a Photo can also be associated with a DM, or a user's avatar, in which case I'd need to create inverse relationships for all three and the willSave implementation will now look like:

@implementation Photo

- (void)willSave {
  if (!self.tweet && !self.user && !self.directMessage) {
    [self.managedObjectContext deleteObject:self];
  }
}
@estromlund
Copy link

@richeterre Hmmm, I clearly haven't worked with CloudKit 😁

If you need, there's a decently efficient implementation of findOrCreate (without constraints) in the Objc.io Core Data book here: link.

That said, if I had to ship an implementation for your problem right now, I'd go with the original option #3 but with a soft delete. Then do the actual MOC deletion later on.

I have to do this in Unlisted for Conversations. Messages find or create a conversation as they're synced from the API, but deleting all messages should delete the conversation. There are plenty of race conditions so I only soft delete the conversation, un-soft-delete it if a new message comes in for it during the same transaction, then do the actual deletion on entering the background if the soft deletion timestamp was more than a minute ago. Or something roughly like that.

Good luck! 😬

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment