Commits

Tuukka Norri  committed ced2743

Work on NSKVO and to-one relationships
- Unit tests currently fail.
- Renamed -addReferencingRelationship: to -addDependentRelationship: as well as the removing method.
- Insert, update and delete notifications should now cause KVO notifications to be posted for inverse to-one relationships. Other to-one relationships won't be handled, because we assume that the foreign key is declared to cascade or restrict on update and delete.
- Removed some old code concerning partial keys. We didn't really use that feature.
- Renamed -setAttributeDependency to -makeAttributeDependency to avoid confusion with NSKVC.
- Only inverse to-one relationships are considered now when setting attribute dependency.
- Setting to-one targets should also cause KVO notifications to be posted for inverse to-one relationships.
- To-one relationships are now properly cached.

  • Participants
  • Parent commits 29c4f72

Comments (0)

Files changed (15)

File Sources/BXAttributeDescription.h

 {
 	Class mAttributeClass;
 	NSString* mDatabaseTypeName;
-	CFMutableSetRef mRelationshipsUsing;	
+	CFMutableSetRef mRelationshipsUsing;
 }
 
 - (Class) attributeValueClass;

File Sources/BXAttributeDescription.m

 #import "BXAttributeDescription.h"
 #import "BXAttributeDescriptionPrivate.h"
 #import "BXEntityDescription.h"
+#import "BXRelationshipDescription.h"
 #import "BXDatabaseAdditions.h"
 #import "BXPropertyDescriptionPrivate.h"
 #import "PGTSCFScannedMemoryAllocator.h"
+#import "BXLogger.h"
 
 
 @class BXRelationshipDescription;
 {
 	if (mRelationshipsUsing)
 		CFRelease (mRelationshipsUsing);
+	
 	[mDatabaseTypeName release];
 	[super dealloc];
 }
 	}
 }
 
-- (void) addReferencingRelationship: (BXRelationshipDescription *) rel
+- (void) addDependentRelationship: (BXRelationshipDescription *) rel
 {
-	if (! mRelationshipsUsing)
+	BXEntityDescription* entity = [self entity];
+	CFSetCallBacks callbacks = PGTSScannedSetCallbacks ();
+	if ([[rel entity] isEqual: entity])
 	{
-		CFSetCallBacks callbacks = PGTSScannedSetCallbacks ();
-		mRelationshipsUsing = CFSetCreateMutable (PGTSScannedMemoryAllocator (), 0, &callbacks);
+		if (! mRelationshipsUsing)
+			mRelationshipsUsing = CFSetCreateMutable (PGTSScannedMemoryAllocator (), 0, &callbacks);
+
+		CFSetAddValue (mRelationshipsUsing, rel);
 	}
-	
-	CFSetAddValue (mRelationshipsUsing, rel);
+	else
+	{
+		BXLogError (@"Tried to add a relationship doesn't correspond to current attribute. Attribute: %@ relationship: %@", self, rel);
+	}
 }
 
-- (void) removeReferencingRelationship: (BXRelationshipDescription *) rel
+- (void) removeDependentRelationship: (BXRelationshipDescription *) rel
 {
 	if (mRelationshipsUsing)
 		CFSetRemoveValue (mRelationshipsUsing, rel);
 }
+
+- (NSSet *) dependentRelationships
+{
+	return (NSSet *) mRelationshipsUsing;
+}
 @end

File Sources/BXAttributeDescriptionPrivate.h

 #import <BaseTen/BXAttributeDescription.h>
 
 @class BXRelationshipDescription;
+@class BXDatabaseObject;
 
 
 @interface BXAttributeDescription (PrivateMethods)
 - (void) setAttributeValueClass: (Class) aClass;
 - (void) setDatabaseTypeName: (NSString *) typeName;
 
-- (void) addReferencingRelationship: (BXRelationshipDescription *) rel;
-- (void) removeReferencingRelationship: (BXRelationshipDescription *) rel;
+- (void) addDependentRelationship: (BXRelationshipDescription *) rel;
+- (void) removeDependentRelationship: (BXRelationshipDescription *) rel;
+- (NSSet *) dependentRelationships;
 @end

File Sources/BXDatabaseContext.m

 #import "BXDelegateProxy.h"
 #import "BXDatabaseContextDelegateDefaultImplementation.h"
 #import "BXRelationshipDescriptionPrivate.h"
+#import "PGTSHOM.h"
 
 
 __strong static NSMutableDictionary* gInterfaceClassSchemes = nil;
 	return retval;
 }
 
