From 28887d3384ed8fda390fe5c9fd2db8ed490a7a22 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Tue, 1 Jan 2013 15:26:36 -0500 Subject: [PATCH] Add support for deletion of mapped objects by predicate. closes #1109 --- Code/CoreData/RKEntityMapping.h | 11 +++ ...KManagedObjectMappingOperationDataSource.m | 79 ++++++++++++++++++- ...agedObjectMappingOperationDataSourceTest.m | 44 +++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/Code/CoreData/RKEntityMapping.h b/Code/CoreData/RKEntityMapping.h index a8191b30..1f58c294 100644 --- a/Code/CoreData/RKEntityMapping.h +++ b/Code/CoreData/RKEntityMapping.h @@ -177,6 +177,17 @@ */ - (RKConnectionDescription *)connectionForRelationship:(id)relationshipOrName; +///------------------------------------ +/// @name Flagging Objects for Deletion +///------------------------------------ + +/** + A predicate that identifies objects for the receiver's entity that are to be deleted from the local store. + + This property provides support for local deletion of managed objects mapped as a 'tombstone' record from the source representation. + */ +@property (nonatomic, copy) NSPredicate *deletionPredicate; + ///------------------------------------------ /// @name Retrieving Default Attribute Values ///------------------------------------------ diff --git a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m index b627edea..3a0aa6af 100644 --- a/Code/CoreData/RKManagedObjectMappingOperationDataSource.m +++ b/Code/CoreData/RKManagedObjectMappingOperationDataSource.m @@ -18,6 +18,7 @@ // limitations under the License. // +#import #import "RKManagedObjectMappingOperationDataSource.h" #import "RKObjectMapping.h" #import "RKEntityMapping.h" @@ -34,6 +35,8 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; +static char kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey; + id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransformer *dateToStringValueTransformer); NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings); @@ -117,6 +120,58 @@ static NSDictionary *RKEntityIdentificationAttributesForEntityMappingWithReprese return entityIdentifierAttributes; } +@interface RKManagedObjectDeletionOperation : NSOperation + +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; +- (void)addEntityMapping:(RKEntityMapping *)entityMapping; +@end + +@interface RKManagedObjectDeletionOperation () +@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; +@property (nonatomic, strong) NSMutableSet *entityMappings; +@end + +@implementation RKManagedObjectDeletionOperation + +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + self = [self init]; + if (self) { + self.managedObjectContext = managedObjectContext; + self.entityMappings = [NSMutableSet new]; + } + return self; +} + +- (void)addEntityMapping:(RKEntityMapping *)entityMapping +{ + if (! entityMapping.deletionPredicate) return; + [self.entityMappings addObject:entityMapping]; +} + +- (void)main +{ + [self.managedObjectContext performBlockAndWait:^{ + NSMutableSet *objectsToDelete = [NSMutableSet set]; + for (RKEntityMapping *entityMapping in self.entityMappings) { + NSFetchRequest *fetchRequest = [NSFetchRequest alloc]; + [fetchRequest setEntity:entityMapping.entity]; + [fetchRequest setPredicate:entityMapping.deletionPredicate]; + NSError *error = nil; + NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; + if (fetchedObjects) { + [objectsToDelete addObjectsFromArray:fetchedObjects]; + } + } + + for (NSManagedObject *managedObject in objectsToDelete) { + [self.managedObjectContext deleteObject:managedObject]; + } + }]; +} + +@end + // Set Logging Component #undef RKLogComponent #define RKLogComponent RKlcl_cRestKitCoreData @@ -126,6 +181,7 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; @interface RKManagedObjectMappingOperationDataSource () @property (nonatomic, strong, readwrite) NSManagedObjectContext *managedObjectContext; @property (nonatomic, strong, readwrite) id managedObjectCache; +@property (nonatomic, strong) NSMutableArray *deletionPredicates; @end @implementation RKManagedObjectMappingOperationDataSource @@ -169,7 +225,7 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; "Unable to update existing object instances by identification attributes. Duplicate objects may be created."); } - // If we have found the entity identifier attributes, try to find an existing instance to update + // If we have found the entity identification attributes, try to find an existing instance to update NSEntityDescription *entity = [entityMapping entity]; NSManagedObject *managedObject = nil; if ([entityIdentifierAttributes count]) { @@ -246,6 +302,27 @@ extern NSString * const RKObjectMappingNestingAttributeKeyName; [operationQueue addOperation:operation]; RKLogTrace(@"Enqueued %@ dependent upon parent operation %@ to operation queue %@", operation, self.parentOperation, operationQueue); } + + // Handle tombstone deletion by predicate + if ([(RKEntityMapping *)mappingOperation.objectMapping deletionPredicate]) { + RKManagedObjectDeletionOperation *deletionOperation = nil; + if (self.parentOperation) { + // Attach a deletion operation for execution after the parent operation completes + deletionOperation = (RKManagedObjectDeletionOperation *)objc_getAssociatedObject(self.parentOperation, &kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey); + if (! deletionOperation) { + deletionOperation = [[RKManagedObjectDeletionOperation alloc] initWithManagedObjectContext:self.managedObjectContext]; + objc_setAssociatedObject(self.parentOperation, &kRKManagedObjectMappingOperationDataSourceAssociatedObjectKey, deletionOperation, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [deletionOperation addDependency:self.parentOperation]; + NSOperationQueue *operationQueue = self.operationQueue ?: [NSOperationQueue currentQueue]; + [operationQueue addOperation:deletionOperation]; + } + [deletionOperation addEntityMapping:(RKEntityMapping *)mappingOperation.objectMapping]; + } else { + deletionOperation = [[RKManagedObjectDeletionOperation alloc] initWithManagedObjectContext:self.managedObjectContext]; + [deletionOperation addEntityMapping:(RKEntityMapping *)mappingOperation.objectMapping]; + [deletionOperation start]; + } + } } return YES; diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m index a33f8a42..30efed76 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m @@ -1064,6 +1064,50 @@ } +- (void)testDeletionOfTombstoneRecords +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *mapping = [[RKEntityMapping alloc] initWithEntity:entity]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + mapping.deletionPredicate = [NSPredicate predicateWithFormat:@"sex = %@", @"female"]; + + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + human.sex = @"female"; + + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:nil]; + NSDictionary *representation = @{ @"name": @"Whatever" }; + RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:representation destinationObject:human mapping:mapping]; + operation.dataSource = dataSource; + NSError *error = nil; + BOOL success = [operation performMapping:&error]; + assertThatBool(success, is(equalToBool(YES))); + expect([human isDeleted]).to.equal(YES); +} + +- (void)testDeletionOfTombstoneRecordsInMapperOperation +{ + RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + RKEntityMapping *mapping = [[RKEntityMapping alloc] initWithEntity:entity]; + [mapping addAttributeMappingsFromArray:@[ @"name" ]]; + mapping.deletionPredicate = [NSPredicate predicateWithFormat:@"sex = %@", @"female"]; + + RKHuman *human = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext]; + human.sex = @"female"; + + RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext + cache:nil]; + NSDictionary *representation = @{ @"name": @"Whatever" }; + NSError *error = nil; + RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:representation mappingsDictionary:@{ [NSNull null]: mapping }]; + mapperOperation.mappingOperationDataSource = dataSource; + BOOL success = [mapperOperation execute:&error]; + assertThatBool(success, is(equalToBool(YES))); + expect([human isDeleted]).to.equal(YES); +} + // TODO: Import bencharmk utility somehow... //- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithFetchRequestMappingCache //{