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
- Relationship:
- Employee
- Relationship:
dept
, Destination:Department
, inverse:employees
- Relationship:
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.