Skip to content

Instantly share code, notes, and snippets.

@newhouseb
Created September 3, 2012 07:03
Show Gist options
  • Save newhouseb/3607474 to your computer and use it in GitHub Desktop.
Save newhouseb/3607474 to your computer and use it in GitHub Desktop.
Core Data being goofy

If you put this code into AppDelegate.m in a blank XCode project with coredata enabled (i.e. the checkbox that generates the boilerplate checked).

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"Length %i", [[object valueForKey:@"employees"] count]);
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    
    NSManagedObjectContext * parent = [self managedObjectContext];
    NSManagedObject * dept = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:parent];
    
    [dept addObserver:self forKeyPath:@"employees" options:0 context:nil];
    
    NSMutableArray * employees = [[NSMutableArray alloc] init];
    for (int i = 0; i < 1000; i++) {
        NSManagedObject * person = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:parent];
        [employees addObject:person];
    }

    NSManagedObjectContext * child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [child setParentContext:parent];
    [child performBlockAndWait:^{
        NSMutableArray * employees_to_add = [[NSMutableArray alloc] init];
        for (int i = 0; i < 1000; i++) {
            [employees_to_add addObject:[child objectWithID:[[employees objectAtIndex:i] objectID]]];
        }
        NSManagedObject * child_dept = [child objectWithID:dept.objectID];
        [child_dept setValue:[[NSOrderedSet alloc] initWithArray:employees_to_add] forKey:@"employees"];
        [child save:nil];
    }];

    
    return YES;
}

and make a couple entities

  • Department
    • Relationship: employees (to many, ordered), Destination: Employee inverse: dept
  • Employee
    • Relationship: dept, Destination: Department, inverse: employees

And then run the code, you'll get something like:

Length 1
Length 2
Length 3
Length 4
...
Length 861
Length 1000

You would expect that only one notification is fired because you only set the relationship that we're monitoring once. However, when you have properly consistent entities with inverse relationships, what really happens is that it happens to handle most (in this case 861) employee objects before the department object when merging and when each employee object is encountered it checks the integrity of its dept relationship and adds itself to the department's list of employees - one by one.

If you want one notification, you can either 'debounce' the KVO somehow or get rid of the inverse relationships!


............................................________ 
....................................,.-'"...................``~., 
.............................,.-"..................................."-., 
.........................,/...............................................":, 
.....................,?......................................................, 
.................../...........................................................,} 
................./......................................................,:`^`..} 
.............../...................................................,:"........./ 
..............?.....__.........................................:`.........../ 
............./__.(....."~-,_..............................,:`........../ 
.........../(_...."~,_........"~,_....................,:`........_/ 
..........{.._$;_......"=,_......."-,_.......,.-~-,},.~";/....} 
...........((.....*~_......."=-._......";,,./`..../"............../ 
...,,,___.`~,......"~.,....................`.....}............../ 
............(....`=-,,.......`........................(......;_,,-" 
............/.`~,......`-...................................../ 
.............`~.*-,.....................................|,./.....,__ 
,,_..........}.>-._...................................|..............`=~-, 
.....`=~-,__......`,................................. 
...................`=~-,,.,............................... 
................................`:,,...........................`..............__ 
.....................................`=-,...................,%`>--==`` 
........................................_..........._,-%.......` 
..................................., 

And, for what it's worth, the following, using mergeChangesFromContextDidSaveNotification, does not appear to have the same problem (edit: it totally does, see below):

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"Length %i", [[object valueForKey:@"employees"] count]);
}