+- (NSDictionary *) targetsByObject: (NSArray *) objects forRelationships: (id) rels fireFaults: (BOOL) shouldFire
+{
+	NSMutableDictionary* targetsByObject = [NSMutableDictionary dictionaryWithCapacity: [objects count]];
+	TSEnumerate (currentObject, e, [objects objectEnumerator])
+	{
+		if ([NSNull null] != currentObject)
+		{
+			id targets = [[rels PGTSKeyCollect] registeredTargetFor: currentObject fireFault: shouldFire]; 
+			if (targets)
+				[targetsByObject setObject: targets forKey: currentObject];
+		}
+	}
+	return targetsByObject;
+}
+
 - (void) updatedObjectsInDatabase: (NSArray *) objectIDs faultObjects: (BOOL) shouldFault
 {
     if (0 < [objectIDs count])
 			
 			if (0 < [objects count])
 			{
-				//Fault the objects and send the notification
-				if (YES == shouldFault)
+				id rels = [entity inverseToOneRelationships];
+				NSDictionary* oldTargets = [self targetsByObject: objects forRelationships: rels fireFaults: NO];
+				NSDictionary* newTargets = [self targetsByObject: objects forRelationships: rels fireFaults: YES];
+				TSEnumerate (currentObject, e, [objects objectEnumerator])
+					[currentObject willChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+				
+				//Fault the objects and send the notifications
+				if (shouldFault)
 				{
 					TSEnumerate (currentObject, e, [objects objectEnumerator])
 						[currentObject removeFromCache: nil postingKVONotifications: YES];
 										  self, kBXDatabaseContextKey,
 										  nil];
 				
-				id notificationNames [2] = {kBXUpdateEarlyNotification, kBXUpdateNotification};
-				for (int i = 0; i < 2; i++)
-				{
-					[nc postNotificationName: notificationNames [i]
-									  object: entity
-									userInfo: userInfo];
-				}
+				[nc postNotificationName: kBXUpdateEarlyNotification object: entity userInfo: userInfo];
+				TSEnumerate (currentObject, e, [objects objectEnumerator])
+					[currentObject didChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+				
+				[nc postNotificationName: kBXUpdateNotification object: entity userInfo: userInfo];
 			}
-			
-#if 0
-			//Handle the views.
-			//This method will be called recursively, when the changed rows have been determined.
-			if (NO == [mDatabaseInterface messagesForViewModifications] && NO == [entity isView])
-			{
-				NSSet* dependentViews = [entity dependentViews];
-				TSEnumerate (currentView, e, [dependentViews objectEnumerator])
-				{
-					NSMutableArray* viewIDs = [NSMutableArray array];
-					TSEnumerate (currentID, e, [objectIDs objectEnumerator])
-					{
-						BXDatabaseObjectID* partialID = [currentID partialKeyForView: currentView];
-						if (nil != [self registeredObjectWithID: partialID])
-							[viewIDs addObject: partialID];
-					}
-					
-					[self updatedObjectsInDatabase: viewIDs faultObjects: YES];
-				}
-			}        
-#endif			
 		}
     }
 }
         TSEnumerate (entity, e, [idsByEntity keyEnumerator])
         {
             NSArray* objectIDs = [idsByEntity objectForKey: entity];
+			NSArray* objects = [self faultsWithIDs: objectIDs];
+			
+			id rels = [entity inverseToOneRelationships];
+			NSDictionary* oldTargets = [self targetsByObject: objects forRelationships: rels fireFaults: NO];
+			NSDictionary* newTargets = [self targetsByObject: objects forRelationships: rels fireFaults: YES];
+			TSEnumerate (currentObject, e, [objects objectEnumerator])
+				[currentObject willChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
 
             //Send the notifications
             NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                 objectIDs, kBXObjectIDsKey,
                 self, kBXDatabaseContextKey,
                 nil];
-            NSString* notificationNames [2] = {kBXInsertEarlyNotification, kBXInsertNotification};
-            for (int i = 0; i < 2; i++)
-                [nc postNotificationName: notificationNames [i] object: entity userInfo: userInfo];
-            
-#if 0
-            //If we can find objects with matching partial keys, send update notifications instead
-            if (NO == [mDatabaseInterface messagesForViewModifications] && NO == [entity isView])
-            {
-                NSSet* dependentViews = [entity dependentViews];
-                NSMutableArray* insertedIDs = [NSMutableArray array];
-                NSMutableArray* updatedIDs = [NSMutableArray array];
-                TSEnumerate (currentView, e, [dependentViews objectEnumerator])
-                {
-                    [insertedIDs removeAllObjects];
-                    [updatedIDs removeAllObjects];
-                    
-                    TSEnumerate (currentID, e, [objectIDs objectEnumerator])
-                    {
-                        BXDatabaseObjectID* partialID = [currentID partialKeyForView: currentView];
-                        if (nil == [self registeredObjectWithID: partialID])
-                            [insertedIDs addObject: partialID];
-                        else
-                            [updatedIDs addObject: partialID];
-                    }
-                    
-                    id updatedIds = [self registeredObjectsWithIDs: updatedIDs];
-                    NSString* notificationNames [4] = {
-                        kBXInsertEarlyNotification, 
-                        kBXUpdateEarlyNotification,
-                        kBXInsertNotification, 
-                        kBXUpdateNotification
-                    };
-                    NSArray* arrays [4] = {insertedIDs, updatedIDs, insertedIDs, updatedIDs};
-                    id objectArrays [4] = {nil, updatedIds, nil, updatedIds};
-                    for (int i = 0; i < 4; i++)
-                    {
-                        if (0 < [arrays [i] count])
-                        {
-                            NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
-                                [[insertedIDs copy] autorelease], kBXObjectIDsKey,
-                                self, kBXDatabaseContextKey,
-                                objectArrays [i], kBXObjectsKey, //This needs to be the last item since objectArrays [i] might be nil
-                                nil];
-                            [nc postNotificationName: notificationNames [i] object: currentView userInfo: userInfo];
-                        }
-                    }
-                }
-            }
-#endif            
+			
+			[nc postNotificationName: kBXInsertEarlyNotification object: entity userInfo: userInfo];
+			TSEnumerate (currentObject, e, [objects objectEnumerator])
+				[currentObject didChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+
+			[nc postNotificationName: kBXInsertNotification object: entity userInfo: userInfo];
         }
     }
 }
         //Post the notifications
         TSEnumerate (entity, e, [idsByEntity keyEnumerator])
         {
+			id rels = [entity inverseToOneRelationships];
+			id objects = [mObjects objectsForKeys: objectIDs notFoundMarker: [NSNull null]];
+
 			TSEnumerate (currentID, e, [objectIDs objectEnumerator])
 				[[self registeredObjectWithID: currentID] setDeleted: kBXObjectDeleted];
         
-			id objects = [mObjects objectsForKeys: objectIDs notFoundMarker: [NSNull null]];
+			NSDictionary* oldTargets = [self targetsByObject: objects forRelationships: rels fireFaults: NO];
+			NSDictionary* newTargets = [self targetsByObject: objects forRelationships: rels fireFaults: YES];
+			TSEnumerate (currentObject, e, [objects objectEnumerator])
+			{
+				if ([NSNull null] != currentObject)
+					[currentObject willChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+			}
         
 			//Send the notifications
 			NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
 									  objects, kBXObjectsKey,
 									  self, kBXDatabaseContextKey,
 									  nil];
-			const int count = 2;
-			NSString* notificationNames [2] = {kBXDeleteEarlyNotification, kBXDeleteNotification};
-			for (int i = 0; i < count; i++)
+			
+			[nc postNotificationName: kBXDeleteEarlyNotification object: entity userInfo: userInfo];
+			
+			TSEnumerate (currentObject, e, [objects objectEnumerator])
 			{
-				[nc postNotificationName: notificationNames [i]
-								  object: entity
-								userInfo: userInfo];
-			}
-        
-#if 0
-			//This method will be called recursively, when the changed rows have been determined
-			if (NO == [mDatabaseInterface messagesForViewModifications] && NO == [entity isView])
-			{
-				NSSet* dependentViews = [entity dependentViews];
-				TSEnumerate (currentView, e, [dependentViews objectEnumerator])
-				{
-					NSMutableSet* knownIDs = [NSMutableSet set];
-					TSEnumerate (currentID, e, [objectIDs objectEnumerator])
-					{
-						BXDatabaseObjectID* partialID = [currentID partialKeyForView: currentView];
-						if (nil != [self registeredObjectWithID: partialID])
-							[knownIDs addObject: partialID];
-					}
-					[self deletedObjectsFromDatabase: [knownIDs allObjects]];
-				}
-			}
-#endif
+				if ([NSNull null] != currentObject)
+					[currentObject didChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+			}			
+			
+			[nc postNotificationName: kBXDeleteNotification object: entity userInfo: userInfo];
 		}
 	}
 }
 		if ([currentEntity hasCapability: kBXEntityCapabilityRelationships])
 		{
 			TSEnumerate (currentRelationship, e, [[currentEntity relationshipsByName] objectEnumerator])
-				[currentRelationship setAttributeDependency];
+				[currentRelationship makeAttributeDependency];
 		}
 	}
 	

