Major overhaul to the Core Data managed object identification and relationship connection support.

* Replaces primary key with `RKEntityIdentifier`
* Add support for use of compound keys for object identification
* Refactor `RKConnectionMapping` to `RKConnectionDescription` and add support for connecting with multiple attributes
* Clarify naming of representation key methods to better match naming conventions
* Add type transformation support for object identification
* Greatly expand test coverage for object identification
* Drop the `NSEntityDescription` category
* Simplify the `RKManagedObjectCaching` protocol
* Add compound key support to the Fetch Request and In Memory Cache implementations
* Replace Kiwi with Specta for tests where contexts are helpful for organization
* Rename `defaultValueForMissingAttribute` to `defaultValueForAttribute`
This commit is contained in:
Blake Watters
2012-11-27 10:29:36 -05:00
parent f3a853e871
commit 8dc54a89b2
50 changed files with 2126 additions and 912 deletions

View File

@@ -26,8 +26,94 @@
#import "RKMappingOperation.h"
#import "RKDynamicMappingMatcher.h"
#import "RKManagedObjectCaching.h"
#import "RKRelationshipConnectionOperation.h"
#import "RKConnectionOperation.h"
#import "RKMappingErrors.h"
#import "RKValueTransformers.h"
extern NSString * const RKObjectMappingNestingAttributeKeyName;
id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransformer *dateToStringValueTransformer);
NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings);
// Return YES if the entity is identified by an attribute that acts as the nesting key in the source representation
static BOOL RKEntityMappingIsIdentifiedByNestingAttribute(RKEntityMapping *entityMapping)
{
for (NSAttributeDescription *attribute in entityMapping.entityIdentifier.attributes) {
RKAttributeMapping *attributeMapping = [[entityMapping propertyMappingsByDestinationKeyPath] objectForKey:[attribute name]];
if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
return YES;
}
}
return NO;
}
// We always need to map the dynamic nesting attribute first so that sub-key attribute mappings apply cleanly
static NSArray *RKEntityIdentifierAttributesInMappingOrder(RKEntityMapping *entityMapping)
{
NSMutableArray *orderedAttributes = [NSMutableArray arrayWithCapacity:[entityMapping.entityIdentifier.attributes count]];
for (NSAttributeDescription *attribute in entityMapping.entityIdentifier.attributes) {
RKAttributeMapping *attributeMapping = [[entityMapping propertyMappingsByDestinationKeyPath] objectForKey:[attribute name]];
if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
// We want to map the nesting attribute first
[orderedAttributes insertObject:attribute atIndex:0];
} else {
[orderedAttributes addObject:attribute];
}
}
return orderedAttributes;
}
static id RKValueForAttributeMappingInRepresentation(RKAttributeMapping *attributeMapping, NSDictionary *representation)
{
if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
return [[representation allKeys] lastObject];
} else {
return [representation valueForKeyPath:attributeMapping.sourceKeyPath];
}
}
static RKAttributeMapping *RKAttributeMappingForNameInMappings(NSString *name, NSArray *attributeMappings)
{
for (RKAttributeMapping *attributeMapping in attributeMappings) {
if ([[attributeMapping destinationKeyPath] isEqualToString:name]) return attributeMapping;
}
return nil;
}
/**
This function is the workhorse for extracting entity identifier attributes from a dictionary representation. It supports type transformations, compound entity identifier attributes, and dynamic nesting keys within the representation.
*/
static NSDictionary *RKEntityIdentifierAttributesForEntityMappingWithRepresentation(RKEntityMapping *entityMapping, NSDictionary *representation)
{
RKDateToStringValueTransformer *dateToStringTransformer = [[RKDateToStringValueTransformer alloc] initWithDateToStringFormatter:entityMapping.preferredDateFormatter
stringToDateFormatters:entityMapping.dateFormatters];
NSArray *orderedAttributes = RKEntityIdentifierAttributesInMappingOrder(entityMapping);
BOOL containsNestingAttribute = RKEntityMappingIsIdentifiedByNestingAttribute(entityMapping);
__block NSArray *attributeMappings = entityMapping.attributeMappings;
if (containsNestingAttribute) RKLogDebug(@"Detected use of nested dictionary key as identifying attribute");
NSMutableDictionary *entityIdentifierAttributes = [NSMutableDictionary dictionaryWithCapacity:[orderedAttributes count]];
[orderedAttributes enumerateObjectsUsingBlock:^(NSAttributeDescription *attribute, NSUInteger idx, BOOL *stop) {
RKAttributeMapping *attributeMapping = RKAttributeMappingForNameInMappings([attribute name], attributeMappings);
Class attributeClass = [entityMapping classForProperty:[attribute name]];
id attributeValue = nil;
if (containsNestingAttribute && idx == 0) {
// This is the nesting attribute
attributeValue = RKTransformedValueWithClass([[representation allKeys] lastObject], attributeClass, dateToStringTransformer);
attributeMappings = RKApplyNestingAttributeValueToMappings([attribute name], attributeValue, attributeMappings);
} else {
id sourceValue = RKValueForAttributeMappingInRepresentation(attributeMapping, representation);
attributeValue = RKTransformedValueWithClass(sourceValue, attributeClass, dateToStringTransformer);
}
[entityIdentifierAttributes setObject:attributeValue ?: [NSNull null] forKey:[attribute name]];
}];
return entityIdentifierAttributes;
}
// Set Logging Component
#undef RKLogComponent
@@ -64,69 +150,41 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName;
return [mapping.objectClass new];
}
RKEntityMapping *entityMapping = (RKEntityMapping *)mapping;
id object = nil;
id primaryKeyValue = nil;
NSString *primaryKeyAttribute;
NSEntityDescription *entity = [entityMapping entity];
RKAttributeMapping *primaryKeyAttributeMapping = nil;
primaryKeyAttribute = [entityMapping primaryKeyAttribute];
if (primaryKeyAttribute) {
// If a primary key has been set on the object mapping, find the attribute mapping
// so that we can extract any existing primary key from the mappable data
for (RKAttributeMapping *attributeMapping in entityMapping.attributeMappings) {
if ([attributeMapping.destinationKeyPath isEqualToString:primaryKeyAttribute]) {
primaryKeyAttributeMapping = attributeMapping;
break;
}
}
// Get the primary key value out of the mappable data (if any)
if ([primaryKeyAttributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
RKLogDebug(@"Detected use of nested dictionary key as primaryKey attribute...");
primaryKeyValue = [[representation allKeys] lastObject];
} else {
NSString* keyPathForPrimaryKeyElement = primaryKeyAttributeMapping.sourceKeyPath;
if (keyPathForPrimaryKeyElement) {
primaryKeyValue = [representation valueForKeyPath:keyPathForPrimaryKeyElement];
} else {
RKLogWarning(@"Unable to find source attribute for primaryKeyAttribute '%@': unable to find existing object instances by primary key.", primaryKeyAttribute);
}
}
}
RKEntityMapping *entityMapping = (RKEntityMapping *)mapping;
NSDictionary *entityIdentifierAttributes = RKEntityIdentifierAttributesForEntityMappingWithRepresentation(entityMapping, representation);
if (! self.managedObjectCache) {
RKLogWarning(@"Performing managed object mapping with a nil managed object cache:\n"
"Unable to update existing object instances by primary key. Duplicate objects may be created.");
}
// If we have found the primary key attribute & value, try to find an existing instance to update
if (primaryKeyAttribute && primaryKeyValue && NO == [primaryKeyValue isEqual:[NSNull null]]) {
object = [self.managedObjectCache findInstanceOfEntity:entity
withPrimaryKeyAttribute:primaryKeyAttribute
value:primaryKeyValue
inManagedObjectContext:self.managedObjectContext];
if (object && [self.managedObjectCache respondsToSelector:@selector(didFetchObject:)]) {
[self.managedObjectCache didFetchObject:object];
// If we have found the entity identifier attributes, try to find an existing instance to update
NSEntityDescription *entity = [entityMapping entity];
NSManagedObject *managedObject = nil;
if ([entityIdentifierAttributes count]) {
NSArray *objects = [self.managedObjectCache managedObjectsWithEntity:entity
attributeValues:entityIdentifierAttributes
inManagedObjectContext:self.managedObjectContext];
if (entityMapping.entityIdentifier.predicate) objects = [objects filteredArrayUsingPredicate:entityMapping.entityIdentifier.predicate];
if ([objects count] > 0) {
managedObject = objects[0];
if ([objects count] > 1) RKLogWarning(@"Managed object cache returned %d objects for the identifier configured for the '%@' entity, expected 1.", [objects count], [entity name]);
}
if (managedObject && [self.managedObjectCache respondsToSelector:@selector(didFetchObject:)]) {
[self.managedObjectCache didFetchObject:managedObject];
}
}
if (object == nil) {
object = [[NSManagedObject alloc] initWithEntity:entity
if (managedObject == nil) {
managedObject = [[NSManagedObject alloc] initWithEntity:entity
insertIntoManagedObjectContext:self.managedObjectContext];
if (primaryKeyAttribute && primaryKeyValue && ![primaryKeyValue isEqual:[NSNull null]]) {
[object setValue:primaryKeyValue forKey:primaryKeyAttribute];
}
[managedObject setValuesForKeysWithDictionary:entityIdentifierAttributes];
if ([self.managedObjectCache respondsToSelector:@selector(didCreateObject:)]) {
[self.managedObjectCache didCreateObject:object];
[self.managedObjectCache didCreateObject:managedObject];
}
}
return object;
return managedObject;
}
// Mapping operations should be executed against managed object contexts with the `NSPrivateQueueConcurrencyType` concurrency type
@@ -150,27 +208,25 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName;
if ([mappingOperation.objectMapping isKindOfClass:[RKEntityMapping class]]) {
[self emitDeadlockWarningIfNecessary];
NSArray *connectionMappings = [(RKEntityMapping *)mappingOperation.objectMapping connectionMappings];
if ([connectionMappings count] > 0 && self.managedObjectCache == nil) {
NSArray *connections = [(RKEntityMapping *)mappingOperation.objectMapping connections];
if ([connections count] > 0 && self.managedObjectCache == nil) {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Cannot map an entity mapping that contains connection mappings with a data source whose managed object cache is nil." };
NSError *localError = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorNilManagedObjectCache userInfo:userInfo];
if (error) *error = localError;
return NO;
}
for (RKConnectionMapping *connectionMapping in connectionMappings) {
RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:mappingOperation.destinationObject
connectionMapping:connectionMapping
managedObjectCache:self.managedObjectCache];
__weak RKRelationshipConnectionOperation *weakOperation = operation;
for (RKConnectionDescription *connection in connections) {
RKConnectionOperation *operation = [[RKConnectionOperation alloc] initWithManagedObject:mappingOperation.destinationObject connection:connection managedObjectCache:self.managedObjectCache];
__weak RKConnectionOperation *weakOperation = operation;
[operation setCompletionBlock:^{
if (weakOperation.connectedValue) {
if ([mappingOperation.delegate respondsToSelector:@selector(mappingOperation:didConnectRelationship:withValue:usingMapping:)]) {
[mappingOperation.delegate mappingOperation:mappingOperation didConnectRelationship:connectionMapping.relationship withValue:weakOperation.connectedValue usingMapping:connectionMapping];
[mappingOperation.delegate mappingOperation:mappingOperation didConnectRelationship:connection.relationship toValue:weakOperation.connectedValue usingConnection:connection];
}
} else {
if ([mappingOperation.delegate respondsToSelector:@selector(mappingOperation:didFailToConnectRelationship:usingMapping:)]) {
[mappingOperation.delegate mappingOperation:mappingOperation didFailToConnectRelationship:connectionMapping.relationship usingMapping:connectionMapping];
[mappingOperation.delegate mappingOperation:mappingOperation didFailToConnectRelationship:connection.relationship usingConnection:connection];
}
}
}];