- (void)merge:(NSNotification *)notification
{
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(merge:) withObject:notification waitUntilDone:NO];
    }
    
    [[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    
    NSManagedObjectContext * parent = [self managedObjectContext];
    NSManagedObject * dept = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:parent];
    
    [dept addObserver:self forKeyPath:@"employees" options:0 context:nil];
    
    NSMutableArray * employees = [[NSMutableArray alloc] init];
    for (int i = 0; i < 1000; i++) {
        NSManagedObject * person = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:parent];
        [employees addObject:person];
    }
    [parent save:nil];

    NSManagedObjectContext * child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [child setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(merge:) name:NSManagedObjectContextDidSaveNotification object:child];
    [child performBlockAndWait:^{
        NSMutableArray * employees_to_add = [[NSMutableArray alloc] init];
        for (int i = 0; i < 1000; i++) {
            [employees_to_add addObject:[child objectWithID:[[employees objectAtIndex:i] objectID]]];
        }
        NSManagedObject * child_dept = [child objectWithID:dept.objectID];
        [child_dept setValue:[[NSOrderedSet alloc] initWithArray:employees_to_add] forKey:@"employees"];
        [child save:nil];
    }];

    
    return YES;
}
......................................__................................................ 
.............................,-~*`¯lllllll`*~,.......................................... 
.......................,-~*`lllllllllllllllllllllllllll¯`*-,.................................... 
..................,-~*llllllllllllllllllllllllllllllllllllllllllll*-,.................................. 
...............,-*llllllllllllllllllllllllllllllllllllllllllllllllllllll.\.......................... ....... 
.............;*`lllllllllllllllllllllllllll,-~*~-,llllllllllllllllllll\................................ 
..............\lllllllllllllllllllllllllll/.........\;;;;llllllllllll,-`~-,......................... .. 
...............\lllllllllllllllllllll,-*...........`~-~-,...(.(¯`*,`,.......................... 
................\llllllllllll,-~*.....................)_-\..*`*;..).......................... 
.................\,-*`¯,*`)............,-~*`~................/..................... 
..................|/.../.../~,......-~*,-~*`;................/.\.................. 
................./.../.../.../..,-,..*~,.`*~*................*...\................. 
................|.../.../.../.*`...\...........................)....)¯`~,.................. 
................|./.../..../.......)......,.)`*~-,............/....|..)...`~-,............. 
..............././.../...,*`-,.....`-,...*`....,---......\..../...../..|.........¯```*~-,,,, 
...............(..........)`*~-,....`*`.,-~*.,-*......|.../..../.../............\........ 
................*-,.......`*-,...`~,..``.,,,-*..........|.,*...,*...|..............\........ 
...................*,.........`-,...)-,..............,-*`...,-*....(`-,............\....... 
......................f`-,.........`-,/...*-,___,,-~*....,-*......|...`-,..........\........  

###Edit: Regular merging actually has this problem too. Add a Company entity with a departments to many relationship, and then try the following code:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"employees"]) {
        NSLog(@"Length %i employees", [[object valueForKey:@"employees"] count]);
    } else if ([keyPath isEqualToString:@"departments"]) {
        NSLog(@"Length %i departments", [[object valueForKey:@"departments"] count]);
    }
}

- (void)merge:(NSNotification *)notification
{
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(merge:) withObject:notification waitUntilDone:NO];
    }
    
    [[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    
    NSManagedObjectContext * parent = [self managedObjectContext];
    NSManagedObject * company = [NSEntityDescription insertNewObjectForEntityForName:@"Company" inManagedObjectContext:parent];
    NSManagedObject * dept = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:parent];
    NSManagedObject * dept2 = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:parent];
    
    [dept addObserver:self forKeyPath:@"employees" options:0 context:nil];
    [company addObserver:self forKeyPath:@"departments" options:0 context:nil];

    NSMutableArray * employees = [[NSMutableArray alloc] init];
    for (int i = 0; i < 1000; i++) {
        NSManagedObject * person = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:parent];
        [employees addObject:person];
    }
    [parent save:nil];
    

    NSManagedObjectContext * child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [child setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(merge:) name:NSManagedObjectContextDidSaveNotification object:child];
    [child performBlockAndWait:^{
        NSMutableArray * employees_to_add = [[NSMutableArray alloc] init];
        for (int i = 0; i < 1000; i++) {
            [employees_to_add addObject:[child objectWithID:[[employees objectAtIndex:i] objectID]]];
        }
        NSManagedObject * child_dept = [child objectWithID:dept.objectID];
        [child_dept setValue:[[NSOrderedSet alloc] initWithArray:employees_to_add] forKey:@"employees"];
        
        NSManagedObject * child_dept2 = [child objectWithID:dept2.objectID];
        NSManagedObject * child_company = [child objectWithID:company.objectID];
        NSArray * departments = [NSArray arrayWithObjects:child_dept, child_dept2, nil];
        [child_company setValue:[[NSOrderedSet alloc] initWithArray:departments] forKey:@"departments"];
        
        [child save:nil];
    }];

    
    return YES;
}