File Sources/BXDatabaseObject.m

 #import "BXObjectStatusInfo.h"
 #import "BXRelationshipDescription.h"
 #import "BXRelationshipDescriptionPrivate.h"
+#import "BXAttributeDescriptionPrivate.h"
 #import "BXLogger.h"
+#import "PGTSHOM.h"
 
 
 static NSString* 
 	NSError* error = nil;
 	id retval = nil;
 	
+	BXAssertValueReturn (nil != mContext, nil, @"Expected mContext not to be nil.");
+	
 	//If we have an error condition, return anything we have in cache.
 	if (! [mContext checkErrorHandling])
 		retval = [self cachedValueForKey: aKey];
 			case kBXDatabaseObjectKnownKey:
 			{
 				retval = [self cachedValueForKey: aKey];
-				
-				if (nil == retval)
+				if (! retval)
 				{
-					BXAssertValueReturn (nil != mContext, nil, @"Expected mContext not to be nil.");
-					NSDictionary* attrs = [[self entity] attributesByName];
-					if ([mContext fireFault: self key: [attrs objectForKey: aKey] error: &error])
-						retval = [self cachedValueForKey: aKey];
+					BXEntityDescription* entity = [self entity];
+					NSDictionary* attrs = [entity attributesByName];
+					BXAttributeDescription* attr = [attrs objectForKey: aKey];					
+					if ([mContext fireFault: self key: attr error: &error])
+						retval = [self cachedValueForKey: aKey];					
 				}
 				break;
 			}
 				
 			case kBXDatabaseObjectForeignKey:
 			{
-                BXAssertValueReturn (nil != mContext, nil, @"Expected mContext not to be nil.");
-
                 BXRelationshipDescription* rel = [[[self entity] relationshipsByName] objectForKey: aKey];
 				retval = [self cachedValueForKey: aKey];
-
-                if (rel)
+                if (rel && ! retval)
                 {
-                    //We'll have to fetch all to-one relationships because they won't be faulted automatically.
-                    //This is a problem in one-to-one relationships on the side that doesn't reference
-                    //the primary key.
-                    if (! ([rel isToMany] && retval))
-                    {
-						retval = [rel targetForObject: self error: &error];
-                        if (! error && [NSNull null] != retval)
-                        {
-							//Caching the result might cause a retain cycle.
-							[self setCachedValue: retval forKey: aKey];
-                        }
-                    }
+					retval = [rel targetForObject: self error: &error];
+					if (! error && [NSNull null] != retval)
+					{
+						//Caching the result might cause a retain cycle.
+						[self setCachedValue: retval forKey: aKey];
+					}
                 }
 				break;
 			}
 
 				BXEntityDescription* entity = [mObjectID entity];
 				BXAttributeDescription* attr = [[entity attributesByName] objectForKey: aKey];
