Commits

Tuukka Norri committed 8da18a9

Bug fix
- Changed KVO notification sending during updates so that willChange methods are guaranteed to be called before the update. This seems to stop some exceptions we got earlier. In case of database notifications this is a bit more problematic. Also, the code works but needs refactoring since it adds to the complexity of BXDatabaseContext.
- Unfortunately had to disable some (undocumented) functionality.
- -[BXDatabaseObject setCachedValue2:] no longer sends KVO notifications. Instead, they are sent in the update method.

Comments (0)

Files changed (5)

Sources/BXDatabaseContext.m

 	return targetsByObject;
 }
 
+//FIXME: clean me up.
 - (void) updatedObjectsInDatabase: (NSArray *) objectIDs faultObjects: (BOOL) shouldFault
 {
     if (0 < [objectIDs count])
 			id rels = [entity inverseToOneRelationships];
 			NSDictionary* oldTargets = nil;
 			NSDictionary* newTargets = nil;
+			
 			if (0 < [rels count])
 			{
 				oldTargets = [self targetsByObject: objects forRelationships: rels fireFaults: NO];
 				newTargets = [self targetsByObject: objects forRelationships: rels fireFaults: YES];
+				
 				BXEnumerate (currentObject, e, [objects objectEnumerator])
 				{
 					if ([NSNull null] != currentObject)
 		if (updatedPkey)
 			oldPkey = [anObject primaryKeyFieldObjects];
 
+		//Handle KVO.
+		NSArray* updatedObjects = nil;
+		{
+			if (anObject)
+				updatedObjects = [NSArray arrayWithObject: anObject];
+			else
+			{
+				updatedObjects = [self executeFetchForEntity: anEntity withPredicate: predicate returningFaults: YES error: &localError];
+				if (localError)
+					goto bail;
+			}
+		}
+		struct update_kvo_ctx updateCtx = [self handleWillChangeForUpdate: updatedObjects newValues: aDict];
+		
 		objectIDs = [mDatabaseInterface executeUpdateWithDictionary: aDict objectID: [anObject objectID]
 															 entity: anEntity predicate: predicate error: &localError];
 		
+		[self handleDidChangeForUpdate: &updateCtx newValues: aDict 
+					 sendNotifications: !(localError || [mDatabaseInterface autocommits])
+						  targetEntity: anEntity];
+		
 		if (nil == localError)
         {
             NSArray* oldIDs = objectIDs;
             //Therefore, we need to notify about the change.
             if (YES == [mDatabaseInterface autocommits])
 			{
+#if 0
 				if (! [anEntity getsChangedByTriggers])
 					[self updatedObjectsInDatabase: oldIDs faultObjects: NO];
+#endif			
+				
 				//FIXME: move this to the if block where oldIDs are set.
 				if (updatedPkey)
 				{
             }
         }
     }
+	
+bail:
     BXHandleError (error, localError);
     return objectIDs;
 }
 	}
 	return mObjectModel;
 }
