mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-01-12 22:51:50 +08:00
Rework implementation of managed object deletion and refetching to use the refreshed visitor implementation. refs #1111, #1113
This commit is contained in:
@@ -35,6 +35,23 @@
|
||||
#undef RKLogComponent
|
||||
#define RKLogComponent RKlcl_cRestKitCoreData
|
||||
|
||||
@interface RKMappingGraphVisitation : NSObject
|
||||
@property (nonatomic, strong) id rootKey; // Will be [NSNull null] or a string value
|
||||
@property (nonatomic, strong) NSString *keyPath;
|
||||
@property (nonatomic, assign, getter = isCyclic) BOOL cyclic;
|
||||
@property (nonatomic, strong) RKMapping *mapping;
|
||||
@end
|
||||
|
||||
@implementation RKMappingGraphVisitation
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@: %p rootKey=%@ keyPath=%@ isCylic=%@ mapping=%@>",
|
||||
[self class], self, self.rootKey, self.keyPath, self.isCyclic ? @"YES" : @"NO", self.mapping];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
This class implements Tarjan's algorithm to efficiently visit all nodes within the mapping graph and detect cycles in the graph.
|
||||
|
||||
@@ -47,15 +64,15 @@
|
||||
|
||||
*/
|
||||
@interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject
|
||||
@property (nonatomic, readonly) NSSet *keyPaths;
|
||||
@property (nonatomic, readonly, strong) NSMutableArray *visitations;
|
||||
- (id)initWithResponseDescriptors:(NSArray *)responseDescriptors;
|
||||
@end
|
||||
|
||||
@interface RKNestedManagedObjectKeyPathMappingGraphVisitor ()
|
||||
@property (nonatomic, strong) NSMutableSet *mutableKeyPaths;
|
||||
@property (nonatomic, strong) NSMutableArray *visitationStack;
|
||||
@property (nonatomic, strong) NSMutableDictionary *lowValues;
|
||||
@property (nonatomic, strong) NSNumber *numberOfDecriptors;
|
||||
@property (nonatomic, strong, readwrite) NSMutableArray *visitations;
|
||||
@end
|
||||
|
||||
@implementation RKNestedManagedObjectKeyPathMappingGraphVisitor
|
||||
@@ -65,37 +82,62 @@
|
||||
self = [self init];
|
||||
if (self) {
|
||||
self.numberOfDecriptors = @([responseDescriptors count]);
|
||||
self.mutableKeyPaths = [NSMutableSet set];
|
||||
self.visitationStack = [NSMutableArray array];
|
||||
self.lowValues = [NSMutableDictionary dictionary];
|
||||
self.visitations = [NSMutableArray array];
|
||||
|
||||
for (RKResponseDescriptor *responseDescriptor in responseDescriptors) {
|
||||
[self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
|
||||
{
|
||||
RKMappingGraphVisitation *visitation = [RKMappingGraphVisitation new];
|
||||
visitation.mapping = mapping;
|
||||
if ([self.visitationStack count] == 0) {
|
||||
// If we are the first item in the stack, we are visiting the rootKey
|
||||
visitation.rootKey = keyPath ?: [NSNull null];
|
||||
} else {
|
||||
// Take the root key from the visitation stack
|
||||
visitation.rootKey = [[self.visitationStack objectAtIndex:0] rootKey];
|
||||
visitation.keyPath = keyPath;
|
||||
}
|
||||
|
||||
return visitation;
|
||||
}
|
||||
|
||||
// Traverse the mappings graph using Tarjan's algorithm
|
||||
- (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
|
||||
{
|
||||
NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping];
|
||||
if ([self.lowValues objectForKey:dictionaryKey]) {
|
||||
// This key path points to a cycle back into the graph
|
||||
if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath];
|
||||
if (![ mapping isKindOfClass:[RKEntityMapping class]]) return;
|
||||
|
||||
NSArray *keyPathComponents = [[self.visitationStack valueForKey:@"keyPath"] arrayByAddingObject:keyPath];
|
||||
NSString *keyPath = [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."];
|
||||
|
||||
RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:mapping atKeyPath:keyPath];
|
||||
cyclicVisitation.cyclic = YES;
|
||||
[self.visitations addObject:cyclicVisitation];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSNumber *lowValue = @([self.lowValues count]);
|
||||
[self.lowValues setObject:lowValue forKey:dictionaryKey];
|
||||
|
||||
NSUInteger stackPosition = [self.visitationStack count];
|
||||
[self.visitationStack addObject:@{ @"mapping": mapping, @"keyPath": keyPath ?: [NSNull null] }];
|
||||
RKMappingGraphVisitation *visitation = [self visitationForMapping:mapping atKeyPath:keyPath];
|
||||
[self.visitationStack addObject:visitation];
|
||||
|
||||
if ([mapping isKindOfClass:[RKObjectMapping class]]) {
|
||||
RKObjectMapping *objectMapping = (RKObjectMapping *)mapping;
|
||||
for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) {
|
||||
NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath;
|
||||
[self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
|
||||
[self visitMapping:relationshipMapping.mapping atKeyPath:relationshipMapping.destinationKeyPath];
|
||||
|
||||
// We want the minimum value
|
||||
NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping];
|
||||
@@ -105,36 +147,38 @@
|
||||
}
|
||||
}
|
||||
} else if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
|
||||
RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping;
|
||||
for (RKMapping *nestedMapping in dynamicMapping.objectMappings) {
|
||||
// Pop the visitation stack to remove the dynamic mapping, since each mapping within the dynamic mapping
|
||||
// is rooted at the same point in the graph
|
||||
[self.visitationStack removeLastObject];
|
||||
|
||||
for (RKMapping *nestedMapping in [(RKDynamicMapping *)mapping objectMappings]) {
|
||||
[self visitMapping:nestedMapping atKeyPath:keyPath];
|
||||
}
|
||||
}
|
||||
|
||||
if ([[self.lowValues objectForKey:dictionaryKey] isEqualToNumber:lowValue]) {
|
||||
NSRange range = NSMakeRange(stackPosition, [self.visitationStack count] - stackPosition);
|
||||
NSArray *mappingDetails = [self.visitationStack subarrayWithRange:range];
|
||||
NSArray *visitations = [self.visitationStack subarrayWithRange:range];
|
||||
[self.visitationStack removeObjectsInRange:range];
|
||||
|
||||
NSArray *mappings = [mappingDetails valueForKey:@"mapping"];
|
||||
for (NSDictionary *dictionary in mappingDetails) {
|
||||
NSString *keyPath = [dictionary objectForKey:@"keyPath"];
|
||||
NSString *mapping = [dictionary objectForKey:@"mapping"];
|
||||
if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:keyPath];
|
||||
}
|
||||
// Take everything left on the stack
|
||||
NSArray *keyPathComponents = [self.visitationStack valueForKey:@"keyPath"];
|
||||
NSString *nestingKeyPath = ([keyPathComponents count] > 1) ? [[keyPathComponents subarrayWithRange:NSMakeRange(1, [keyPathComponents count] - 1)] componentsJoinedByString:@"."] : nil;
|
||||
|
||||
for (RKMapping *mapping in mappings) {
|
||||
[visitations enumerateObjectsUsingBlock:^(RKMappingGraphVisitation *visitation, NSUInteger idx, BOOL *stop) {
|
||||
// If this is an entity mapping, collect the complete key path
|
||||
if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) {
|
||||
visitation.keyPath = nestingKeyPath ? [@[ nestingKeyPath, visitation.keyPath ] componentsJoinedByString:@"."] : visitation.keyPath;
|
||||
[self.visitations addObject:visitation];
|
||||
}
|
||||
|
||||
// Update the low value
|
||||
NSValue *relationshipKey = [NSValue valueWithNonretainedObject:mapping];
|
||||
[self.lowValues setObject:self.numberOfDecriptors forKey:relationshipKey];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSSet *)keyPaths
|
||||
{
|
||||
return self.mutableKeyPaths;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL)
|
||||
@@ -148,6 +192,49 @@ NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSU
|
||||
return fetchRequests;
|
||||
}
|
||||
|
||||
static NSSet *RKFlattenCollectionToSet(id collection)
|
||||
{
|
||||
NSMutableSet *mutableSet = [NSMutableSet set];
|
||||
if ([collection conformsToProtocol:@protocol(NSFastEnumeration)]) {
|
||||
for (id nestedObject in collection) {
|
||||
if ([nestedObject conformsToProtocol:@protocol(NSFastEnumeration)]) {
|
||||
if ([nestedObject isKindOfClass:[NSArray class]]) {
|
||||
[mutableSet unionSet:RKFlattenCollectionToSet([NSSet setWithArray:nestedObject])];
|
||||
} else if ([nestedObject isKindOfClass:[NSSet class]]) {
|
||||
[mutableSet unionSet:RKFlattenCollectionToSet(nestedObject)];
|
||||
} else if ([nestedObject isKindOfClass:[NSOrderedSet class]]) {
|
||||
[mutableSet unionSet:RKFlattenCollectionToSet([(NSOrderedSet *)nestedObject set])];
|
||||
}
|
||||
} else {
|
||||
[mutableSet addObject:nestedObject];
|
||||
}
|
||||
}
|
||||
} else if (collection) {
|
||||
[mutableSet addObject:collection];
|
||||
}
|
||||
|
||||
return mutableSet;
|
||||
}
|
||||
|
||||
/**
|
||||
Traverses a set of cyclic key paths within the mapping result. Because these relationships are cyclic, we continue collecting managed objects and traversing until the values returned by the key path are a complete subset of all objects already in the set.
|
||||
*/
|
||||
static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *cyclicKeyPaths, NSMutableSet *mutableSet)
|
||||
{
|
||||
if ([graph respondsToSelector:@selector(count)] && [graph count] == 0) return;
|
||||
|
||||
for (NSString *cyclicKeyPath in cyclicKeyPaths) {
|
||||
NSSet *objectsAtCyclicKeyPath = RKFlattenCollectionToSet([graph valueForKeyPath:cyclicKeyPath]);
|
||||
if ([objectsAtCyclicKeyPath count] == 0 || [objectsAtCyclicKeyPath isEqualToSet:[NSSet setWithObject:[NSNull null]]]) continue;
|
||||
if (! [objectsAtCyclicKeyPath isSubsetOfSet:mutableSet]) {
|
||||
[mutableSet unionSet:objectsAtCyclicKeyPath];
|
||||
for (id nestedValue in objectsAtCyclicKeyPath) {
|
||||
RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(nestedValue, cyclicKeyPaths, mutableSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the set of keys containing the outermost nesting keypath for all children.
|
||||
For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested'
|
||||
@@ -170,28 +257,17 @@ NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths)
|
||||
}];
|
||||
}
|
||||
|
||||
// When we map the root object, it is returned under the key `[NSNull null]`
|
||||
static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary)
|
||||
{
|
||||
@try {
|
||||
return ([keyPath isEqual:[NSNull null]]) ? [dictionary objectForKey:[NSNull null]] : [dictionary valueForKeyPath:keyPath];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
if ([[exception name] isEqualToString:NSUndefinedKeyException]) {
|
||||
RKLogWarning(@"Caught undefined key exception for keyPath '%@' in mapping result: This likely indicates an ambiguous keyPath is used across response descriptor or dynamic mappings.", keyPath);
|
||||
return nil;
|
||||
}
|
||||
|
||||
[exception raise];
|
||||
}
|
||||
}
|
||||
|
||||
static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary)
|
||||
static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSString *keyPath, NSMutableDictionary *dictionary)
|
||||
{
|
||||
NSCParameterAssert(value);
|
||||
NSCParameterAssert(keyPath);
|
||||
NSCParameterAssert(rootKey);
|
||||
NSCParameterAssert(dictionary);
|
||||
[keyPath isEqual:[NSNull null]] ? [dictionary setObject:value forKey:keyPath] : [dictionary setValue:value forKeyPath:keyPath];
|
||||
if (keyPath && ![keyPath isEqual:[NSNull null]]) {
|
||||
id valueAtRootKey = [dictionary objectForKey:rootKey];
|
||||
[valueAtRootKey setValue:value forKeyPath:keyPath];
|
||||
} else {
|
||||
[dictionary setObject:value forKey:rootKey];
|
||||
}
|
||||
}
|
||||
|
||||
// Precondition: Must be called from within the correct context
|
||||
@@ -210,44 +286,52 @@ static NSManagedObject *RKRefetchManagedObjectInContext(NSManagedObject *managed
|
||||
}
|
||||
|
||||
// Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects
|
||||
static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSSet *keyPaths, NSManagedObjectContext *managedObjectContext)
|
||||
static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSArray *visitations, NSManagedObjectContext *managedObjectContext)
|
||||
{
|
||||
if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects;
|
||||
NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy];
|
||||
[managedObjectContext performBlockAndWait:^{
|
||||
for (NSString *keyPath in keyPaths) {
|
||||
id value = RKMappedValueForKeyPathInDictionary(keyPath, dictionaryOfManagedObjects);
|
||||
if (! value) {
|
||||
continue;
|
||||
} else if ([value isKindOfClass:[NSArray class]]) {
|
||||
BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
|
||||
NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
|
||||
for (__strong id object in value) {
|
||||
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
|
||||
if (object) [newValue addObject:object];
|
||||
}
|
||||
value = (isMutable) ? newValue : [newValue copy];
|
||||
} else if ([value isKindOfClass:[NSSet class]]) {
|
||||
BOOL isMutable = [value isKindOfClass:[NSMutableSet class]];
|
||||
NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]];
|
||||
for (__strong id object in value) {
|
||||
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
|
||||
if (object) [newValue addObject:object];
|
||||
}
|
||||
value = (isMutable) ? newValue : [newValue copy];
|
||||
} else if ([value isKindOfClass:[NSOrderedSet class]]) {
|
||||
BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]];
|
||||
NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet];
|
||||
[(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
|
||||
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
|
||||
if (object) [newValue setObject:object atIndex:index];
|
||||
}];
|
||||
value = (isMutable) ? newValue : [newValue copy];
|
||||
} else if ([value isKindOfClass:[NSManagedObject class]]) {
|
||||
value = RKRefetchManagedObjectInContext(value, managedObjectContext);
|
||||
}
|
||||
NSArray *rootKeys = [visitations valueForKey:@"rootKey"];
|
||||
for (id rootKey in rootKeys) {
|
||||
NSSet *keyPaths = [visitations valueForKey:@"keyPath"];
|
||||
// If keyPaths contains null, then the root object is a managed object and we only need to refetch it
|
||||
NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths);
|
||||
|
||||
if (value) RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary);
|
||||
NSDictionary *mappingResultsAtRootKey = [dictionaryOfManagedObjects objectForKey:rootKey];
|
||||
for (NSString *keyPath in nonNestedKeyPaths) {
|
||||
id value = [keyPath isEqual:[NSNull null]] ? mappingResultsAtRootKey : [mappingResultsAtRootKey valueForKeyPath:keyPath];
|
||||
if (! value) {
|
||||
continue;
|
||||
} else if ([value isKindOfClass:[NSArray class]]) {
|
||||
BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
|
||||
NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
|
||||
for (__strong id object in value) {
|
||||
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
|
||||
if (object) [newValue addObject:object];
|
||||
}
|
||||
value = (isMutable) ? newValue : [newValue copy];
|
||||
} else if ([value isKindOfClass:[NSSet class]]) {
|
||||
BOOL isMutable = [value isKindOfClass:[NSMutableSet class]];
|
||||
NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]];
|
||||
for (__strong id object in value) {
|
||||
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
|
||||
if (object) [newValue addObject:object];
|
||||
}
|
||||
value = (isMutable) ? newValue : [newValue copy];
|
||||
} else if ([value isKindOfClass:[NSOrderedSet class]]) {
|
||||
BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]];
|
||||
NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet];
|
||||
[(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
|
||||
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
|
||||
if (object) [newValue setObject:object atIndex:index];
|
||||
}];
|
||||
value = (isMutable) ? newValue : [newValue copy];
|
||||
} else if ([value isKindOfClass:[NSManagedObject class]]) {
|
||||
value = RKRefetchManagedObjectInContext(value, managedObjectContext);
|
||||
}
|
||||
|
||||
if (value) RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -426,7 +510,7 @@ static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *re
|
||||
return localObjects;
|
||||
}
|
||||
|
||||
- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result atKeyPaths:(NSSet *)keyPaths error:(NSError **)error
|
||||
- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result withVisitor:(RKNestedManagedObjectKeyPathMappingGraphVisitor *)visitor error:(NSError **)error
|
||||
{
|
||||
if (! self.deletesOrphanedObjects) {
|
||||
RKLogDebug(@"Skipping deletion of orphaned objects: disabled as deletesOrphanedObjects=NO");
|
||||
@@ -446,20 +530,16 @@ static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *re
|
||||
// Build an aggregate collection of all the managed objects in the mapping result
|
||||
NSMutableSet *managedObjectsInMappingResult = [NSMutableSet set];
|
||||
NSDictionary *mappingResultDictionary = result.dictionary;
|
||||
for (NSString *keyPath in keyPaths) {
|
||||
id managedObjects = RKMappedValueForKeyPathInDictionary(keyPath, mappingResultDictionary);
|
||||
if (! managedObjects) {
|
||||
continue;
|
||||
} else if ([managedObjects isKindOfClass:[NSManagedObject class]]) {
|
||||
[managedObjectsInMappingResult addObject:managedObjects];
|
||||
} else if ([managedObjects isKindOfClass:[NSSet class]]) {
|
||||
[managedObjectsInMappingResult unionSet:managedObjects];
|
||||
} else if ([managedObjects isKindOfClass:[NSArray class]]) {
|
||||
[managedObjectsInMappingResult addObjectsFromArray:managedObjects];
|
||||
} else if ([managedObjects isKindOfClass:[NSOrderedSet class]]) {
|
||||
[managedObjectsInMappingResult addObjectsFromArray:[managedObjects array]];
|
||||
} else {
|
||||
[NSException raise:NSInternalInconsistencyException format:@"Unexpected object type '%@' encountered at keyPath '%@': Expected an `NSManagedObject`, `NSArray`, or `NSSet`.", [managedObjects class], keyPath];
|
||||
|
||||
for (RKMappingGraphVisitation *visitation in visitor.visitations) {
|
||||
id objectsAtRoot = [mappingResultDictionary objectForKey:visitation.rootKey];
|
||||
id managedObjects = visitation.keyPath ? [objectsAtRoot valueForKeyPath:visitation.keyPath] : objectsAtRoot;
|
||||
[managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)];
|
||||
|
||||
if (visitation.isCyclic) {
|
||||
NSSet *cyclicKeyPaths = [NSSet setWithArray:[visitation valueForKeyPath:@"mapping.relationshipMappings.destinationKeyPath"]];
|
||||
[managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)];
|
||||
RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(managedObjects, cyclicKeyPaths, managedObjectsInMappingResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +610,6 @@ static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *re
|
||||
|
||||
// Construct a set of key paths to all of the managed objects in the mapping result
|
||||
RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseDescriptors];
|
||||
NSSet *managedObjectMappingResultKeyPaths = visitor.keyPaths;
|
||||
|
||||
// Handle any cleanup
|
||||
success = [self deleteTargetObjectIfAppropriate:&error];
|
||||
@@ -539,7 +618,7 @@ static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *re
|
||||
return;
|
||||
}
|
||||
|
||||
success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult atKeyPaths:managedObjectMappingResultKeyPaths error:&error];
|
||||
success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult withVisitor:visitor error:&error];
|
||||
if (! success) {
|
||||
self.error = error;
|
||||
return;
|
||||
@@ -556,8 +635,7 @@ static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *re
|
||||
|
||||
// Refetch all managed objects nested at key paths within the results dictionary before returning
|
||||
if (self.mappingResult) {
|
||||
NSSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(managedObjectMappingResultKeyPaths);
|
||||
NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], nonNestedKeyPaths, self.managedObjectContext);
|
||||
NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext([self.mappingResult dictionary], visitor.visitations, self.managedObjectContext);
|
||||
self.mappingResult = [[RKMappingResult alloc] initWithDictionary:resultsDictionaryFromOriginalContext];
|
||||
}
|
||||
}
|
||||
|
||||
61
Tests/Fixtures/JSON/humans/nested_self_referential.json
Normal file
61
Tests/Fixtures/JSON/humans/nested_self_referential.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"houses": [
|
||||
{
|
||||
"houseID": 1,
|
||||
"city": "New York City",
|
||||
"state": "New York",
|
||||
"owner": {
|
||||
"humanID": 1,
|
||||
"name": "Blake"
|
||||
},
|
||||
"occupants": [
|
||||
{
|
||||
"humanID": 2,
|
||||
"name": "John",
|
||||
"landlord": {
|
||||
"humanID": 1,
|
||||
"name": "Blake"
|
||||
},
|
||||
"roommates": [
|
||||
{
|
||||
"humanID": 3,
|
||||
"name": "Mary",
|
||||
"landlord": {
|
||||
"humanID": 1,
|
||||
"name": "Blake"
|
||||
},
|
||||
"roommates": [
|
||||
{
|
||||
"humanID": 2,
|
||||
"name": "John"
|
||||
},
|
||||
{
|
||||
"humanID": 4,
|
||||
"name": "Edward"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"humanID": 4,
|
||||
"name": "Edward",
|
||||
"landlord": {
|
||||
"humanID": 1,
|
||||
"name": "Blake"
|
||||
},
|
||||
"roommates": [
|
||||
{
|
||||
"humanID": 2,
|
||||
"name": "John"
|
||||
},
|
||||
{
|
||||
"humanID": 3,
|
||||
"name": "Mary"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
26
Tests/Fixtures/JSON/humans/self_referential.json
Normal file
26
Tests/Fixtures/JSON/humans/self_referential.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Blake",
|
||||
"id": 1,
|
||||
"friends": [
|
||||
{
|
||||
"name": "Sarah",
|
||||
"id": 2,
|
||||
"friends": [
|
||||
{
|
||||
"name": "Monkey",
|
||||
"id": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Colin",
|
||||
"id": 4,
|
||||
"friends": [
|
||||
{
|
||||
"id": "3",
|
||||
"name": "Monkey"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -555,12 +555,148 @@ NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths);
|
||||
};
|
||||
managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ];
|
||||
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
|
||||
RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new];
|
||||
managedObjectRequestOperation.managedObjectCache = managedObjectCache;
|
||||
[managedObjectRequestOperation start];
|
||||
expect(managedObjectRequestOperation.error).to.beNil();
|
||||
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
|
||||
expect(orphanedHuman.managedObjectContext).to.beNil();
|
||||
}
|
||||
|
||||
- (void)testDeletionOfObjectsMappedFindsObjectsMappedBySelfReferentialMappings
|
||||
{
|
||||
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
|
||||
RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
|
||||
entityMapping.identificationAttributes = @[ @"railsID" ];
|
||||
[entityMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"id": @"railsID" }];
|
||||
[entityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"friends" toKeyPath:@"friends" withMapping:entityMapping]];
|
||||
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:entityMapping pathPattern:nil keyPath:nil statusCodes:[NSIndexSet indexSetWithIndex:200]];
|
||||
|
||||
// Create Blake, Sarah, Colin, Monkey & Orphan
|
||||
NSManagedObjectContext *context = managedObjectStore.persistentStoreManagedObjectContext;
|
||||
NSUInteger count = [context countForEntityForName:@"Human" predicate:nil error:nil];
|
||||
expect(count).to.equal(0);
|
||||
RKHuman *blake = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(1), @"name": @"Blake" }];
|
||||
RKHuman *sarah = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(2), @"name": @"Sarah" }];
|
||||
RKHuman *monkey = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(3), @"name": @"Monkey" }];
|
||||
RKHuman *colin = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(4), @"name": @"Colin" }];
|
||||
RKHuman *orphan = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(5), @"name": @"Orphan" }];
|
||||
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/self_referential.json" relativeToURL:[RKTestFactory baseURL]]];
|
||||
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
|
||||
RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
|
||||
return [NSFetchRequest fetchRequestWithEntityName:@"Human"];
|
||||
};
|
||||
managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ];
|
||||
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
|
||||
RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new];
|
||||
managedObjectRequestOperation.managedObjectCache = managedObjectCache;
|
||||
[managedObjectRequestOperation start];
|
||||
expect(managedObjectRequestOperation.error).to.beNil();
|
||||
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
|
||||
|
||||
// Verify that orphan was deleted
|
||||
count = [context countForEntityForName:@"Human" predicate:nil error:nil];
|
||||
expect(count).to.equal(4);
|
||||
|
||||
expect(blake.managedObjectContext).notTo.beNil();
|
||||
expect(sarah.managedObjectContext).notTo.beNil();
|
||||
expect(monkey.managedObjectContext).notTo.beNil();
|
||||
expect(colin.managedObjectContext).notTo.beNil();
|
||||
expect(orphan.managedObjectContext).to.beNil();
|
||||
}
|
||||
|
||||
- (void)testDeletionOfObjectsMappedFindsObjectsMappedByNestedSelfReferentialMappings
|
||||
{
|
||||
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
|
||||
RKEntityMapping *houseMapping = [RKEntityMapping mappingForEntityForName:@"House" inManagedObjectStore:managedObjectStore];
|
||||
[houseMapping addAttributeMappingsFromDictionary:@{ @"houseID": @"railsID" }];
|
||||
[houseMapping addAttributeMappingsFromArray:@[ @"city", @"state" ]];
|
||||
houseMapping.identificationAttributes = @[ @"railsID" ];
|
||||
|
||||
RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
|
||||
humanMapping.identificationAttributes = @[ @"railsID" ];
|
||||
[humanMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }];
|
||||
[humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"roommates" toKeyPath:@"friends" withMapping:humanMapping]];
|
||||
[humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"landlord" toKeyPath:@"landlord" withMapping:humanMapping]];
|
||||
|
||||
[houseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"owner" toKeyPath:@"owner" withMapping:humanMapping]];
|
||||
[houseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"occupants" toKeyPath:@"occupants" withMapping:humanMapping]];
|
||||
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:houseMapping pathPattern:nil keyPath:@"houses" statusCodes:[NSIndexSet indexSetWithIndex:200]];
|
||||
|
||||
// Create Blake, Sarah, Colin, Monkey & Orphan
|
||||
NSManagedObjectContext *context = managedObjectStore.persistentStoreManagedObjectContext;
|
||||
RKHuman *orphan = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(5), @"name": @"Orphan" }];
|
||||
RKHuman *edward = [RKTestFactory insertManagedObjectForEntityForName:@"Human" inManagedObjectContext:context withProperties:@{ @"railsID": @(4), @"name": @"Edward" }];
|
||||
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/nested_self_referential.json" relativeToURL:[RKTestFactory baseURL]]];
|
||||
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
|
||||
RKFetchRequestBlock humanFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
|
||||
return [NSFetchRequest fetchRequestWithEntityName:@"Human"];
|
||||
};
|
||||
RKFetchRequestBlock houseFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
|
||||
return [NSFetchRequest fetchRequestWithEntityName:@"House"];
|
||||
};
|
||||
managedObjectRequestOperation.fetchRequestBlocks = @[ humanFetchRequestBlock, houseFetchRequestBlock ];
|
||||
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
|
||||
managedObjectRequestOperation.deletesOrphanedObjects = YES;
|
||||
RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new];
|
||||
managedObjectRequestOperation.managedObjectCache = managedObjectCache;
|
||||
[managedObjectRequestOperation start];
|
||||
expect(managedObjectRequestOperation.error).to.beNil();
|
||||
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
|
||||
|
||||
NSUInteger count = [context countForEntityForName:@"Human" predicate:nil error:nil];
|
||||
expect(count).to.equal(4);
|
||||
|
||||
count = [context countForEntityForName:@"House" predicate:nil error:nil];
|
||||
expect(count).to.equal(1);
|
||||
|
||||
expect(edward.managedObjectContext).notTo.beNil();
|
||||
expect(orphan.managedObjectContext).to.beNil();
|
||||
}
|
||||
|
||||
- (void)testMappingWithDynamicMappingContainingIncompatibleEntityMappingsAtSameKeyPath
|
||||
{
|
||||
RKDynamicMapping *dynamicMapping = [RKDynamicMapping new];
|
||||
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
|
||||
RKEntityMapping *humanMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
|
||||
humanMapping.identificationAttributes = @[ @"railsID" ];
|
||||
[humanMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }];
|
||||
[humanMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"roommates" toKeyPath:@"friends" withMapping:humanMapping]];
|
||||
|
||||
RKEntityMapping *childMapping = [RKEntityMapping mappingForEntityForName:@"Child" inManagedObjectStore:managedObjectStore];
|
||||
RKEntityMapping *parentMapping = [RKEntityMapping mappingForEntityForName:@"Parent" inManagedObjectStore:managedObjectStore];
|
||||
parentMapping.identificationAttributes = @[ @"railsID" ];
|
||||
[parentMapping addAttributeMappingsFromDictionary:@{ @"name": @"name", @"humanID": @"railsID" }];
|
||||
[parentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"children" toKeyPath:@"children" withMapping:childMapping]];
|
||||
|
||||
[dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"invalid" expectedValue:@"whatever" objectMapping:humanMapping]];
|
||||
[dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:@"name" expectedValue:@"Blake" objectMapping:parentMapping]];
|
||||
|
||||
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping pathPattern:nil keyPath:@"houses.owner" statusCodes:[NSIndexSet indexSetWithIndex:200]];
|
||||
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/nested_self_referential.json" relativeToURL:[RKTestFactory baseURL]]];
|
||||
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
|
||||
RKFetchRequestBlock humanFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
|
||||
return [NSFetchRequest fetchRequestWithEntityName:@"Human"];
|
||||
};
|
||||
RKFetchRequestBlock parentFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
|
||||
return [NSFetchRequest fetchRequestWithEntityName:@"Parent"];
|
||||
};
|
||||
RKFetchRequestBlock childFetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
|
||||
return [NSFetchRequest fetchRequestWithEntityName:@"Child"];
|
||||
};
|
||||
managedObjectRequestOperation.fetchRequestBlocks = @[ humanFetchRequestBlock, parentFetchRequestBlock, childFetchRequestBlock ];
|
||||
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
|
||||
managedObjectRequestOperation.deletesOrphanedObjects = YES;
|
||||
RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new];
|
||||
managedObjectRequestOperation.managedObjectCache = managedObjectCache;
|
||||
[managedObjectRequestOperation start];
|
||||
expect(managedObjectRequestOperation.error).to.beNil();
|
||||
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
|
||||
}
|
||||
|
||||
- (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoObjectRequestOperationConcurrentlyDoesNotCreateDuplicateObjects
|
||||
{
|
||||
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
|
||||
|
||||
Reference in New Issue
Block a user