+				
+				NSSet* rels = [attr dependentRelationships];
+				id oldTargets = [[rels PGTSKeyCollect] registeredTargetFor: self fireFault: NO];
+				[self setCachedValue: aVal forKey: aKey];
+				id newTargets = [[rels PGTSKeyCollect] registeredTargetFor: self fireFault: NO];
+				[self willChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+				
 				[mContext executeUpdateObject: self entity: nil predicate: nil
 							   withDictionary: [NSDictionary dictionaryWithObject: aVal forKey: attr]
 										error: &error];
-                break;
+				
+				[self didChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+				break;
 			}
 				
 			case kBXDatabaseObjectForeignKey:
 		id attr = [attributes objectForKey: currentKey];
 		[dict setObject: [aDict objectForKey: currentKey] forKey: attr];
 	}
+
+	NSMutableSet* rels = [NSMutableSet set];
+	TSEnumerate (currentAttr, e, [dict objectEnumerator])
+		[rels unionSet: [currentAttr dependentRelationships]];
+
+	id oldTargets = [[rels PGTSKeyCollect] registeredTargetFor: self fireFault: NO];
+	[self setCachedValuesForKeysWithDictionary: aDict];
+	id newTargets = [[rels PGTSKeyCollect] registeredTargetFor: self fireFault: NO];
+	[self willChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
+		
+    if (! [mContext executeUpdateObject: self entity: nil predicate: nil withDictionary: dict error: &error])
+		[[mContext internalDelegate] databaseContext: mContext hadError: error willBePassedOn: NO];
 	
-    if (NO == [mContext executeUpdateObject: self entity: nil predicate: nil withDictionary: dict error: &error])
-		[[mContext internalDelegate] databaseContext: mContext hadError: error willBePassedOn: NO];
+	[self didChangeInverseToOneRelationships: rels from: oldTargets to: newTargets];
 }
 
 /** 
 		[self didTurnIntoFault];
 }	
 
+- (NSDictionary *) valuesForRelationships: (id) relationships fireFault: (BOOL) fireFault
+{
+	Expect (relationships);
+	NSMutableDictionary* retval = [NSMutableDictionary dictionaryWithCapacity: [relationships count]];
+	TSEnumerate (currentRelationship, e, [relationships objectEnumerator])
+	{
+		BXDatabaseObject* value = [currentRelationship registeredTargetFor: self fireFault: fireFault];
+		if (value)
+			[retval setObject: value forKey: currentRelationship];
+	}
+	return retval;
+}
+
+- (void) changeInverseToOneRelationships: (id) relationships 
+									from: (NSDictionary *) oldTargets 
+									  to: (NSDictionary *) newTargets 
+								callback: (void (*)(id, NSString*)) callback
+{
+	ExpectV (relationships);
+	ExpectV (oldTargets);
+	ExpectV (newTargets);
+	
+	TSEnumerate (currentRelationship, e, [relationships objectEnumerator])
+	{
+		callback (self, [currentRelationship name]);
+		BXRelationshipDescription* inverse = (BXRelationshipDescription *) [currentRelationship inverseRelationship];
+		if (inverse)
+		{
+			BXDatabaseObject* oldTarget = [oldTargets objectForKey: currentRelationship];
+			BXDatabaseObject* newTarget = [newTargets objectForKey: currentRelationship];
+			if (oldTarget && newTarget)
+			{
+				NSString* inverseName = [inverse name];
+				callback (oldTarget, inverseName);
+				callback (newTarget, inverseName);
+			}
+		}
+	}	
+}
+
+static void
+WillChange (id sender, NSString* key)
+{
+	[sender willChangeValueForKey: key];
+}
+
+static void
+DidChange (id sender, NSString* key)
+{
+	[sender didChangeValueForKey: key];
+}
+
+- (void) willChangeInverseToOneRelationships: (id) relationships from: (NSDictionary *) oldTargets to: (NSDictionary *) newTargets
+{
+	[self changeInverseToOneRelationships: relationships from: oldTargets to: newTargets callback: &WillChange];
+}
+
+- (void) didChangeInverseToOneRelationships: (id) relationships from: (NSDictionary *) oldTargets to: (NSDictionary *) newTargets
+{
+	[self changeInverseToOneRelationships: relationships from: oldTargets to: newTargets callback: &DidChange];
+}
 @end

File Sources/BXDatabaseObjectPrivate.h

 - (NSDictionary *) allValues;
 - (void) removeFromCache: (NSString *) aKey postingKVONotifications: (BOOL) posting;
 - (id) valueForUndefinedKey2: (NSString *) aKey;
+
+- (NSDictionary *) valuesForRelationships: (id) relationships fireFault: (BOOL) fireFault;
+- (void) willChangeInverseToOneRelationships: (id) relationships from: (NSDictionary *) oldTargets to: (NSDictionary *) newTargets;
+- (void) didChangeInverseToOneRelationships: (id) relationships from: (NSDictionary *) oldTargets to: (NSDictionary *) newTargets;
 @end

File Sources/BXEntityDescription.m

 #import "BXDatabaseContext.h"
 #import "BXAttributeDescription.h"
 #import "BXRelationshipDescription.h"
+#import "BXRelationshipDescriptionPrivate.h"
 #import "BXAttributeDescriptionPrivate.h"
 #import "BXPropertyDescriptionPrivate.h"
 #import "BXDatabaseObject.h"
 	return [[mRelationships copy] autorelease];
 }
 
+
 - (BOOL) hasCapability: (enum BXEntityCapability) aCapability
 {
 	return (mCapabilities & aCapability ? YES : NO);
 	else
 		mFlags &= ~kBXEntityIsEnabled;
 }
+
+static int
+InverseToOneRelationships (id arg)
+{
+	int retval = 0;
+	BXRelationshipDescription* relationship = (BXRelationshipDescription *) arg;
+	if ([relationship isInverse] && ! [relationship isToMany])
+		retval = 1;
+	return retval;
+}
+
+
+- (id) inverseToOneRelationships;
+{
+	return [mRelationships PGTSValueSelectFunction: &InverseToOneRelationships];
+}
 @end

File Sources/BXEntityDescriptionPrivate.h

 - (void) removeRelationship: (BXRelationshipDescription *) aRelationship;
 - (void) setHasCapability: (enum BXEntityCapability) aCapability to: (BOOL) flag;
 - (void) setEnabled: (BOOL) flag;
+- (id) inverseToOneRelationships;
 @end

File Sources/BXForeignKey.h

 - (NSDeleteRule) deleteRule;
 - (void) setDeleteRule: (NSDeleteRule) aRule;
 
-- (BXDatabaseObjectID *) objectIDForSrcEntity: (BXEntityDescription *) srcEntity fromObject: (BXDatabaseObject *) object;
-- (BXDatabaseObjectID *) objectIDForDstEntity: (BXEntityDescription *) dstEntity fromObject: (BXDatabaseObject *) anObject;
+- (BXDatabaseObjectID *) objectIDForSrcEntity: (BXEntityDescription *) srcEntity fromObject: (BXDatabaseObject *) object fireFault: (BOOL) fireFault;
+- (BXDatabaseObjectID *) objectIDForDstEntity: (BXEntityDescription *) dstEntity fromObject: (BXDatabaseObject *) anObject fireFault: (BOOL) fireFault;
 //- (NSPredicate *) predicateForSrcEntity: (BXEntityDescription *) srcEntity valuesInObject: (BXDatabaseObject *) anObject;
 //- (NSPredicate *) predicateForDstEntity: (BXEntityDescription *) dstEntity valuesInObject: (BXDatabaseObject *) anObject;
 - (NSMutableDictionary *) srcDictionaryFor: (BXEntityDescription *) entity valuesFromDstObject: (BXDatabaseObject *) object;

File Sources/BXForeignKey.m

 #endif
 
 - (BXDatabaseObjectID *) objectIDForEntity: (BXEntityDescription *) entity fromObject: (BXDatabaseObject *) object
-							   entityIndex: (int) ei objectIndex: (int) oi
+							   entityIndex: (int) ei objectIndex: (int) oi fireFault: (BOOL) fireFault
 {
 	BOOL haveValues = YES;
 	NSMutableDictionary* values = [NSMutableDictionary dictionaryWithCapacity: [mFieldNames count]];
 	{
 		NSString* name = [currentFieldArray objectAtIndex: ei];
 		NSString* objectKey = [currentFieldArray objectAtIndex: oi];
-		id value = [object primitiveValueForKey: objectKey];
+		
+		id value = nil;
+		if (fireFault)
+			value = [object primitiveValueForKey: objectKey];
+		else
+			value = [object cachedValueForKey: objectKey];
+		
 		if (value)
 			[values setObject: value forKey: name];
 		else
 }
 
 
-- (BXDatabaseObjectID *) objectIDForDstEntity: (BXEntityDescription *) dstEntity fromObject: (BXDatabaseObject *) object
+- (BXDatabaseObjectID *) objectIDForDstEntity: (BXEntityDescription *) dstEntity fromObject: (BXDatabaseObject *) object fireFault: (BOOL) fireFault
 {
-	return [self objectIDForEntity: dstEntity fromObject: object entityIndex: 1 objectIndex: 0];
+	return [self objectIDForEntity: dstEntity fromObject: object entityIndex: 1 objectIndex: 0 fireFault: fireFault];
 }
 
-- (BXDatabaseObjectID *) objectIDForSrcEntity: (BXEntityDescription *) srcEntity fromObject: (BXDatabaseObject *) object
+- (BXDatabaseObjectID *) objectIDForSrcEntity: (BXEntityDescription *) srcEntity fromObject: (BXDatabaseObject *) object fireFault: (BOOL) fireFault
 {
-	return [self objectIDForEntity: srcEntity fromObject: object entityIndex: 0 objectIndex: 1];
+	return [self objectIDForEntity: srcEntity fromObject: object entityIndex: 0 objectIndex: 1 fireFault: fireFault];
 }
 
 - (NSMutableDictionary *) srcDictionaryFor: (BXEntityDescription *) entity valuesFromDstObject: (BXDatabaseObject *) object

File Sources/BXManyToManyRelationshipDescription.m

 }
 
 
-- (void) setAttributeDependency
+- (void) makeAttributeDependency
 {
 }
 @end

File Sources/BXOneToOneRelationshipDescription.m

 }
 
 
-- (void) setAttributeDependency
+- (void) makeAttributeDependency
 {
 	if (! [self isInverse])
-		[super setAttributeDependency];
+		[super makeAttributeDependency];
 }
-
 @end

File Sources/BXRelationshipDescription.m

 {
 	struct rel_attr_st* ctx = (struct rel_attr_st *) context;
 	BXRelationshipDescription* self = ctx->ra_sender;
-	BXRelationshipDescription* inverse = [self inverseRelationship];
 	NSDictionary* attributes = ctx->ra_attrs;
 	
 	BXAttributeDescription* attr = [attributes objectForKey: srcKey];
-	[attr removeReferencingRelationship: self];
-	[attr removeReferencingRelationship: inverse];
+	[attr removeDependentRelationship: self];
 }
 
 
 {
 	struct rel_attr_st* ctx = (struct rel_attr_st *) context;
 	BXRelationshipDescription* self = ctx->ra_sender;
-	BXRelationshipDescription* inverse = [self inverseRelationship];
 	NSDictionary* attributes = ctx->ra_attrs;
 	
 	BXAttributeDescription* attr = [attributes objectForKey: srcKey];
-	[attr addReferencingRelationship: self];
-
-    ExpectCV (inverse);
-	[attr addReferencingRelationship: inverse];
+	[attr addDependentRelationship: self];
 }
 
 
 - (void) removeAttributeDependency
 {
-	if (! [self isToMany])
+	if ([self isInverse] && ![self isToMany])
 	{
 		struct rel_attr_st ctx = {self, [[self entity] attributesByName]};
 		[self iterateForeignKey: &RemoveRelFromAttribute context: &ctx];
 }
 
 
-- (void) setAttributeDependency
+- (void) makeAttributeDependency
 {
-	if (! [self isToMany])
+	if ([self isInverse] && ![self isToMany])
 	{
 		struct rel_attr_st ctx = {self, [[self entity] attributesByName]};
 		[self iterateForeignKey: &AddRelToAttribute context: &ctx];
 	return [BXSetRelationProxy class];
 }
 
-- (id) targetForObject: (BXDatabaseObject *) aDatabaseObject error: (NSError **) error
+- (id) toOneTargetFor: (BXDatabaseObject *) databaseObject registeredOnly: (BOOL) registeredOnly 
+			fireFault: (BOOL) fireFault error: (NSError **) error
 {
-    BXAssertValueReturn (NULL != error, nil , @"Expected error to be set.");
-    BXAssertValueReturn (nil != aDatabaseObject, nil, @"Expected aDatabaseObject not to be nil.");
-    BXAssertValueReturn ([[self entity] isEqual: [aDatabaseObject entity]], nil, 
-						 @"Expected object's entity to match. Self: %@ aDatabaseObject: %@", self, aDatabaseObject);
+	Expect ([self isInverse]);
+	Expect (! [self isToMany]);
+	Expect (registeredOnly || error);
+
+	BXDatabaseObject* retval = nil;
+	BXEntityDescription* entity = [self destinationEntity];
+	BXDatabaseObjectID* objectID = [mForeignKey objectIDForDstEntity: entity fromObject: databaseObject fireFault: fireFault];
+	if (objectID)
+	{
+		BXDatabaseContext* ctx = [databaseObject databaseContext];
+		if (registeredOnly)
+			retval = [ctx registeredObjectWithID: objectID];
+		else
+			retval = [ctx objectWithID: objectID error: error];	
+	}
+	return retval;
+}
+			
+- (id) registeredTargetFor: (BXDatabaseObject *) databaseObject fireFault: (BOOL) fireFault
+{
+	id retval = nil;
+	if (databaseObject)
+		retval = [self toOneTargetFor: databaseObject registeredOnly: YES fireFault: fireFault error: NULL];
+	return retval;
+}
+
+- (id) targetForObject: (BXDatabaseObject *) databaseObject error: (NSError **) error
+{
+	Expect (error);
+	Expect (databaseObject);
+    BXAssertValueReturn ([[self entity] isEqual: [databaseObject entity]], nil, 
+						 @"Expected object's entity to match. Self: %@ aDatabaseObject: %@", self, databaseObject);
 	
 	id retval = nil;
 	//If we can determine an object ID, fetch the target object from the context's cache.
     if (! [self isToMany] && [self isInverse])
-    {
-		BXDatabaseObjectID* objectID = [mForeignKey objectIDForDstEntity: [self destinationEntity] fromObject: aDatabaseObject];
-		if (objectID)
-			retval = [[aDatabaseObject databaseContext] objectWithID: objectID error: error];
-    }
+		[self toOneTargetFor: databaseObject registeredOnly: NO fireFault: YES error: error];
 	else
 	{
 		BXEntityDescription* entity = [self destinationEntity];
-		NSPredicate* predicate = [self predicateForObject: aDatabaseObject];
+		NSPredicate* predicate = [self predicateForObject: databaseObject];
 		Class fetchedClass = [self fetchedClass];
-		id res = [[aDatabaseObject databaseContext] executeFetchForEntity: entity
-															withPredicate: predicate 
-														  returningFaults: NO
-														  excludingFields: nil
-															returnedClass: fetchedClass
-																	error: error];
+		id res = [[databaseObject databaseContext] executeFetchForEntity: entity
+														   withPredicate: predicate 
+														 returningFaults: NO
+														 excludingFields: nil
+														   returnedClass: fetchedClass
+																   error: error];
 		if (fetchedClass)
-			[res fetchedForRelationship: self owner: aDatabaseObject key: [self name]];
+			[res fetchedForRelationship: self owner: databaseObject key: [self name]];
 		
 		if ([self isToMany])
 			retval = res;
 	ExpectR (databaseObject, NO);
 	ExpectR ([[self entity] isEqual: [databaseObject entity]], NO);
 	BOOL retval = NO;
+	BXRelationshipDescription* inverse = [self inverseRelationship];
+	NSString* inverseName = [inverse name];
 	
     //We always want to modify the foreign key's (or corresponding view's) entity, hence the branch here.
+	//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 = [mForeignKey srcDictionaryFor: [self entity] valuesFromDstObject: target];
 		
+		BXDatabaseObject* oldTarget = nil;
+		if (inverseName)
+		{
+			oldTarget = [self registeredTargetFor: databaseObject fireFault: NO];
+			[oldTarget willChangeValueForKey: inverseName];
+			[target willChangeValueForKey: inverseName];
+		}
+		
     	[[databaseObject databaseContext] executeUpdateObject: nil
     													entity: [self entity]
     												 predicate: predicate
     											withDictionary: values
     													 error: error];
-    	if (nil == *error)
+    	if (! *error)
     		[databaseObject setCachedValue: target forKey: [self name]];
-		else
+
+		if (inverseName)
+		{
+			[oldTarget didChangeValueForKey: inverseName];
+			[target didChangeValueForKey: inverseName];
+		}
+
+		if (*error)
 			goto bail;
     }
     else
     	//       non-empty or maximum size constraints, which are likely CHECK clauses.
     	//FIXME: these should be inside a transaction. Use the undo manager?
 		
+		BXDatabaseObject* oldTarget = nil;
+		if (! [self isToMany] && inverseName)
+		{
+			oldTarget = [databaseObject cachedValueForKey: [self name]];
+			[oldTarget willChangeValueForKey: inverseName];
+			[target willChangeValueForKey: inverseName];
+		}
+		
 		NSPredicate* predicate = nil;
     	if ((predicate = [self predicateForRemoving: target databaseObject: databaseObject]))
     	{
 			if (*error)
 				goto bail;
 		}
-		
+				
 		//Don't set if we are updating a collection because if the object has the
 		//value, it will be self-updating one.
-		if (! [self isToMany])
-			[databaseObject setCachedValue: target forKey: [self name]];
-    }
+		if (! [self isToMany] && inverseName)
+		{
+			[oldTarget didChangeValueForKey: inverseName];
+			[target didChangeValueForKey: inverseName];
+		}
+	}
 	
 	retval = YES;
 bail:

File Sources/BXRelationshipDescriptionPrivate.h

 - (void) setDeleteRule: (NSDeleteRule) aRule;
 
 //Remember to override these in subclasses.
+- (id) registeredTargetFor: (BXDatabaseObject *) databaseObject fireFault: (BOOL) fireFault;
 - (id) targetForObject: (BXDatabaseObject *) anObject error: (NSError **) error;
 - (BOOL) setTarget: (id) target
 		 forObject: (BXDatabaseObject *) aDatabaseObject
 - (void) iterateForeignKey: (void (*)(NSString*, NSString*, void*) )callback context: (void *) ctx;
 
 - (void) removeAttributeDependency;
-- (void) setAttributeDependency;
+- (void) makeAttributeDependency;
 @end
 
 

File UnitTests/ToOneChangeNotificationTests.m

     if (kObservingContext == context) 
 	{
 		mNotesReceived++;
+		NSLog (@"Got note!");
 	}
 	else 
 	{
 	[b1 addObserver: self forKeyPath: @"test2" options: 0 context: kObservingContext];
 	[b2 addObserver: self forKeyPath: @"test2" options: 0 context: kObservingContext];
 	
-	MKCAssertTrue (0 == mNotesReceived);
+	MKCAssertEquals (0, mNotesReceived);
 	[a setPrimitiveValue: [NSNull null] forKey: @"fkt1id"];
 	[a setPrimitiveValue: [NSNumber numberWithInteger: 2] forKey: @"fkt1id"];
-	MKCAssertTrue (4 == mNotesReceived);
+	MKCAssertEquals (4, mNotesReceived);
 	
 	[a removeObserver: self forKeyPath: @"test1"];
 	[b1 removeObserver: self forKeyPath: @"test2"];
 	[b1 addObserver: self forKeyPath: @"ototest1" options: 0 context: kObservingContext];
 	[b2 addObserver: self forKeyPath: @"ototest1" options: 0 context: kObservingContext];
 	
-	MKCAssertTrue (0 == mNotesReceived);
+	MKCAssertEquals (0, mNotesReceived);
 	[b1 setPrimitiveValue: [NSNull null] forKey: @"r1"];
 	[b2 setPrimitiveValue: [NSNumber numberWithInteger: 1] forKey: @"r1"];
-	MKCAssertTrue (4 == mNotesReceived);
+	MKCAssertEquals (4, mNotesReceived);
 	
 	[a removeObserver: self forKeyPath: @"ototest2"];
 	[b1 removeObserver: self forKeyPath: @"ototest1"];