+
+- (struct update_kvo_ctx) handleWillChangeForUpdate: (NSArray *) givenObjects newValues: (NSDictionary *) newValues
+{
+	NSMutableArray* changedObjects = [NSMutableArray array];
+	NSMutableDictionary* relsByEntity = [NSMutableDictionary dictionary];
+	NSMutableDictionary* oldTargetsByObject = [NSMutableDictionary dictionary];
+	NSMutableDictionary* newTargetsByObject = [NSMutableDictionary dictionary];
+	NSArray* objectIDs = (id) [[givenObjects PGTSCollect] objectID];
+	NSMutableDictionary* idsByEntity = ObjectIDsByEntity (objectIDs);
+	AddObjectIDsForInheritance (idsByEntity);
+	
+	BXEnumerate (entity, e, [idsByEntity keyEnumerator])
+	{
+		NSArray* objectIDs = [idsByEntity objectForKey: entity];
+		NSArray* objects = [self registeredObjectsWithIDs: objectIDs nullObjects: NO];
+		id rels = [entity inverseToOneRelationships];
+		NSDictionary* oldTargets = nil;
+		NSDictionary* newTargets = nil;
+		
+		[changedObjects addObjectsFromArray: objects];
+		
+		BXEnumerate (currentObject, e, [objects objectEnumerator])
+		{
+			BXEnumerate (currentAttr, e, [newValues keyEnumerator])
+			[currentObject willChangeValueForKey: [currentAttr name]];
+		}
+		
+		if (0 < [rels count])
+		{
+			[relsByEntity setObject: rels forKey: entity];
+			BXEnumerate (currentObject, e, [objects objectEnumerator])
+			{
+				oldTargets = [[rels PGTSCollectDK] registeredTargetFor: currentObject fireFault: NO] ?: [NSDictionary dictionary];
+				
+				//FIXME: this seems really bad.
+				NSDictionary* oldValues = [[[currentObject cachedValues] copy] autorelease];
+				[currentObject setCachedValuesForKeysWithDictionary: newValues];
+				
+				newTargets = [[rels PGTSCollectDK] registeredTargetFor: currentObject fireFault: NO] ?: [NSDictionary dictionary];
+				[currentObject setCachedValuesForKeysWithDictionary: oldValues];
+				
+				[currentObject willChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+				
+				[oldTargetsByObject setObject: oldTargets forKey: currentObject];
+				[newTargetsByObject setObject: newTargets forKey: currentObject];
+			}
+		}
+	}
+	
+	struct update_kvo_ctx retval = {relsByEntity, changedObjects, oldTargetsByObject, newTargetsByObject};
+	return retval;
+}
+
+
+- (void) handleDidChangeForUpdate: (struct update_kvo_ctx *) ctx newValues: (NSDictionary *) newValues sendNotifications: (BOOL) shouldSend targetEntity: (BXEntityDescription *) entity
+{
+	NSNotificationCenter* nc = [self notificationCenter];
+	NSArray* changedObjects = ctx->ukc_objects;
+	NSDictionary* relsByEntity = ctx->ukc_rels_by_entity;
+	NSDictionary* oldTargetsByObject = ctx->ukc_old_targets_by_object;
+	NSDictionary* newTargetsByObject = ctx->ukc_new_targets_by_object;
+	
+	BXEnumerate (currentObject, e, [changedObjects objectEnumerator])
+	[currentObject setCachedValuesForKeysWithDictionary: newValues];
+	
+	NSArray* objectIDs = nil;
+	NSDictionary* userInfo = nil;
+	if (shouldSend)
+	{
+		objectIDs = (id) [[changedObjects PGTSCollect] objectID];
+		userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
+					objectIDs, kBXObjectIDsKey,
+					changedObjects, kBXObjectsKey,
+					self, kBXDatabaseContextKey,
+					nil];
+	}
+	
+	BXEnumerate (currentObject, e, [changedObjects objectEnumerator])
+	{
+		BXEnumerate (currentAttr, e, [newValues keyEnumerator])
+		[currentObject didChangeValueForKey: [currentAttr name]];
+	}
+	
+	BXEnumerate (currentObject, e, [changedObjects objectEnumerator])
+	{
+		NSDictionary* oldTargets = [oldTargetsByObject objectForKey: currentObject];
+		NSDictionary* newTargets = [newTargetsByObject objectForKey: currentObject];
+		id rels = [relsByEntity objectForKey: [currentObject entity]];
+		if (0 < [rels count])
+			[currentObject didChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+	}
+	
+	if (shouldSend)
+		[nc postNotificationName: kBXUpdateEarlyNotification object: entity userInfo: userInfo];
+}
 @end
 
 

Sources/BXDatabaseContextPrivate.h

 #import <BaseTen/BaseTen.h>
 
 
+struct update_kvo_ctx
+{
+	__strong NSDictionary*	ukc_rels_by_entity;
+	__strong NSArray*		ukc_objects;
+	__strong NSDictionary*	ukc_old_targets_by_object;
+	__strong NSDictionary*	ukc_new_targets_by_object;
+};
+
+
 @interface BXDatabaseContext (PrivateMethods)
 /* Moved from the context. */
 - (BOOL) executeDeleteFromEntity: (BXEntityDescription *) anEntity withPredicate: (NSPredicate *) predicate 
 
 - (void) setDatabaseObjectModel: (BXDatabaseObjectModel *) model;
 - (void) setDatabaseInterface: (id <BXInterface>) interface;
+
+- (struct update_kvo_ctx) handleWillChangeForUpdate: (NSArray *) objects newValues: (NSDictionary *) newValues;
+- (void) handleDidChangeForUpdate: (struct update_kvo_ctx *) ctx newValues: (NSDictionary *) newValues 
+				sendNotifications: (BOOL) shouldSend targetEntity: (BXEntityDescription *) entity;
 @end
 
 

Sources/BXDatabaseObject.m

 					[self willChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
 				}
 				
-				WillChange (self, aKey);
 				[mContext executeUpdateObject: self entity: nil predicate: nil
 							   withDictionary: [NSDictionary dictionaryWithObject: aVal forKey: attr]
 										error: &error];
-				DidChange (self, aKey);
 				
 				if (0 < [rels count])
 					[self didChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
 			{
 				BXEntityDescription* entity = [mObjectID entity];
 				BXRelationshipDescription* rel = [[entity relationshipsByName] objectForKey: aKey];
-				WillChange (self, aKey); //Calling KVO methods from using inverse to-one relationships sometimes fails.
+				//WillChange (self, aKey); //Calling KVO methods from using inverse to-one relationships sometimes fails.
 				[rel setTarget: aVal forObject: self error: &error];
-				DidChange (self, aKey);
+				//DidChange (self, aKey);
 				break;
 			}
                     
 - (void) setCachedValue2: (id) aValue forKey: (id) givenKey
 {
 	NSString* key = [givenKey BXAttributeName];
-	
-	//Emptying the cache sends a KVO notification.
-	BOOL changes = NO;
-	id oldValue = [mValues objectForKey: givenKey];
-	if (nil != oldValue && oldValue != aValue)
-		changes = YES;
-	
-	if (changes) [self willChangeValueForKey: key];
-	
-	if (nil == aValue)
+	if (! aValue)
 		[mValues removeObjectForKey: key];
 	else
-		[mValues setObject: aValue forKey: key];
-	
-	if (changes) [self didChangeValueForKey: key];
+		[mValues setObject: aValue forKey: key];	
 }
 
 - (void) setCreatedInCurrentTransaction: (BOOL) aBool

Sources/BXPGInterface.m

 		if ([mContext sendsLockQueries])
 			MarkLocked (mTransactionHandler, entity, mQueryBuilder, whereClause, whereClauseParameters, NO);
 		[mTransactionHandler checkSuperEntities: entity];
+		
 		NSArray* objectIDs = ObjectIDs (entity, res);
-
 		NSDictionary* values = (id) [[valueDict PGTSKeyCollectD] name];
-		if (objectID)
+		BXEnumerate (currentID, e, [objectIDs objectEnumerator])
 		{
-			BXDatabaseObject* object = [mContext registeredObjectWithID: objectID];
+			BXDatabaseObject* object = [mContext registeredObjectWithID: currentID];
 			[object setCachedValuesForKeysWithDictionary: values];
-		}
-		else
-		{
-			BXEnumerate (currentID, e, [objectIDs objectEnumerator])
-			{
-				BXDatabaseObject* object = [mContext registeredObjectWithID: currentID];
-				[object setCachedValuesForKeysWithDictionary: values];
-			}
-		}
-													   
+		}													   
 		retval = objectIDs;
 	}
 	

Sources/BXRelationshipDescription.m

 	//Also with one-to-many relationships the false branch is for modifying a collection of objects.
     if (mIsInverse)
     {		
-    	NSPredicate* predicate = [[databaseObject objectID] predicate];
 		NSDictionary* values = BXFkeySrcDictionary (mForeignKey, [self entity], target);
 		
 		BXDatabaseObject* oldTarget = nil;
 			[target willChangeValueForKey: inverseName];
 		}
 		
-    	[[databaseObject databaseContext] executeUpdateObject: nil
-    													entity: [self entity]
-    												 predicate: predicate
-    											withDictionary: values
-    													 error: error];
+    	[[databaseObject databaseContext] executeUpdateObject: databaseObject
+													   entity: [self entity]
+													predicate: nil
+											   withDictionary: values
+														error: error];
     	if (! *error)
     		[databaseObject setCachedValue: target forKey: [self name]];