For me, this prints:

Length 1 departments
Length 1000 employees
Length 2 departments

Which furthers the hypothesis that merges are done on a per object basis instead of atomically. Boo

###Edit #2 As a last attempt, instead of merging changes, I try refreshing the objects from disk. I added changed Company here to be (counter-intuitively) a to many relationship for departments (say... if two companies shared an external HR department).

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"employees"]) {
        NSLog(@"Length %i employees", [[object valueForKey:@"employees"] count]);
    } else if ([keyPath isEqualToString:@"departments"]) {
        NSLog(@"Length %i departments", [[object valueForKey:@"departments"] count]);
    } else if ([keyPath isEqualToString:@"company"]) {
        NSLog(@"Length %i company", [[object valueForKey:@"company"] count]);
    }
}

- (void)merge:(NSNotification *)notification
{
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(merge:) withObject:notification waitUntilDone:NO];
    }
    
    for (NSManagedObject * obj in [[notification object] registeredObjects]) {
        [[self managedObjectContext] refreshObject:[[self managedObjectContext] objectWithID:obj.objectID] mergeChanges:YES];
    }
    //[[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    
    NSManagedObjectContext * parent = [self managedObjectContext];
    NSManagedObject * company = [NSEntityDescription insertNewObjectForEntityForName:@"Company" inManagedObjectContext:parent];
    NSManagedObject * company2 = [NSEntityDescription insertNewObjectForEntityForName:@"Company" inManagedObjectContext:parent];
    NSManagedObject * dept = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:parent];
    NSManagedObject * dept2 = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:parent];
    
    [dept addObserver:self forKeyPath:@"employees" options:0 context:nil];
    [company addObserver:self forKeyPath:@"departments" options:0 context:nil];
    [dept2 addObserver:self forKeyPath:@"company" options:0 context:nil];

    NSMutableArray * employees = [[NSMutableArray alloc] init];
    for (int i = 0; i < 1000; i++) {
        NSManagedObject * person = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:parent];
        [employees addObject:person];
    }
    [parent save:nil];
    

    NSManagedObjectContext * child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [child setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(merge:) name:NSManagedObjectContextDidSaveNotification object:child];
    [child performBlockAndWait:^{
        NSMutableArray * employees_to_add = [[NSMutableArray alloc] init];
        for (int i = 0; i < 1000; i++) {
            [employees_to_add addObject:[child objectWithID:[[employees objectAtIndex:i] objectID]]];
        }
        NSManagedObject * child_dept = [child objectWithID:dept.objectID];
        [child_dept setValue:[[NSOrderedSet alloc] initWithArray:employees_to_add] forKey:@"employees"];
        
        NSManagedObject * child_dept2 = [child objectWithID:dept2.objectID];
        NSManagedObject * child_company = [child objectWithID:company.objectID];
        NSManagedObject * child_company2 = [child objectWithID:company2.objectID];
        [child_dept2 setValue:[[NSOrderedSet alloc] initWithArray:[NSArray arrayWithObjects:child_company, child_company2, nil]] forKey:@"company"];
        NSArray * departments = [NSArray arrayWithObjects:child_dept, child_dept2, nil];
        [child_company setValue:[[NSOrderedSet alloc] initWithArray:departments] forKey:@"departments"];
        
        [child save:nil];
    }];

    
    return YES;
}

Which prints

Length 1 company
Length 2 departments
Length 1000 employees
Length 2 company

If things were atomic, we should only see three log lines here.

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