0

For example, say I have a User managed object with a friends relationship that goes User<->User many-to-many.

Now let's say for some user a, 2 users x and d are already in a's friend relationship. But then:

  1. I add users b and c to friends.
  2. I remove d from friends (note that I do not delete the object itself from the context, just remove it from the relationship).

At this point, I'd like to somehow be able to tell that for the friends relationship:

  • x has no changes since it was always there.
  • b and c are in a "inserted set".
  • d is in a "removed set".

Or alternatively, that, for relationship friends:

  • b, c, and d have changes, and still being able to tell b and c's changes are of type "insertion" while d's is of type "deletion" / "removal"

I haven't yet figured out a generic way to achieve this. The context has updated, inserted and deleted sets but that's for the context, not for specific relationships.

SaldaVonSchwartz
  • 3,769
  • 2
  • 41
  • 78
  • In this whole process, are you expecting to save changes at any point? And if not, does it actually make sense to consider `d` to be removed when changes were never saved while they were in the relationship? – Tom Harrington Apr 25 '13 at 20:22
  • well the coredata store is in-memory. But in the background, the managed objects are being instantiated from json representations coming from a server and committed back to the server. Which is why the example talks about the x and d objects. the idea is x came from the server so if it gets removed from the relationship i need to issue a DELETE for it on the friends route but otherwise nothing. And for the other objects, b and c are new so I'd issue a post for them over the same endpoint. Finally d was inserted and removed before the POST so it should not be included in POST to friends route. – SaldaVonSchwartz Apr 25 '13 at 20:30
  • As far as coredata, I can do a [context save:] or not if it helps with figuring out the changes, since it makes no difference as far as the Entity <-> JSON mapping and server requests. – SaldaVonSchwartz Apr 25 '13 at 20:32
  • btw, yeah you are right, d being removed doesn't make sense cause I messed up the example, sorry. I'll correct it. Basically assume x and d both started in the relationship. x has no changes by the time I commit and d is in the removed set since it was there but no longer is – SaldaVonSchwartz Apr 25 '13 at 20:39

2 Answers2

0

NSManagedObject can give you more info about its changes by calling - (NSDictionary *)changedValues. But, unfortunately, it is not enough for tracking changes in relationships. Below, I've shared a technique that I used earlier. You should find changes before performing save:.

NSDictionary* relationshipsByName = [[object entity] relationshipsByName];
NSDictionary* changedValues = [object changedValues];
NSManagedObjectContext* contextToCompateChanges = [[NSManagedObjectContext alloc] init];
[contextToCompateChanges setPersistentStoreCoordinator:persistentStoreCoordinator];

for (NSString* propertyName in changedValues) {
    NSRelationshipDescription* relationshipDescription = [relationshipsByName objectForKey:propertyName];

    if (relationshipDescription == nil]) {
        continue;
    }

    if ([relationshipDescription isToMany]) {
        NSSet* changedValue = [changedValues objectForKey:propertyName];
        NSManagedObject* oldObject = [contextToCompateChanges objectWithID:[object objectID]];
        NSSet* oldValue = [oldObject valueForKey:propertyName];

        NSExpression* expr = [NSExpression expressionForKeyPath:@"objectID"];
        NSSet* changedIDs = [expr expressionValueWithObject:changedValue context:nil];
        NSSet* oldIDs = [expr expressionValueWithObject:oldValue context:nil];

        NSMutableSet* insertedSet = [NSMutableSet setWithSet:changedIDs];
        [insertedSet minusSet:oldIDs]];
        // insertedSet will contain objectsIDs of newly inserted objects

        NSMutableSet* deletedSet = [NSMutableSet setWithSet:oldIDs];
        [deletedSet minusSet:changedIDs];
        // deletedSet will contain objectsIDs of deleted objects
    } else {
        NSManagedObject* newRelationshipValue = [changedValues objectForKey:propertyName];
        if ((NSNull*)newRelationshipValue == [NSNull null]) {
            NSManagedObject* oldObject = [contextToCompateChanges objectWithID:[object objectID]];
            newRelationshipValue = [oldObject valueForKey:propertyName];

            if (newRelationshipValue == nil) {
                continue;
            }
            // newRelationshipValue is object that was deleted from relationsip
        } else {
            // newRelationshipValue is object that was inserted into relationsip
        }
    }       
}

[contextToCompateChanges release];

Hope this help you.

Mark Kryzhanouski
  • 7,251
  • 2
  • 22
  • 22
0

So far I came up with the following solution:

//Assume context, entityName, nameOfRelationship,etc... to be properly defined

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:entityName];
        NSArray *fetchResult = [self.context executeFetchRequest:fetchRequest error:&error];

        NSArray *deletedObjects = [fetchResult filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
            id relationship = [evaluatedObject changedValues][nameOfRelationship];
            return relationship && ![relationship containsObject:self.currentUser];
        }]];

        NSArray *insertedObjects = [fetchResult filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
            id relationship = [evaluatedObject changedValues][nameOfRelationship];
            return relationship && [relationship containsObject:self.currentUser];
        }]];

At this point I have 2 arrays of insertions and deletions (I know, I said sets in my questions... this works for now though). The one problem I observed though, is that for this to work I need to first retrieve data from the server, parse it into managed objects, place those managed objects in their appropriate "default" relationships and then do a context save. Only after the context save will changedValues reflect a change such as an object being deleted or inserted (if it wasn't inserted by default).

The real problem now is that a -save: on the context saves ALL objects. So, for instance if I have 2 entities each with relations (that is 2 separate entities each with a different relation and all my example applying to both entities), if I go through the above code, then issue the POST and DELETE requests and then save the context so that at that point those objects are "committed", trying to next do the same with the other entity and its' relationship will fails since the context save clears the changedValues for all objects.

SaldaVonSchwartz
  • 3,769
  • 2
  • 41
  • 78