From fbcef6abd5f6cfb84d2fa8ffb9de6db4c95a6b02 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 4 May 2012 22:15:30 -0400 Subject: [PATCH] First functional cut at new Core Data cacheing implementation. Tests clean, needs polishing. --- Code/CoreData/RKEntityByAttributeCache.h | 61 + Code/CoreData/RKEntityByAttributeCache.m | 270 +++ Code/CoreData/RKEntityCache.h | 91 + Code/CoreData/RKEntityCache.m | 122 + Code/CoreData/RKInMemoryManagedObjectCache.h | 1 - Code/CoreData/RKInMemoryManagedObjectCache.m | 24 +- Code/CoreData/RKManagedObjectCaching.h | 8 + Code/CoreData/RKManagedObjectMapping.m | 2 +- Gemfile | 1 + Gemfile.lock | 4 + Rakefile | 22 + RestKit.xcodeproj/project.pbxproj | 44 + .../JSON/benchmark_parents_and_children.json | 2062 +++++++++++++++++ Tests/Logic/CoreData/RKEntityCacheTest.m | 155 ++ .../RKInMemoryEntityAttributeCacheTest.h | 13 + .../RKInMemoryEntityAttributeCacheTest.m | 345 +++ .../CoreData/RKInMemoryEntityCacheTest.m | 4 +- .../RKManagedObjectMappingOperationTest.m | 72 + .../CoreData/RKManagedObjectMappingTest.m | 15 +- 19 files changed, 3304 insertions(+), 12 deletions(-) create mode 100644 Code/CoreData/RKEntityByAttributeCache.h create mode 100644 Code/CoreData/RKEntityByAttributeCache.m create mode 100644 Code/CoreData/RKEntityCache.h create mode 100644 Code/CoreData/RKEntityCache.m create mode 100644 Tests/Fixtures/JSON/benchmark_parents_and_children.json create mode 100644 Tests/Logic/CoreData/RKEntityCacheTest.m create mode 100644 Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.h create mode 100644 Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.m diff --git a/Code/CoreData/RKEntityByAttributeCache.h b/Code/CoreData/RKEntityByAttributeCache.h new file mode 100644 index 00000000..fba8c969 --- /dev/null +++ b/Code/CoreData/RKEntityByAttributeCache.h @@ -0,0 +1,61 @@ +// +// RKEntityByAttributeCache.h +// RestKit +// +// Created by Blake Watters on 5/1/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import + +// RKManagedObjectContext +// Maybe RKManagedObjectContextCache | RKEntityCache | RKEntityByAttributeCache +// TODO: Better name... RKEntityAttributeCache ?? +@interface RKEntityByAttributeCache : NSObject + +///----------------------------------------------------------------------------- +/// @name Creating a Cache +///----------------------------------------------------------------------------- + +- (id)initWithEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName managedObjectContext:(NSManagedObjectContext *)context; + +///----------------------------------------------------------------------------- +/// @name Getting Cache Identity +///----------------------------------------------------------------------------- + +@property (nonatomic, readonly) NSEntityDescription *entity; +@property (nonatomic, readonly) NSString *attribute; +@property (nonatomic, readonly) NSManagedObjectContext *managedObjectContext; +@property (nonatomic, assign) BOOL monitorsContextForChanges; + +///----------------------------------------------------------------------------- +/// @name Loading and Flushing the Cache +///----------------------------------------------------------------------------- + +- (void)load; +- (void)flush; + +///----------------------------------------------------------------------------- +/// @name Inspecting Cache State +///----------------------------------------------------------------------------- + +- (BOOL)isLoaded; + +- (NSUInteger)count; +- (NSUInteger)countWithAttributeValue:(id)attributeValue; + +- (BOOL)containsObject:(NSManagedObject *)object; +- (BOOL)containsObjectWithAttributeValue:(id)attributeValue; + +// Retrieve the object with the value for the attribute +- (NSManagedObject *)objectWithAttributeValue:(id)attributeValue; +- (NSSet *)objectsWithAttributeValue:(id)attributeValue; + +///----------------------------------------------------------------------------- +/// @name Managing Cached Objects +///----------------------------------------------------------------------------- + +- (void)addObject:(NSManagedObject *)object; +- (void)removeObject:(NSManagedObject *)object; + +@end diff --git a/Code/CoreData/RKEntityByAttributeCache.m b/Code/CoreData/RKEntityByAttributeCache.m new file mode 100644 index 00000000..a7d61cf2 --- /dev/null +++ b/Code/CoreData/RKEntityByAttributeCache.m @@ -0,0 +1,270 @@ +// +// RKEntityByAttributeCache.m +// RestKit +// +// Created by Blake Watters on 5/1/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#if TARGET_OS_IPHONE +#import +#endif + +#import "RKEntityByAttributeCache.h" +#import "RKLog.h" +#import "RKObjectPropertyInspector.h" +#import "RKObjectPropertyInspector+CoreData.h" + +// Set Logging Component +#undef RKLogComponent +#define RKLogComponent lcl_cRestKitCoreData + +@interface RKEntityByAttributeCache () +@property (nonatomic, retain) NSMutableDictionary *attributeValuesToObjectIDs; +@end + +@implementation RKEntityByAttributeCache + +@synthesize entity = _entity; +@synthesize attribute = _attribute; +@synthesize managedObjectContext = _managedObjectContext; +@synthesize attributeValuesToObjectIDs = _attributeValuesToObjectIDs; +@synthesize monitorsContextForChanges = _monitorsContextForChanges; + +- (id)initWithEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName managedObjectContext:(NSManagedObjectContext *)context +{ + self = [self init]; + if (self) { + _entity = [entity retain]; + _attribute = [attributeName retain]; + _managedObjectContext = [context retain]; + _monitorsContextForChanges = YES; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(managedObjectContextDidChange:) + name:NSManagedObjectContextObjectsDidChangeNotification + object:context]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(managedObjectContextDidSave:) + name:NSManagedObjectContextDidSaveNotification + object:context]; +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveMemoryWarning:) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; +#endif + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [_entity release]; + [_attribute release]; + [_managedObjectContext release]; + [_attributeValuesToObjectIDs release]; + [super dealloc]; +} + +- (NSUInteger)count +{ + return [self.attributeValuesToObjectIDs count]; +} + +- (NSUInteger)countWithAttributeValue:(id)attributeValue +{ + return [[self objectsWithAttributeValue:attributeValue] count]; +} + +- (BOOL)shouldCoerceAttributeToString:(NSString *)attributeValue +{ + if ([attributeValue isKindOfClass:[NSString class]] || [attributeValue isEqual:[NSNull null]]) { + return NO; + } + + Class attributeType = [[RKObjectPropertyInspector sharedInspector] typeForProperty:self.attribute ofEntity:self.entity]; + return [attributeType instancesRespondToSelector:@selector(stringValue)]; +} + +- (void)load +{ + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:self.entity]; + [fetchRequest setResultType:NSManagedObjectIDResultType]; + + NSError *error = nil; + NSArray *objectIDs = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; + if (error) { + RKLogError(@"Failed to load entity cache: %@", error); + return; + } + [fetchRequest release]; + + self.attributeValuesToObjectIDs = [NSMutableDictionary dictionaryWithCapacity:[objectIDs count]]; + for (NSManagedObjectID* objectID in objectIDs) { + NSError *error = nil; + NSManagedObject *object = [self.managedObjectContext existingObjectWithID:objectID error:&error]; + if (! object && error) { + RKLogError(@"Failed to retrieve managed object with ID %@: %@", objectID, error); + } + + [self addObject:object]; + } +} + +- (void)flush +{ + self.attributeValuesToObjectIDs = nil; +} + +- (void)reload +{ + [self flush]; + [self load]; +} + +- (BOOL)isLoaded +{ + return (self.attributeValuesToObjectIDs != nil); +} + +- (NSManagedObject *)objectWithAttributeValue:(id)attributeValue +{ + return [[self objectsWithAttributeValue:attributeValue] anyObject]; +} + +- (NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID { + /* + NOTE: + We use existingObjectWithID: as opposed to objectWithID: as objectWithID: can return us a fault + that will raise an exception when fired. existingObjectWithID:error: will return nil if the ID has been + deleted. objectRegisteredForID: is also an acceptable approach. + */ + NSError *error = nil; + NSManagedObject *object = [self.managedObjectContext existingObjectWithID:objectID error:&error]; + if (! object && error) { + RKLogError(@"Failed to retrieve managed object with ID %@. Error %@\n%@", objectID, [error localizedDescription], [error userInfo]); + return nil; + } + + return object; +} + +- (NSSet *)objectsWithAttributeValue:(id)attributeValue +{ + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + NSMutableSet *set = [self.attributeValuesToObjectIDs valueForKey:attributeValue]; + if (set) { + NSSet *objectIDs = [NSSet setWithSet:set]; + NSMutableSet *objects = [NSMutableSet setWithCapacity:[objectIDs count]]; + for (NSManagedObjectID *objectID in objectIDs) { + NSManagedObject *object = [self objectWithID:objectID]; + if (object) [objects addObject:object]; + } + + return objects; + } + + return [NSSet set]; +} + +- (void)addObject:(NSManagedObject *)object +{ + NSAssert([object.entity isEqual:self.entity], @"Cannot add object with entity '%@' to cache with entity of '%@'", [[object entity] name], [self.entity name]); + id attributeValue = [object valueForKey:self.attribute]; + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + if (attributeValue) { + NSManagedObjectID *objectID = [object objectID]; + BOOL isTemporary = [objectID isTemporaryID]; + NSMutableSet *set = [self.attributeValuesToObjectIDs valueForKey:attributeValue]; + if (set) { + [set addObject:objectID]; + } else { + set = [NSMutableSet setWithObject:objectID]; + } + + if (nil == self.attributeValuesToObjectIDs) self.attributeValuesToObjectIDs = [NSMutableDictionary dictionary]; + [self.attributeValuesToObjectIDs setValue:set forKey:attributeValue]; + } else { + RKLogWarning(@"Unable to add object with nil value for attribute '%@': %@", self.attribute, object); + } +} + +- (void)removeObject:(NSManagedObject *)object +{ + NSAssert([object.entity isEqual:self.entity], @"Cannot remove object with entity '%@' from cache with entity of '%@'", [[object entity] name], [self.entity name]); + id attributeValue = [object valueForKey:self.attribute]; + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + if (attributeValue) { + NSManagedObjectID *objectID = [object objectID]; + NSMutableSet *set = [self.attributeValuesToObjectIDs valueForKey:attributeValue]; + if (set) { + [set removeObject:objectID]; + } + } else { + RKLogWarning(@"Unable to remove object with nil value for attribute '%@': %@", self.attribute, object); + } +} + +- (BOOL)containsObjectWithAttributeValue:(id)attributeValue +{ + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + return [[self objectsWithAttributeValue:attributeValue] count] > 0; +} + +- (BOOL)containsObject:(NSManagedObject *)object +{ + if (! [object.entity isEqual:self.entity]) return NO; + id attributeValue = [object valueForKey:self.attribute]; + // Coerce to a string if possible + attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue; + return [[self objectsWithAttributeValue:attributeValue] containsObject:object]; +} + +- (void)managedObjectContextDidChange:(NSNotification *)notification +{ + if (self.monitorsContextForChanges == NO) return; + + NSDictionary *userInfo = notification.userInfo; + NSSet *insertedObjects = [userInfo objectForKey:NSInsertedObjectsKey]; + NSSet *updatedObjects = [userInfo objectForKey:NSUpdatedObjectsKey]; + NSSet *deletedObjects = [userInfo objectForKey:NSDeletedObjectsKey]; + RKLogTrace(@"insertedObjects=%@, updatedObjects=%@, deletedObjects=%@", insertedObjects, updatedObjects, deletedObjects); + + NSMutableSet *objectsToAdd = [NSMutableSet setWithSet:insertedObjects]; + [objectsToAdd unionSet:updatedObjects]; + + for (NSManagedObject *object in objectsToAdd) { + if ([object.entity isEqual:self.entity]) { + [self addObject:object]; + } + } + + for (NSManagedObject *object in deletedObjects) { + if ([object.entity isEqual:self.entity]) { + [self removeObject:object]; + } + } +} + +- (void)managedObjectContextDidSave:(NSNotification *)notification +{ + // After the MOC has been saved, we flush to ensure any temporary + // objectID references are converted into permanent ID's on the next load. + [self flush]; +} + +- (void)didReceiveMemoryWarning:(NSNotification *)notification +{ + [self flush]; +} + +@end diff --git a/Code/CoreData/RKEntityCache.h b/Code/CoreData/RKEntityCache.h new file mode 100644 index 00000000..c1626e10 --- /dev/null +++ b/Code/CoreData/RKEntityCache.h @@ -0,0 +1,91 @@ +// +// RKEntityCache.h +// RestKit +// +// Created by Blake Watters on 5/2/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import + +@class RKEntityByAttributeCache; + +/** + Instances of RKInMemoryEntityCache provide an in-memory caching mechanism for + objects in a Core Data managed object context. Managed objects can be cached by + attribute for fast retrieval without repeatedly hitting the Core Data persistent store. + This can provide a substantial speed advantage over issuing fetch requests + in cases where repeated look-ups of the same data are performed using a small set + of attributes as the query key. Internally, the cache entries are maintained as + references to the NSManagedObjectID of corresponding cached objects. + */ +@interface RKEntityCache : NSObject + +/// @name Initializing the Cache + +/** + Initializes the receiver with a managed object context containing the entity instances to be cached. + + @param context The managed object context containing objects to be cached. + @returns self, initialized with context. + */ +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context; + +@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext; + +/// @name Cacheing Objects by Attribute + +/** + Caches all instances of an entity using the value for an attribute as the cache key. + + @param entity The entity to cache all instances of. + @param attributeName The attribute to cache the instances by. + */ +- (void)cacheObjectsForEntity:(NSEntityDescription *)entity byAttribute:(NSString *)attributeName; + +/** + Returns a Boolean value indicating if all instances of an entity have been cached by a given attribute name. + + @param entity The entity to check the cache status of. + @param attributeName The attribute to check the cache status with. + @return YES if the cache has been loaded with instances with the given attribute, else NO. + */ +- (BOOL)isEntity:(NSEntityDescription *)entity cachedByAttribute:(NSString *)attributeName; + +/** + Retrieves the first cached instance of a given entity where the specified attribute matches the given value. + + @param entity The entity to search the cache for instances of. + @param attributeName The attribute to search the cache for matches with. + @param attributeValue The value of the attribute to return a match for. + @return A matching managed object instance or nil. + @raise NSInvalidArgumentException Raised if instances of the entity and attribute have not been cached. + */ +- (NSManagedObject *)objectForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue; + +/** + Retrieves the all cached instances of a given entity where the specified attribute matches the given value. + + @param entity The entity to search the cache for instances of. + @param attributeName The attribute to search the cache for matches with. + @param attributeValue The value of the attribute to return a match for. + @return All matching managed object instances or nil. + @raise NSInvalidArgumentException Raised if instances of the entity and attribute have not been cached. + */ +- (NSSet *)objectsForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue; + +// @name Accessing Underlying Caches +- (RKEntityByAttributeCache *)attributeCacheForEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName; +- (NSSet *)attributeCachesForEntity:(NSEntityDescription *)entity; + +// @name Managing the Cache + +/** + Empties the cache by releasing all cached objects. + */ +- (void)flush; + +- (void)addObject:(NSManagedObject *)object; +- (void)removeObject:(NSManagedObject *)object; + +@end diff --git a/Code/CoreData/RKEntityCache.m b/Code/CoreData/RKEntityCache.m new file mode 100644 index 00000000..7c938128 --- /dev/null +++ b/Code/CoreData/RKEntityCache.m @@ -0,0 +1,122 @@ +// +// RKEntityCache.m +// RestKit +// +// Created by Blake Watters on 5/2/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKEntityCache.h" +#import "RKEntityByAttributeCache.h" + +@interface RKEntityCache () +@property (nonatomic, retain) NSMutableSet *attributeCaches; +@end + +@implementation RKEntityCache + +@synthesize managedObjectContext = _managedObjectContext; +@synthesize attributeCaches = _attributeCaches; + +- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context +{ + self = [self init]; + if (self) { + _managedObjectContext = [context retain]; + _attributeCaches = [[NSMutableSet alloc] init]; + } + + return self; +} + +- (void)dealloc +{ + [_managedObjectContext release]; + [_attributeCaches release]; + [super dealloc]; +} + +- (void)cacheObjectsForEntity:(NSEntityDescription *)entity byAttribute:(NSString *)attributeName +{ + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + if (attributeCache && !attributeCache.isLoaded) { + [attributeCache load]; + } else { + attributeCache = [[RKEntityByAttributeCache alloc] initWithEntity:entity attribute:attributeName managedObjectContext:self.managedObjectContext]; + [attributeCache load]; + [self.attributeCaches addObject:attributeCache]; + [attributeCache release]; + } +} + +- (BOOL)isEntity:(NSEntityDescription *)entity cachedByAttribute:(NSString *)attributeName +{ + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + return (attributeCache && attributeCache.isLoaded); +} + +- (NSManagedObject *)objectForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue +{ + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + if (attributeCache) { + return [attributeCache objectWithAttributeValue:attributeValue]; + } + + return nil; +} + +- (NSSet *)objectsForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue +{ + RKEntityByAttributeCache *attributeCache = [self attributeCacheForEntity:entity attribute:attributeName]; + if (attributeCache) { + return [attributeCache objectsWithAttributeValue:attributeValue]; + } + + return [NSSet set]; +} + +- (RKEntityByAttributeCache *)attributeCacheForEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName +{ + for (RKEntityByAttributeCache *cache in self.attributeCaches) { + if ([cache.entity isEqual:entity] && [cache.attribute isEqualToString:attributeName]) { + return cache; + } + } + + return nil; +} + +- (NSSet *)attributeCachesForEntity:(NSEntityDescription *)entity +{ + NSMutableSet *set = [NSMutableSet set]; + for (RKEntityByAttributeCache *cache in self.attributeCaches) { + if ([cache.entity isEqual:entity]) { + [set addObject:cache]; + } + } + + return [NSSet setWithSet:set]; +} + +- (void)flush +{ + [self.attributeCaches makeObjectsPerformSelector:@selector(flush)]; +} + +- (void)addObject:(NSManagedObject *)object +{ + NSSet *attributeCaches = [self attributeCachesForEntity:object.entity]; + for (RKEntityByAttributeCache *cache in attributeCaches) { + [cache addObject:object]; + } +} + +- (void)removeObject:(NSManagedObject *)object +{ + NSSet *attributeCaches = [self attributeCachesForEntity:object.entity]; + for (RKEntityByAttributeCache *cache in attributeCaches) { + [cache removeObject:object]; + } +} + +@end diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.h b/Code/CoreData/RKInMemoryManagedObjectCache.h index bd4f5730..4ae4642b 100644 --- a/Code/CoreData/RKInMemoryManagedObjectCache.h +++ b/Code/CoreData/RKInMemoryManagedObjectCache.h @@ -7,7 +7,6 @@ // #import "RKManagedObjectCaching.h" -#import "RKInMemoryEntityCache.h" /** Provides a fast managed object cache where-in object instances are retained in diff --git a/Code/CoreData/RKInMemoryManagedObjectCache.m b/Code/CoreData/RKInMemoryManagedObjectCache.m index 794dae3f..ab380b18 100644 --- a/Code/CoreData/RKInMemoryManagedObjectCache.m +++ b/Code/CoreData/RKInMemoryManagedObjectCache.m @@ -7,6 +7,7 @@ // #import "RKInMemoryManagedObjectCache.h" +#import "RKEntityCache.h" #import "RKLog.h" // Set Logging Component @@ -30,14 +31,23 @@ static NSString * const RKInMemoryObjectManagedObjectCacheThreadDictionaryKey = contextDictionary = [NSMutableDictionary dictionaryWithCapacity:1]; [[[NSThread currentThread] threadDictionary] setObject:contextDictionary forKey:RKInMemoryObjectManagedObjectCacheThreadDictionaryKey]; } - NSNumber *hashNumber = [NSNumber numberWithUnsignedInteger:[managedObjectContext hash]]; - RKInMemoryEntityCache *cache = [contextDictionary objectForKey:hashNumber]; - if (! cache) { - cache = [[RKInMemoryEntityCache alloc] initWithManagedObjectContext:managedObjectContext]; - [contextDictionary setObject:cache forKey:hashNumber]; - [cache release]; + NSNumber *hashNumber = [NSNumber numberWithUnsignedInteger:[managedObjectContext hash]]; + RKEntityCache *entityCache = [contextDictionary objectForKey:hashNumber]; + if (! entityCache) { + RKLogInfo(@"Creating thread-local entity cache for managed object context: %@", managedObjectContext); + entityCache = [[RKEntityCache alloc] initWithManagedObjectContext:managedObjectContext]; + [contextDictionary setObject:entityCache forKey:hashNumber]; + [entityCache release]; } - return [cache cachedObjectForEntity:entity withAttribute:primaryKeyAttribute value:primaryKeyValue inContext:managedObjectContext]; + + if (! [entityCache isEntity:entity cachedByAttribute:primaryKeyAttribute]) { + RKLogInfo(@"Cacheing instances of Entity '%@' by attribute '%@'", entity.name, primaryKeyAttribute); + [entityCache cacheObjectsForEntity:entity byAttribute:primaryKeyAttribute]; + RKEntityByAttributeCache *attributeCache = [entityCache attributeCacheForEntity:entity attribute:primaryKeyAttribute]; + RKLogTrace(@"Cached %d objects", [attributeCache count]); + } + + return [entityCache objectForEntity:entity withAttribute:primaryKeyAttribute value:primaryKeyValue]; } @end diff --git a/Code/CoreData/RKManagedObjectCaching.h b/Code/CoreData/RKManagedObjectCaching.h index 0099f7cf..d791b3f7 100644 --- a/Code/CoreData/RKManagedObjectCaching.h +++ b/Code/CoreData/RKManagedObjectCaching.h @@ -16,6 +16,8 @@ */ @protocol RKManagedObjectCaching +@required + /** Retrieves a model object from the object store given a Core Data entity and the primary key attribute and value for the desired object. @@ -32,4 +34,10 @@ value:(id)primaryKeyValue inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; +@optional + +- (void)didFetchObject:(NSManagedObject *)object; +- (void)didCreateObject:(NSManagedObject *)object; +- (void)didDeleteObject:(NSManagedObject *)object; + @end diff --git a/Code/CoreData/RKManagedObjectMapping.m b/Code/CoreData/RKManagedObjectMapping.m index a9d675ef..3ab535f3 100644 --- a/Code/CoreData/RKManagedObjectMapping.m +++ b/Code/CoreData/RKManagedObjectMapping.m @@ -167,7 +167,7 @@ } // If we have found the primary key attribute & value, try to find an existing instance to update - if (primaryKeyAttribute && primaryKeyValue) { + if (primaryKeyAttribute && primaryKeyValue && NO == [primaryKeyValue isEqual:[NSNull null]]) { object = [self.objectStore.cacheStrategy findInstanceOfEntity:entity withPrimaryKeyAttribute:self.primaryKeyAttribute value:primaryKeyValue inManagedObjectContext:[self.objectStore managedObjectContextForCurrentThread]]; } diff --git a/Gemfile b/Gemfile index 3a50ce11..c9a1e3ea 100644 --- a/Gemfile +++ b/Gemfile @@ -7,3 +7,4 @@ gem "thin", "~> 1.3.1" gem 'xcoder', :git => "git://github.com/rayh/xcoder.git" gem 'restkit', :git => 'git://github.com/RestKit/RestKit-Gem.git' gem 'ruby-debug19' +gem 'faker', '1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 822082e8..0edf6d26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -32,6 +32,9 @@ GEM columnize (0.3.6) daemons (1.1.4) eventmachine (0.12.10) + faker (1.0.1) + i18n (~> 0.4) + i18n (0.6.0) json (1.6.6) linecache19 (0.5.12) ruby_core_source (>= 0.1.4) @@ -65,6 +68,7 @@ PLATFORMS DEPENDENCIES bundler (~> 1.1.0) + faker (= 1.0.1) rake (~> 0.9.0) restkit! ruby-debug19 diff --git a/Rakefile b/Rakefile index b942a451..6a439673 100644 --- a/Rakefile +++ b/Rakefile @@ -181,3 +181,25 @@ desc "Validate a branch is ready for merging by checking for common issues" task :validate => [:build, 'docs:check', 'uispec:all'] do puts "Project state validated successfully. Proceed with merge." end + +namespace :payload do + task :generate do + require 'json' + require 'faker' + + ids = (1..25).to_a + child_ids = (50..100).to_a + child_counts = (10..25).to_a + hash = ids.inject({'parents' => []}) do |hash, parent_id| + child_count = child_counts.sample + children = (0..child_count).collect do + {'name' => Faker::Name.name, 'childID' => child_ids.sample} + end + parent = {'parentID' => parent_id, 'name' => Faker::Name.name, 'children' => children} + hash['parents'] << parent + hash + end + File.open('payload.json', 'w+') { |f| f << hash.to_json } + puts "Generated payload at: payload.json" + end +end diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index 67d01375..1d5cdecf 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -523,10 +523,24 @@ 259C3027151280A1003066A2 /* grayArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE314F8022600C3CF3F /* grayArrow@2x.png */; }; 259C3028151280A1003066A2 /* whiteArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE414F8022600C3CF3F /* whiteArrow.png */; }; 259C3029151280A1003066A2 /* whiteArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 25EC1AE514F8022600C3CF3F /* whiteArrow@2x.png */; }; + 259D983C154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */ = {isa = PBXBuildFile; fileRef = 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */; }; + 259D983D154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */ = {isa = PBXBuildFile; fileRef = 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */; }; 259D9847154F8744008C90F5 /* RKBenchmark.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D9845154F8744008C90F5 /* RKBenchmark.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259D9848154F8744008C90F5 /* RKBenchmark.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D9845154F8744008C90F5 /* RKBenchmark.h */; settings = {ATTRIBUTES = (Public, ); }; }; 259D9849154F8744008C90F5 /* RKBenchmark.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D9846154F8744008C90F5 /* RKBenchmark.m */; }; 259D984A154F8744008C90F5 /* RKBenchmark.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D9846154F8744008C90F5 /* RKBenchmark.m */; }; + 259D98541550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */; }; + 259D98551550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */; }; + 259D98561550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */; }; + 259D98571550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */; }; + 259D985A1550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98591550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m */; }; + 259D985B1550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D98591550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m */; }; + 259D985E155218E5008C90F5 /* RKEntityCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D985C155218E4008C90F5 /* RKEntityCache.h */; }; + 259D985F155218E5008C90F5 /* RKEntityCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 259D985C155218E4008C90F5 /* RKEntityCache.h */; }; + 259D9860155218E5008C90F5 /* RKEntityCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D985D155218E4008C90F5 /* RKEntityCache.m */; }; + 259D9861155218E5008C90F5 /* RKEntityCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D985D155218E4008C90F5 /* RKEntityCache.m */; }; + 259D986415521B20008C90F5 /* RKEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */; }; + 259D986515521B20008C90F5 /* RKEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */; }; 25A2476E153E667E003240B6 /* RKCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A2476D153E667E003240B6 /* RKCacheTest.m */; }; 25A2476F153E667E003240B6 /* RKCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A2476D153E667E003240B6 /* RKCacheTest.m */; }; 25A34245147D8AAA0009758D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25A34244147D8AAA0009758D /* Security.framework */; }; @@ -1049,8 +1063,16 @@ 257ABAB41511371C00CCAA76 /* NSManagedObject+RKAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+RKAdditions.h"; sourceTree = ""; }; 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+RKAdditions.m"; sourceTree = ""; }; 259C301615128079003066A2 /* RestKitResources.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RestKitResources.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = benchmark_parents_and_children.json; sourceTree = ""; }; 259D9845154F8744008C90F5 /* RKBenchmark.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RKBenchmark.h; path = Testing/RKBenchmark.h; sourceTree = ""; }; 259D9846154F8744008C90F5 /* RKBenchmark.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RKBenchmark.m; path = Testing/RKBenchmark.m; sourceTree = ""; }; + 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKEntityByAttributeCache.h; sourceTree = ""; }; + 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityByAttributeCache.m; sourceTree = ""; }; + 259D98581550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKInMemoryEntityAttributeCacheTest.h; sourceTree = ""; }; + 259D98591550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKInMemoryEntityAttributeCacheTest.m; sourceTree = ""; }; + 259D985C155218E4008C90F5 /* RKEntityCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKEntityCache.h; sourceTree = ""; }; + 259D985D155218E4008C90F5 /* RKEntityCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityCache.m; sourceTree = ""; }; + 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityCacheTest.m; sourceTree = ""; }; 25A2476D153E667E003240B6 /* RKCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKCacheTest.m; sourceTree = ""; }; 25A34244147D8AAA0009758D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = SDKs/MacOSX10.7.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; 25B408241491CDDB00F21111 /* RKDirectory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKDirectory.h; sourceTree = ""; }; @@ -1381,6 +1403,10 @@ 257ABAB51511371D00CCAA76 /* NSManagedObject+RKAdditions.m */, 25079C6D151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h */, 25079C6E151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m */, + 259D98521550C69A008C90F5 /* RKEntityByAttributeCache.h */, + 259D98531550C69A008C90F5 /* RKEntityByAttributeCache.m */, + 259D985C155218E4008C90F5 /* RKEntityCache.h */, + 259D985D155218E4008C90F5 /* RKEntityCache.m */, ); path = CoreData; sourceTree = ""; @@ -1685,6 +1711,9 @@ 25E36E0115195CED00F9E448 /* RKFetchRequestMappingCacheTest.m */, 25079C75151B952200266AE7 /* NSEntityDescription+RKAdditionsTest.m */, 25DB7507151BD551009F01AF /* NSManagedObject+ActiveRecordTest.m */, + 259D98581550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.h */, + 259D98591550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m */, + 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */, ); name = CoreData; path = Logic/CoreData; @@ -1712,6 +1741,7 @@ 25160FD01456F2330060A5C5 /* JSON */ = { isa = PBXGroup; children = ( + 259D983B154F6C90008C90F5 /* benchmark_parents_and_children.json */, 252EFB2714DA0689004863C8 /* NakedEvents.json */, 25160FD11456F2330060A5C5 /* ArrayOfNestedDictionaries.json */, 25160FD21456F2330060A5C5 /* ArrayOfResults.json */, @@ -2234,6 +2264,8 @@ 25079C6F151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h in Headers */, 252A202D153471380078F8AD /* NSArray+RKAdditions.h in Headers */, 259D9847154F8744008C90F5 /* RKBenchmark.h in Headers */, + 259D98541550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */, + 259D985E155218E5008C90F5 /* RKEntityCache.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2356,6 +2388,8 @@ 25079C70151B93DB00266AE7 /* NSEntityDescription+RKAdditions.h in Headers */, 252A20311534714D0078F8AD /* NSArray+RKAdditions.h in Headers */, 259D9848154F8744008C90F5 /* RKBenchmark.h in Headers */, + 259D98551550C69A008C90F5 /* RKEntityByAttributeCache.h in Headers */, + 259D985F155218E5008C90F5 /* RKEntityCache.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2526,6 +2560,7 @@ 252EFB2814DA0689004863C8 /* NakedEvents.json in Resources */, 25CAAA9415254E7800CAE5D7 /* ArrayOfHumans.json in Resources */, 25119FB6154A34B400C6BC58 /* parents_and_children.json in Resources */, + 259D983C154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2587,6 +2622,7 @@ 252EFB2914DA0689004863C8 /* NakedEvents.json in Resources */, 25CAAA9515254E7800CAE5D7 /* ArrayOfHumans.json in Resources */, 25119FB7154A34B400C6BC58 /* parents_and_children.json in Resources */, + 259D983D154F6C90008C90F5 /* benchmark_parents_and_children.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2759,6 +2795,8 @@ 25079C71151B93DB00266AE7 /* NSEntityDescription+RKAdditions.m in Sources */, 252A202E153471380078F8AD /* NSArray+RKAdditions.m in Sources */, 259D9849154F8744008C90F5 /* RKBenchmark.m in Sources */, + 259D98561550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */, + 259D9860155218E5008C90F5 /* RKEntityCache.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2845,6 +2883,8 @@ 252A2034153477870078F8AD /* NSArray+RKAdditionsTest.m in Sources */, 2501405315366000004E0466 /* RKObjectiveCppTest.mm in Sources */, 25A2476E153E667E003240B6 /* RKCacheTest.m in Sources */, + 259D985A1550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m in Sources */, + 259D986415521B20008C90F5 /* RKEntityCacheTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2944,6 +2984,8 @@ 250B849E152B6F63002581F9 /* RKObjectMappingProvider+CoreData.m in Sources */, 252A2030153471470078F8AD /* NSArray+RKAdditions.m in Sources */, 259D984A154F8744008C90F5 /* RKBenchmark.m in Sources */, + 259D98571550C69A008C90F5 /* RKEntityByAttributeCache.m in Sources */, + 259D9861155218E5008C90F5 /* RKEntityCache.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3030,6 +3072,8 @@ 252A2035153477870078F8AD /* NSArray+RKAdditionsTest.m in Sources */, 2501405415366000004E0466 /* RKObjectiveCppTest.mm in Sources */, 25A2476F153E667E003240B6 /* RKCacheTest.m in Sources */, + 259D985B1550C6BE008C90F5 /* RKInMemoryEntityAttributeCacheTest.m in Sources */, + 259D986515521B20008C90F5 /* RKEntityCacheTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests/Fixtures/JSON/benchmark_parents_and_children.json b/Tests/Fixtures/JSON/benchmark_parents_and_children.json new file mode 100644 index 00000000..ba802542 --- /dev/null +++ b/Tests/Fixtures/JSON/benchmark_parents_and_children.json @@ -0,0 +1,2062 @@ +{ + "parents": [ + { + "parentID": 1, + "name": "Megane Rosenbaum", + "children": [ + { + "name": "Miss Jazmin Dooley", + "childID": 73 + }, + { + "name": "Llewellyn Hilll", + "childID": 60 + }, + { + "name": "Adan Vandervort", + "childID": 66 + }, + { + "name": "Joshuah Langworth III", + "childID": 81 + }, + { + "name": "Yazmin Christiansen PhD", + "childID": 80 + }, + { + "name": "Ms. Alan Carter", + "childID": 64 + }, + { + "name": "Mrs. Sally Dickens", + "childID": 79 + }, + { + "name": "Ms. Norval D'Amore", + "childID": 72 + }, + { + "name": "Joan Crooks", + "childID": 59 + }, + { + "name": "Tiara Rolfson", + "childID": 84 + }, + { + "name": "Jacinthe Abernathy", + "childID": 77 + }, + { + "name": "Beaulah Tremblay", + "childID": 77 + }, + { + "name": "Fermin Douglas", + "childID": 56 + }, + { + "name": "Kole Ullrich", + "childID": 95 + }, + { + "name": "Miss Pete Gutkowski", + "childID": 65 + }, + { + "name": "Kamren Kozey", + "childID": 52 + }, + { + "name": "Vladimir Hegmann", + "childID": 84 + }, + { + "name": "Miss Dannie Grant", + "childID": 77 + }, + { + "name": "Miss Jayde Rice", + "childID": 59 + }, + { + "name": "Otilia Hartmann", + "childID": 74 + }, + { + "name": "Donnell Sanford", + "childID": 73 + }, + { + "name": "Bill Hackett", + "childID": 94 + }, + { + "name": "Bulah Sauer IV", + "childID": 70 + }, + { + "name": "Baylee Rippin", + "childID": 58 + } + ] + }, + { + "parentID": 2, + "name": "Brice Maggio", + "children": [ + { + "name": "Skye Gerhold", + "childID": 53 + }, + { + "name": "Leonie Price Sr.", + "childID": 86 + }, + { + "name": "Eulah Douglas", + "childID": 100 + }, + { + "name": "Miss Rosina Waters", + "childID": 55 + }, + { + "name": "Verda Krajcik", + "childID": 96 + }, + { + "name": "Daren Crist", + "childID": 100 + }, + { + "name": "Dock Gibson", + "childID": 99 + }, + { + "name": "Ashlynn Marquardt", + "childID": 84 + }, + { + "name": "Krystal Swaniawski", + "childID": 78 + }, + { + "name": "Sadie Wyman", + "childID": 57 + }, + { + "name": "Breanne Denesik", + "childID": 77 + }, + { + "name": "Miss Bryce Lockman", + "childID": 96 + }, + { + "name": "Jewel Hickle", + "childID": 50 + }, + { + "name": "Cordell Little", + "childID": 53 + }, + { + "name": "Ollie Balistreri", + "childID": 99 + }, + { + "name": "Delfina Wehner", + "childID": 51 + }, + { + "name": "Carmela Effertz", + "childID": 99 + }, + { + "name": "Devon Nolan", + "childID": 96 + }, + { + "name": "Miss Sandrine Dicki", + "childID": 96 + }, + { + "name": "Anderson Collier Jr.", + "childID": 65 + }, + { + "name": "Dr. Cielo Kozey", + "childID": 66 + }, + { + "name": "Tabitha Cummings", + "childID": 92 + }, + { + "name": "Amelia Goldner", + "childID": 60 + }, + { + "name": "Arielle Welch IV", + "childID": 69 + }, + { + "name": "Hallie Friesen", + "childID": 83 + }, + { + "name": "Halie Von", + "childID": 51 + } + ] + }, + { + "parentID": 3, + "name": "Mrs. Marlene Hayes", + "children": [ + { + "name": "Holden Fay", + "childID": 75 + }, + { + "name": "Giovanna Bayer", + "childID": 99 + }, + { + "name": "Olaf McClure", + "childID": 73 + }, + { + "name": "Krista McCullough", + "childID": 75 + }, + { + "name": "Monserrate Larkin", + "childID": 96 + }, + { + "name": "Alice Ledner", + "childID": 73 + }, + { + "name": "Marina Stanton Jr.", + "childID": 79 + }, + { + "name": "William Hills", + "childID": 93 + }, + { + "name": "Neal Vandervort", + "childID": 54 + }, + { + "name": "Susana Erdman", + "childID": 84 + }, + { + "name": "Dudley Blick", + "childID": 55 + }, + { + "name": "Mrs. Triston Langosh", + "childID": 100 + }, + { + "name": "Loma Kreiger", + "childID": 72 + }, + { + "name": "Ida Crona", + "childID": 80 + }, + { + "name": "Allison Pouros", + "childID": 98 + }, + { + "name": "Frederique Hansen", + "childID": 100 + }, + { + "name": "Fred Shields", + "childID": 92 + }, + { + "name": "Mr. Jaylon Lebsack", + "childID": 82 + }, + { + "name": "Noah Crist", + "childID": 90 + }, + { + "name": "Shakira Witting", + "childID": 57 + }, + { + "name": "Bradford Kunde", + "childID": 65 + } + ] + }, + { + "parentID": 4, + "name": "Ms. Sallie Keeling", + "children": [ + { + "name": "Dr. Daija Waelchi", + "childID": 57 + }, + { + "name": "Halle Wilkinson", + "childID": 78 + }, + { + "name": "Jennyfer Mante", + "childID": 75 + }, + { + "name": "Hilda Lebsack", + "childID": 50 + }, + { + "name": "Sibyl Jast", + "childID": 58 + }, + { + "name": "Maybell Ebert DVM", + "childID": 73 + }, + { + "name": "Dandre Reynolds", + "childID": 69 + }, + { + "name": "Roberto Stehr", + "childID": 60 + }, + { + "name": "Amie Rowe DVM", + "childID": 95 + }, + { + "name": "Edgardo Blanda", + "childID": 61 + }, + { + "name": "Hector Prohaska PhD", + "childID": 63 + }, + { + "name": "Alycia Schultz", + "childID": 65 + }, + { + "name": "Mr. Joannie Bauch", + "childID": 57 + }, + { + "name": "Dr. Hassan Mosciski", + "childID": 51 + } + ] + }, + { + "parentID": 5, + "name": "Dr. Elissa Bernier", + "children": [ + { + "name": "Vernie Gusikowski", + "childID": 53 + }, + { + "name": "Gennaro Wisoky", + "childID": 87 + }, + { + "name": "Preston Luettgen", + "childID": 71 + }, + { + "name": "Ada Hammes", + "childID": 68 + }, + { + "name": "Ms. Linnea Gottlieb", + "childID": 50 + }, + { + "name": "Mavis Wiegand III", + "childID": 56 + }, + { + "name": "Nathan Stroman I", + "childID": 87 + }, + { + "name": "Mr. Lilly Lemke", + "childID": 55 + }, + { + "name": "Jermain Tremblay", + "childID": 92 + }, + { + "name": "Jamison Shanahan", + "childID": 72 + }, + { + "name": "Evans Considine", + "childID": 70 + }, + { + "name": "Lorine Carter", + "childID": 50 + }, + { + "name": "Ms. Jacey Pouros", + "childID": 65 + }, + { + "name": "Rasheed Schroeder", + "childID": 77 + }, + { + "name": "Elinore Dickinson", + "childID": 53 + }, + { + "name": "Emelie Wolff V", + "childID": 62 + }, + { + "name": "Maria Grant", + "childID": 97 + }, + { + "name": "Ms. Serenity Yundt", + "childID": 69 + }, + { + "name": "Cleta Mosciski", + "childID": 76 + }, + { + "name": "Fausto Ortiz", + "childID": 86 + }, + { + "name": "Monty Metz", + "childID": 89 + } + ] + }, + { + "parentID": 6, + "name": "Jewell Kihn", + "children": [ + { + "name": "Rasheed Altenwerth", + "childID": 58 + }, + { + "name": "Theron Medhurst", + "childID": 56 + }, + { + "name": "Etha Bins", + "childID": 53 + }, + { + "name": "Caroline Dare", + "childID": 89 + }, + { + "name": "Bennett Tremblay", + "childID": 67 + }, + { + "name": "Jailyn Steuber MD", + "childID": 95 + }, + { + "name": "Pamela Effertz", + "childID": 58 + }, + { + "name": "Blair Jacobs", + "childID": 71 + }, + { + "name": "Anna Stoltenberg DVM", + "childID": 70 + }, + { + "name": "Jerel Halvorson", + "childID": 78 + }, + { + "name": "Elroy Littel", + "childID": 75 + }, + { + "name": "Sheridan Romaguera", + "childID": 82 + }, + { + "name": "Dr. Jayce Wisozk", + "childID": 70 + }, + { + "name": "Elmore Barrows", + "childID": 52 + }, + { + "name": "Kylee Ruecker", + "childID": 84 + }, + { + "name": "Pierre Koss", + "childID": 60 + }, + { + "name": "Allene Fisher", + "childID": 56 + }, + { + "name": "Marcellus Conroy III", + "childID": 79 + }, + { + "name": "Kaylie Daniel IV", + "childID": 51 + }, + { + "name": "Jason Anderson", + "childID": 71 + }, + { + "name": "Pedro Wolf", + "childID": 93 + }, + { + "name": "Broderick Kunde", + "childID": 66 + } + ] + }, + { + "parentID": 7, + "name": "Armando Hammes", + "children": [ + { + "name": "Aleen Sanford", + "childID": 97 + }, + { + "name": "Ofelia Wolff", + "childID": 87 + }, + { + "name": "Deron Goodwin", + "childID": 58 + }, + { + "name": "Natalie Volkman", + "childID": 63 + }, + { + "name": "Vanessa Hudson", + "childID": 78 + }, + { + "name": "Jamaal Feest", + "childID": 86 + }, + { + "name": "Miss Marilou Kessler", + "childID": 98 + }, + { + "name": "Rosa Armstrong", + "childID": 65 + }, + { + "name": "Ms. Claudie Ankunding", + "childID": 85 + }, + { + "name": "Franco Hamill", + "childID": 78 + }, + { + "name": "Kristin Nicolas", + "childID": 100 + } + ] + }, + { + "parentID": 8, + "name": "Hilbert West", + "children": [ + { + "name": "Eryn Steuber", + "childID": 70 + }, + { + "name": "Arlo Kling", + "childID": 70 + }, + { + "name": "Deondre Feil", + "childID": 98 + }, + { + "name": "Demarco Welch", + "childID": 86 + }, + { + "name": "Dr. Dallin Monahan", + "childID": 70 + }, + { + "name": "Blanche Jenkins", + "childID": 71 + }, + { + "name": "Emmalee Spinka MD", + "childID": 98 + }, + { + "name": "Rahul Stark", + "childID": 80 + }, + { + "name": "Yasmeen Lynch", + "childID": 90 + }, + { + "name": "Margret Auer", + "childID": 52 + }, + { + "name": "Mrs. Velda Nitzsche", + "childID": 73 + }, + { + "name": "Madaline Gusikowski", + "childID": 58 + }, + { + "name": "Kyleigh Price", + "childID": 63 + }, + { + "name": "Maia Powlowski", + "childID": 88 + }, + { + "name": "Estrella Dare", + "childID": 86 + }, + { + "name": "Lizzie Christiansen", + "childID": 57 + }, + { + "name": "Enola Purdy", + "childID": 77 + }, + { + "name": "Marcus Bosco", + "childID": 75 + }, + { + "name": "Estevan Ondricka III", + "childID": 87 + }, + { + "name": "Anissa Krajcik V", + "childID": 91 + }, + { + "name": "Malika Hamill", + "childID": 56 + }, + { + "name": "Natalia Sipes", + "childID": 59 + } + ] + }, + { + "parentID": 9, + "name": "Shanel Douglas I", + "children": [ + { + "name": "Zita Schamberger", + "childID": 50 + }, + { + "name": "Emerson Williamson", + "childID": 61 + }, + { + "name": "Macie Hoppe", + "childID": 74 + }, + { + "name": "Estell Champlin", + "childID": 73 + }, + { + "name": "Mr. Lelah Hodkiewicz", + "childID": 91 + }, + { + "name": "Cole Baumbach", + "childID": 83 + }, + { + "name": "Izabella Kovacek", + "childID": 59 + }, + { + "name": "Katelyn Abshire", + "childID": 80 + }, + { + "name": "Michele Torp", + "childID": 99 + }, + { + "name": "Dessie Murray", + "childID": 68 + }, + { + "name": "Noah Lesch", + "childID": 76 + }, + { + "name": "Jailyn Heaney", + "childID": 59 + }, + { + "name": "Manuel Dach", + "childID": 81 + }, + { + "name": "Brent Wolff DVM", + "childID": 100 + }, + { + "name": "Marcellus Moen", + "childID": 90 + }, + { + "name": "Mrs. Wilfredo Sawayn", + "childID": 51 + }, + { + "name": "Gideon Hilpert", + "childID": 54 + }, + { + "name": "Davonte DuBuque MD", + "childID": 96 + }, + { + "name": "Vivienne Langosh", + "childID": 52 + }, + { + "name": "Mozelle Hagenes", + "childID": 88 + } + ] + }, + { + "parentID": 10, + "name": "Koby Kuhic", + "children": [ + { + "name": "Jules Lueilwitz", + "childID": 70 + }, + { + "name": "Buford Bergstrom", + "childID": 100 + }, + { + "name": "Miss Gustave Murphy", + "childID": 71 + }, + { + "name": "Rudy Sanford Sr.", + "childID": 77 + }, + { + "name": "Mrs. Eloisa O'Kon", + "childID": 64 + }, + { + "name": "Elmore Wolf", + "childID": 93 + }, + { + "name": "Mrs. Beverly Farrell", + "childID": 86 + }, + { + "name": "Terry Cruickshank", + "childID": 80 + }, + { + "name": "Susie Ritchie", + "childID": 53 + }, + { + "name": "Andrew Cole", + "childID": 50 + }, + { + "name": "Arlo Prohaska", + "childID": 52 + }, + { + "name": "Jayne Wyman DVM", + "childID": 69 + } + ] + }, + { + "parentID": 11, + "name": "Miss Jovan Ebert", + "children": [ + { + "name": "Jackeline Mertz", + "childID": 93 + }, + { + "name": "Adriana Larkin", + "childID": 80 + }, + { + "name": "Ottilie Vandervort", + "childID": 56 + }, + { + "name": "Ericka Gibson V", + "childID": 52 + }, + { + "name": "Chester Turner", + "childID": 75 + }, + { + "name": "Delfina Anderson", + "childID": 56 + }, + { + "name": "Bulah White", + "childID": 53 + }, + { + "name": "Michel Bergnaum", + "childID": 53 + }, + { + "name": "Mr. Hettie Lesch", + "childID": 51 + }, + { + "name": "Alexanne Muller", + "childID": 96 + }, + { + "name": "Regan Heaney", + "childID": 96 + }, + { + "name": "Dane Stracke V", + "childID": 94 + }, + { + "name": "Macie Kilback", + "childID": 99 + }, + { + "name": "Albertha Considine", + "childID": 97 + }, + { + "name": "Winifred Littel", + "childID": 90 + }, + { + "name": "Kara Wuckert", + "childID": 92 + } + ] + }, + { + "parentID": 12, + "name": "Chelsey Zieme", + "children": [ + { + "name": "Damian Schaden", + "childID": 88 + }, + { + "name": "Ima O'Keefe", + "childID": 78 + }, + { + "name": "Henriette Berge MD", + "childID": 67 + }, + { + "name": "Paolo Wiegand", + "childID": 92 + }, + { + "name": "Wilber Lesch", + "childID": 52 + }, + { + "name": "Dale Greenholt", + "childID": 64 + }, + { + "name": "Aliya Leffler Jr.", + "childID": 50 + }, + { + "name": "Kasandra Carroll", + "childID": 64 + }, + { + "name": "Daren Upton DDS", + "childID": 99 + }, + { + "name": "Chase Heathcote", + "childID": 73 + }, + { + "name": "Lamont Wolf", + "childID": 88 + } + ] + }, + { + "parentID": 13, + "name": "Pearl Roob DDS", + "children": [ + { + "name": "Roderick Mohr", + "childID": 63 + }, + { + "name": "Rolando Becker", + "childID": 78 + }, + { + "name": "Mr. Karlie Brakus", + "childID": 55 + }, + { + "name": "Annetta Spinka", + "childID": 92 + }, + { + "name": "Nathan Walker", + "childID": 89 + }, + { + "name": "Miss Desiree Jacobs", + "childID": 63 + }, + { + "name": "Laney Corwin", + "childID": 98 + }, + { + "name": "Chanel Cole", + "childID": 58 + }, + { + "name": "Nia Prohaska", + "childID": 76 + }, + { + "name": "Rogelio Stamm", + "childID": 91 + }, + { + "name": "Abby Harris", + "childID": 97 + }, + { + "name": "Monserrate Mraz", + "childID": 65 + }, + { + "name": "Esta Harvey", + "childID": 54 + }, + { + "name": "Alejandrin Keeling", + "childID": 87 + }, + { + "name": "Lue Considine", + "childID": 65 + }, + { + "name": "Raphaelle Ward", + "childID": 56 + }, + { + "name": "Cordie Rice Jr.", + "childID": 98 + }, + { + "name": "Fabiola Rohan", + "childID": 99 + }, + { + "name": "Katrina Stroman", + "childID": 84 + }, + { + "name": "Abbigail Crist", + "childID": 53 + }, + { + "name": "Gregg Robel", + "childID": 64 + }, + { + "name": "Margret Hintz", + "childID": 54 + }, + { + "name": "Jody Lowe", + "childID": 98 + }, + { + "name": "Mr. Nya Hessel", + "childID": 53 + }, + { + "name": "Malachi Schoen Jr.", + "childID": 80 + }, + { + "name": "Gwendolyn Witting", + "childID": 77 + } + ] + }, + { + "parentID": 14, + "name": "Ms. Kris Murazik", + "children": [ + { + "name": "Dr. Murphy Bailey", + "childID": 78 + }, + { + "name": "Jennyfer Ankunding", + "childID": 88 + }, + { + "name": "Demetrius Kerluke", + "childID": 71 + }, + { + "name": "Gina Metz", + "childID": 57 + }, + { + "name": "Miss Durward Hettinger", + "childID": 100 + }, + { + "name": "Jameson Kunze IV", + "childID": 67 + }, + { + "name": "Henri Denesik MD", + "childID": 64 + }, + { + "name": "Oleta Bartell V", + "childID": 93 + }, + { + "name": "Adrianna Hegmann", + "childID": 54 + }, + { + "name": "Geo Turner II", + "childID": 78 + }, + { + "name": "Darius White", + "childID": 97 + }, + { + "name": "Vella Shanahan", + "childID": 64 + }, + { + "name": "Richie Ratke", + "childID": 83 + } + ] + }, + { + "parentID": 15, + "name": "Wilmer Boyle", + "children": [ + { + "name": "Alisha Ritchie DVM", + "childID": 59 + }, + { + "name": "Ari Dietrich", + "childID": 56 + }, + { + "name": "Catalina Roob", + "childID": 95 + }, + { + "name": "Will Hansen PhD", + "childID": 50 + }, + { + "name": "Stanton Howe", + "childID": 57 + }, + { + "name": "Pat Zemlak", + "childID": 58 + }, + { + "name": "Jazmyne Bartoletti MD", + "childID": 95 + }, + { + "name": "Idell Rowe", + "childID": 75 + }, + { + "name": "Sarina Nicolas", + "childID": 51 + }, + { + "name": "Dr. Anthony McCullough", + "childID": 88 + }, + { + "name": "Lenna Ritchie", + "childID": 74 + }, + { + "name": "Jerald Blick", + "childID": 60 + }, + { + "name": "Destini Pfeffer", + "childID": 95 + }, + { + "name": "Marianna Okuneva", + "childID": 69 + }, + { + "name": "Roosevelt Schiller Jr.", + "childID": 58 + }, + { + "name": "Rosemary Batz Sr.", + "childID": 75 + }, + { + "name": "Joy Bahringer", + "childID": 81 + }, + { + "name": "Reuben Bogan II", + "childID": 86 + }, + { + "name": "Ms. Anabelle Leuschke", + "childID": 59 + }, + { + "name": "Muhammad Toy Sr.", + "childID": 66 + }, + { + "name": "Jarret Keeling", + "childID": 50 + }, + { + "name": "Ms. Isabell Gusikowski", + "childID": 75 + }, + { + "name": "Grover Stroman", + "childID": 65 + }, + { + "name": "Mrs. Ole Bahringer", + "childID": 94 + } + ] + }, + { + "parentID": 16, + "name": "Suzanne Von", + "children": [ + { + "name": "Maud Bode", + "childID": 96 + }, + { + "name": "Vita Koelpin MD", + "childID": 77 + }, + { + "name": "Diana Stamm", + "childID": 57 + }, + { + "name": "Belle O'Conner", + "childID": 52 + }, + { + "name": "Mr. Cheyanne Huel", + "childID": 79 + }, + { + "name": "Francesca Reilly III", + "childID": 62 + }, + { + "name": "Isadore Reichert", + "childID": 83 + }, + { + "name": "Miguel Kuhlman", + "childID": 90 + }, + { + "name": "Miss Rasheed Kunze", + "childID": 100 + }, + { + "name": "King Casper", + "childID": 57 + }, + { + "name": "Daphnee Herzog", + "childID": 81 + }, + { + "name": "Rose Abbott DDS", + "childID": 94 + }, + { + "name": "Mr. Fae Gulgowski", + "childID": 63 + }, + { + "name": "Marlen Huel", + "childID": 77 + }, + { + "name": "Jalyn Jaskolski", + "childID": 60 + }, + { + "name": "Ben Bauch", + "childID": 54 + }, + { + "name": "Khalid Parker", + "childID": 64 + }, + { + "name": "Walter Gutkowski", + "childID": 52 + }, + { + "name": "Cecelia Rodriguez III", + "childID": 91 + }, + { + "name": "Adam Grant", + "childID": 61 + } + ] + }, + { + "parentID": 17, + "name": "Jerrod Cruickshank", + "children": [ + { + "name": "Tavares Friesen", + "childID": 91 + }, + { + "name": "Moses Brakus", + "childID": 100 + }, + { + "name": "Micah Windler", + "childID": 87 + }, + { + "name": "Zola Murray", + "childID": 53 + }, + { + "name": "Laverne Durgan", + "childID": 72 + }, + { + "name": "Mitchell Bernhard", + "childID": 59 + }, + { + "name": "Lavina Shields", + "childID": 61 + }, + { + "name": "Talon Jerde Jr.", + "childID": 93 + }, + { + "name": "Mr. Maude Lakin", + "childID": 97 + }, + { + "name": "Ms. Ruben Terry", + "childID": 56 + }, + { + "name": "Fredy Purdy", + "childID": 74 + }, + { + "name": "Marie Flatley", + "childID": 96 + }, + { + "name": "Nicolas Moen", + "childID": 58 + }, + { + "name": "August Gutkowski", + "childID": 51 + }, + { + "name": "Frederique Larkin", + "childID": 80 + }, + { + "name": "Kenyon Lowe", + "childID": 65 + }, + { + "name": "Javier Rippin", + "childID": 70 + }, + { + "name": "Levi Olson", + "childID": 67 + }, + { + "name": "Bret Jenkins", + "childID": 62 + }, + { + "name": "Madelyn Ratke", + "childID": 98 + }, + { + "name": "Mrs. Rae Hamill", + "childID": 99 + }, + { + "name": "Ms. Imelda Bailey", + "childID": 58 + } + ] + }, + { + "parentID": 18, + "name": "Emiliano Yost", + "children": [ + { + "name": "Cathrine Prohaska", + "childID": 60 + }, + { + "name": "Gabriel Christiansen", + "childID": 62 + }, + { + "name": "Ima O'Conner", + "childID": 50 + }, + { + "name": "Justine Schmidt", + "childID": 52 + }, + { + "name": "Amelie White", + "childID": 83 + }, + { + "name": "Vivianne Boyer V", + "childID": 100 + }, + { + "name": "Bernice King Sr.", + "childID": 100 + }, + { + "name": "Morton Powlowski", + "childID": 59 + }, + { + "name": "Linnie Wyman", + "childID": 100 + }, + { + "name": "Christop Fisher", + "childID": 99 + }, + { + "name": "Candido Kovacek", + "childID": 60 + }, + { + "name": "Wellington Bernier", + "childID": 56 + }, + { + "name": "Joel Wintheiser", + "childID": 88 + }, + { + "name": "Miles Gorczany", + "childID": 96 + }, + { + "name": "Elijah Kunze", + "childID": 96 + }, + { + "name": "Ruthe Breitenberg", + "childID": 72 + }, + { + "name": "Mrs. Camden Jast", + "childID": 60 + } + ] + }, + { + "parentID": 19, + "name": "Marco Yundt", + "children": [ + { + "name": "Rashawn Hegmann", + "childID": 59 + }, + { + "name": "Obie Hills V", + "childID": 78 + }, + { + "name": "Ms. Alfonzo Runolfsson", + "childID": 86 + }, + { + "name": "Esteban Collins", + "childID": 100 + }, + { + "name": "Verlie Green", + "childID": 99 + }, + { + "name": "Jettie Sanford", + "childID": 59 + }, + { + "name": "Dominic McLaughlin", + "childID": 78 + }, + { + "name": "Morgan Olson", + "childID": 62 + }, + { + "name": "Autumn Feest DVM", + "childID": 90 + }, + { + "name": "Aliyah Kemmer", + "childID": 86 + }, + { + "name": "Bryana Sawayn", + "childID": 100 + }, + { + "name": "Anita Emard II", + "childID": 79 + }, + { + "name": "Mr. Arlo Hudson", + "childID": 100 + }, + { + "name": "Lavern Ziemann", + "childID": 86 + }, + { + "name": "Destany Jewess", + "childID": 78 + }, + { + "name": "Jayson Dickens", + "childID": 94 + }, + { + "name": "Eliane Hudson", + "childID": 77 + }, + { + "name": "Arvid Collier MD", + "childID": 77 + }, + { + "name": "Miss Ena Torp", + "childID": 50 + }, + { + "name": "Mrs. Daphney Renner", + "childID": 57 + }, + { + "name": "Joan Erdman", + "childID": 52 + }, + { + "name": "Pasquale Bosco", + "childID": 83 + }, + { + "name": "Finn Crona", + "childID": 97 + }, + { + "name": "Miss Euna Marvin", + "childID": 51 + } + ] + }, + { + "parentID": 20, + "name": "Laurie Satterfield", + "children": [ + { + "name": "Wilfred Nitzsche", + "childID": 88 + }, + { + "name": "Joany Borer", + "childID": 90 + }, + { + "name": "Nigel Upton", + "childID": 58 + }, + { + "name": "Chesley Beier", + "childID": 56 + }, + { + "name": "Ida Pagac", + "childID": 79 + }, + { + "name": "Keyshawn Sipes", + "childID": 54 + }, + { + "name": "Makayla Cole", + "childID": 82 + }, + { + "name": "Mr. Melvina Rohan", + "childID": 65 + }, + { + "name": "Gregg Metz", + "childID": 91 + }, + { + "name": "Daniella Wintheiser", + "childID": 88 + }, + { + "name": "Greyson Hintz", + "childID": 92 + }, + { + "name": "Issac Hermiston", + "childID": 63 + } + ] + }, + { + "parentID": 21, + "name": "Nicolas Conn MD", + "children": [ + { + "name": "Ms. Vicenta Jerde", + "childID": 56 + }, + { + "name": "Hollis Walter", + "childID": 96 + }, + { + "name": "Miss Zena Stiedemann", + "childID": 83 + }, + { + "name": "Mrs. Katelin Rutherford", + "childID": 90 + }, + { + "name": "Dr. Brenna Roberts", + "childID": 65 + }, + { + "name": "Phyllis O'Conner V", + "childID": 80 + }, + { + "name": "Allie Ankunding", + "childID": 66 + }, + { + "name": "Bridie Cummerata Jr.", + "childID": 57 + }, + { + "name": "Oma Keeling", + "childID": 90 + }, + { + "name": "Horace Davis", + "childID": 77 + }, + { + "name": "Mrs. Marielle Sanford", + "childID": 53 + }, + { + "name": "Mr. Ashlee Connelly", + "childID": 92 + }, + { + "name": "Ms. Fredy Kunde", + "childID": 87 + }, + { + "name": "Emile Auer Jr.", + "childID": 98 + }, + { + "name": "Nikolas Hartmann PhD", + "childID": 84 + } + ] + }, + { + "parentID": 22, + "name": "Matilda Hammes DVM", + "children": [ + { + "name": "Destini Denesik", + "childID": 98 + }, + { + "name": "Ms. Abigayle Christiansen", + "childID": 81 + }, + { + "name": "Annabel Kling PhD", + "childID": 67 + }, + { + "name": "Dr. Kattie Zboncak", + "childID": 74 + }, + { + "name": "Mrs. Sigurd Hettinger", + "childID": 65 + }, + { + "name": "Miss Katheryn O'Connell", + "childID": 92 + }, + { + "name": "Hilario Rosenbaum", + "childID": 55 + }, + { + "name": "Miss Paxton Jones", + "childID": 62 + }, + { + "name": "Jason Schuppe", + "childID": 78 + }, + { + "name": "Clovis Ryan", + "childID": 87 + }, + { + "name": "Randi Witting", + "childID": 86 + }, + { + "name": "Cristian Connelly Sr.", + "childID": 62 + }, + { + "name": "Juston Schmidt III", + "childID": 78 + }, + { + "name": "Johnnie Blanda II", + "childID": 50 + }, + { + "name": "Irma Feest", + "childID": 87 + }, + { + "name": "Mr. Donnie Schamberger", + "childID": 59 + }, + { + "name": "Emil Fay", + "childID": 71 + }, + { + "name": "Anthony Hartmann", + "childID": 74 + }, + { + "name": "Annalise Heller", + "childID": 78 + }, + { + "name": "Laverna Blick", + "childID": 100 + }, + { + "name": "Vita Wolf", + "childID": 70 + } + ] + }, + { + "parentID": 23, + "name": "Dr. Lilian Bogan", + "children": [ + { + "name": "Joshua Hilpert Sr.", + "childID": 74 + }, + { + "name": "Carroll Hermiston", + "childID": 77 + }, + { + "name": "Pierre Ziemann", + "childID": 66 + }, + { + "name": "Gerard Goldner", + "childID": 73 + }, + { + "name": "Julian Jacobson", + "childID": 54 + }, + { + "name": "Laney Pfeffer MD", + "childID": 52 + }, + { + "name": "Dion Heathcote", + "childID": 82 + }, + { + "name": "Brooke Bauch", + "childID": 59 + }, + { + "name": "Kira Kuhn", + "childID": 72 + }, + { + "name": "Darius Miller Jr.", + "childID": 95 + }, + { + "name": "Flossie Abbott", + "childID": 78 + }, + { + "name": "Oma Schuster V", + "childID": 66 + }, + { + "name": "Gilbert Wintheiser", + "childID": 95 + }, + { + "name": "Lorenzo Daugherty", + "childID": 67 + }, + { + "name": "Aaron Stamm", + "childID": 70 + }, + { + "name": "Darian Bosco IV", + "childID": 73 + }, + { + "name": "Aiden Deckow", + "childID": 57 + }, + { + "name": "Elias Hackett", + "childID": 80 + }, + { + "name": "Will Bernier PhD", + "childID": 88 + }, + { + "name": "Samara Ledner", + "childID": 88 + }, + { + "name": "Yadira Pfannerstill", + "childID": 83 + }, + { + "name": "Shanny Schoen PhD", + "childID": 59 + }, + { + "name": "Camila Ebert", + "childID": 64 + }, + { + "name": "Ethan Hagenes", + "childID": 58 + } + ] + }, + { + "parentID": 24, + "name": "Bettye Morissette Jr.", + "children": [ + { + "name": "Jannie Hills", + "childID": 92 + }, + { + "name": "Elody Renner", + "childID": 98 + }, + { + "name": "Miss Ariel D'Amore", + "childID": 86 + }, + { + "name": "Mr. Dejon Mills", + "childID": 53 + }, + { + "name": "Marie Heaney", + "childID": 56 + }, + { + "name": "Donna Ondricka", + "childID": 72 + }, + { + "name": "Tyler Halvorson", + "childID": 64 + }, + { + "name": "Katlynn Wiegand", + "childID": 67 + }, + { + "name": "Trevor Hickle", + "childID": 58 + }, + { + "name": "Einar Tromp", + "childID": 60 + }, + { + "name": "Jacey Hartmann", + "childID": 81 + }, + { + "name": "Mr. Jacey Block", + "childID": 73 + }, + { + "name": "Dr. Earline Bergstrom", + "childID": 76 + }, + { + "name": "Filiberto Hansen", + "childID": 94 + }, + { + "name": "Keyon Beahan", + "childID": 91 + } + ] + }, + { + "parentID": 25, + "name": "Amber Brakus", + "children": [ + { + "name": "Kaci Fadel", + "childID": 53 + }, + { + "name": "Jairo Gorczany", + "childID": 75 + }, + { + "name": "Nasir Dickens", + "childID": 62 + }, + { + "name": "Miss Elvis Denesik", + "childID": 66 + }, + { + "name": "Justice Schumm", + "childID": 93 + }, + { + "name": "Francisca Gerlach", + "childID": 72 + }, + { + "name": "Americo McLaughlin Jr.", + "childID": 100 + }, + { + "name": "Alexanne Littel", + "childID": 75 + }, + { + "name": "Alaina Jones", + "childID": 98 + }, + { + "name": "Julianne O'Kon", + "childID": 58 + }, + { + "name": "Alexis Kiehn", + "childID": 64 + }, + { + "name": "Elmer Armstrong V", + "childID": 91 + }, + { + "name": "Mr. Madelyn Kunde", + "childID": 98 + }, + { + "name": "Buster Schamberger", + "childID": 64 + }, + { + "name": "Monica Boyer Jr.", + "childID": 62 + }, + { + "name": "Mrs. Karolann Luettgen", + "childID": 82 + }, + { + "name": "Gayle Keeling", + "childID": 60 + }, + { + "name": "Sammie Jenkins", + "childID": 82 + }, + { + "name": "William Crist", + "childID": 55 + }, + { + "name": "Theo Anderson V", + "childID": 76 + }, + { + "name": "Camila Hackett", + "childID": 72 + }, + { + "name": "Caitlyn Terry DVM", + "childID": 53 + }, + { + "name": "Austyn Hilll II", + "childID": 51 + }, + { + "name": "Dr. Everardo Brown", + "childID": 64 + } + ] + } + ] +} diff --git a/Tests/Logic/CoreData/RKEntityCacheTest.m b/Tests/Logic/CoreData/RKEntityCacheTest.m new file mode 100644 index 00000000..ec904215 --- /dev/null +++ b/Tests/Logic/CoreData/RKEntityCacheTest.m @@ -0,0 +1,155 @@ +// +// RKEntityCacheTest.m +// RestKit +// +// Created by Blake Watters on 5/2/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKEntityCache.h" +#import "RKEntityByAttributeCache.h" +#import "RKHuman.h" + +@interface RKEntityCacheTest : RKTestCase +@property (nonatomic, retain) RKManagedObjectStore *objectStore; +@property (nonatomic, retain) RKEntityCache *cache; +@property (nonatomic, retain) NSEntityDescription *entity; +@end + +@implementation RKEntityCacheTest + +@synthesize objectStore = _objectStore; +@synthesize cache = _cache; +@synthesize entity = _entity; + +- (void)setUp +{ + [RKTestFactory setUp]; + + self.objectStore = [RKTestFactory managedObjectStore]; + _cache = [[RKEntityCache alloc] initWithManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + self.entity = [RKHuman entityDescriptionInContext:self.objectStore.primaryManagedObjectContext]; +} + +- (void)tearDown +{ + self.objectStore = nil; + self.cache = nil; + + [RKTestFactory tearDown]; +} + +- (void)testInitializationSetsManagedObjectContext +{ + assertThat(_cache.managedObjectContext, is(equalTo(self.objectStore.primaryManagedObjectContext))); +} + +- (void)testIsEntityCachedByAttribute +{ + assertThatBool([_cache isEntity:self.entity cachedByAttribute:@"railsID"], is(equalToBool(NO))); + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + assertThatBool([_cache isEntity:self.entity cachedByAttribute:@"railsID"], is(equalToBool(YES))); +} + +- (void)testRetrievalOfUnderlyingEntityAttributeCache +{ + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + RKEntityByAttributeCache *attributeCache = [_cache attributeCacheForEntity:self.entity attribute:@"railsID"]; + assertThat(attributeCache, is(notNilValue())); +} + +- (void)testRetrievalOfUnderlyingEntityAttributeCaches +{ + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + NSSet *caches = [_cache attributeCachesForEntity:self.entity]; + assertThat(caches, is(notNilValue())); + assertThatInteger([caches count], is(equalToInteger(1))); +} + +- (void)testRetrievalOfObjectForEntityWithAttributeValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + NSError *error = nil; + [self.objectStore save:&error]; + + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + NSManagedObject *fetchedObject = [self.cache objectForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(fetchedObject, is(notNilValue())); +} + +- (void)testRetrievalOfObjectsForEntityWithAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + NSError *error = nil; + [self.objectStore save:&error]; + + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + NSSet *objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat(objects, containsInAnyOrder(human1, human2, nil)); +} + +- (void)testThatFlushEmptiesAllUnderlyingAttributeCaches +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + human1.name = @"Blake"; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + human2.name = @"Sarah"; + + [self.objectStore save:nil]; + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + [_cache cacheObjectsForEntity:self.entity byAttribute:@"name"]; + + NSSet *objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat(objects, containsInAnyOrder(human1, human2, nil)); + + objects = [self.cache objectsForEntity:self.entity withAttribute:@"name" value:@"Blake"]; + assertThat(objects, hasCountOf(1)); + assertThat(objects, contains(human1, nil)); + + [self.cache flush]; + objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, is(empty())); + objects = [self.cache objectsForEntity:self.entity withAttribute:@"name" value:@"Blake"]; + assertThat(objects, is(empty())); +} + +- (void)testAddingObjectAddsToEachUnderlyingEntityAttributeCaches +{ + [_cache cacheObjectsForEntity:self.entity byAttribute:@"railsID"]; + [_cache cacheObjectsForEntity:self.entity byAttribute:@"name"]; + + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + human1.name = @"Blake"; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + human2.name = @"Sarah"; + + [_cache addObject:human1]; + [_cache addObject:human2]; + + NSSet *objects = [self.cache objectsForEntity:self.entity withAttribute:@"railsID" value:[NSNumber numberWithInteger:12345]]; + assertThat(objects, hasCountOf(2)); + assertThat(objects, containsInAnyOrder(human1, human2, nil)); + + objects = [self.cache objectsForEntity:self.entity withAttribute:@"name" value:@"Blake"]; + assertThat(objects, hasCountOf(1)); + assertThat(objects, contains(human1, nil)); +} + +- (void)testRemovingObjectRemovesFromUnderlyingEntityAttributeCaches +{ + +} + +@end diff --git a/Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.h b/Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.h new file mode 100644 index 00000000..574abed4 --- /dev/null +++ b/Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.h @@ -0,0 +1,13 @@ +// +// RKInMemoryEntityAttributeCacheTest.h +// RestKit +// +// Created by Blake Watters on 5/1/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import + +@interface RKInMemoryEntityAttributeCacheTest : NSObject + +@end diff --git a/Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.m b/Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.m new file mode 100644 index 00000000..697d086e --- /dev/null +++ b/Tests/Logic/CoreData/RKInMemoryEntityAttributeCacheTest.m @@ -0,0 +1,345 @@ +// +// RKInMemoryEntityAttributeCacheTest.m +// RestKit +// +// Created by Blake Watters on 5/1/12. +// Copyright (c) 2012 RestKit. All rights reserved. +// + +#import "RKTestEnvironment.h" +#import "NSEntityDescription+RKAdditions.h" +#import "RKEntityByAttributeCache.h" +#import "RKHuman.h" +#import "RKChild.h" + +@interface RKInMemoryEntityAttributeCacheTest : RKTestCase +@property (nonatomic, retain) RKManagedObjectStore *objectStore; +@property (nonatomic, retain) RKEntityByAttributeCache *cache; +@end + +@implementation RKInMemoryEntityAttributeCacheTest + +@synthesize objectStore = _objectStore; +@synthesize cache = _cache; + +- (void)setUp +{ + [RKTestFactory setUp]; + self.objectStore = [RKTestFactory managedObjectStore]; + + NSEntityDescription *entity = [RKHuman entityDescriptionInContext:self.objectStore.primaryManagedObjectContext]; + self.cache = [[RKEntityByAttributeCache alloc] initWithEntity:entity + attribute:@"railsID" + managedObjectContext:self.objectStore.primaryManagedObjectContext]; + // Disable cache monitoring. Tested in specific cases. + self.cache.monitorsContextForChanges = NO; +} + +- (void)tearDown +{ + self.objectStore = nil; + [RKTestFactory tearDown]; +} + +#pragma mark - Identity Tests + +- (void)testEntityIsAssigned +{ + NSEntityDescription *entity = [RKHuman entityDescriptionInContext:self.objectStore.primaryManagedObjectContext]; + assertThat(self.cache.entity, is(equalTo(entity))); +} + +- (void)testManagedObjectContextIsAssigned +{ + NSManagedObjectContext *context = self.objectStore.primaryManagedObjectContext; + assertThat(self.cache.managedObjectContext, is(equalTo(context))); +} + +- (void)testAttributeNameIsAssigned +{ + assertThat(self.cache.attribute, is(equalTo(@"railsID"))); +} + +#pragma mark - Loading and Flushing + +- (void)testLoadSetsLoadedToYes +{ + [self.cache load]; + assertThatBool(self.cache.isLoaded, is(equalToBool(YES))); +} + +- (void)testLoadSetsCountAppropriately +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + NSError *error = nil; + [self.objectStore save:&error]; + + assertThat(error, is(nilValue())); + [self.cache load]; + assertThatInteger([self.cache count], is(equalToInteger(1))); +} + +- (void)testFlushCacheRemovesObjects +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + [self.cache flush]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(NO))); +} + +- (void)testFlushCacheReturnsCountToZero +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + [self.cache flush]; + assertThatInteger([self.cache count], is(equalToInteger(0))); +} + +#pragma mark - Retrieving Objects + +- (void)testRetrievalByNumericValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + [self.cache load]; + + NSManagedObject *object = [self.cache objectWithAttributeValue:[NSNumber numberWithInteger:12345]]; + assertThat(object, is(equalTo(human))); +} + +- (void)testRetrievalOfNumericPropertyByStringValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + [self.cache load]; + + NSManagedObject *object = [self.cache objectWithAttributeValue:@"12345"]; + assertThat(object, is(equalTo(human))); +} + +- (void)testRetrievalOfObjectsWithAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + + NSSet *set = [self.cache objectsWithAttributeValue:[NSNumber numberWithInt:12345]]; + assertThat(set, hasCountOf(2)); + assertThat([set anyObject], is(instanceOf([NSManagedObject class]))); +} + +- (void)testAddingObjectToCache +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(YES))); +} + +- (void)testAddingObjectWithDuplicateAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); +} + +- (void)testRemovingObjectFromCache +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(YES))); + [self.cache removeObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(NO))); +} + +- (void)testRemovingObjectWithExistingAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); + [self.cache removeObject:human1]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); +} + +#pragma mark - Inspecting Cache State + +- (void)testContainsObjectReturnsNoForDifferingEntities +{ + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCloud" inManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + NSManagedObject *cloud = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + assertThatBool([self.cache containsObject:cloud], is(equalToBool(NO))); +} + +- (void)testContainsObjectReturnsNoForSubEntities +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + RKChild *child = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + child.railsID = [NSNumber numberWithInteger:12345]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObject:human], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:child], is(equalToBool(NO))); +} + +- (void)testContainsObjectWithAttributeValue +{ + RKHuman *human = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human]; + assertThatBool([self.cache containsObjectWithAttributeValue:[NSNumber numberWithInteger:12345]], is(equalToBool(YES))); +} + +- (void)testCountWithAttributeValue +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatInteger([self.cache countWithAttributeValue:[NSNumber numberWithInteger:12345]], is(equalToInteger(2))); +} + +- (void)testThatUnloadedCacheReturnsCountOfZero +{ + assertThatInteger([self.cache count], is(equalToInteger(0))); +} + +#pragma mark - Lifecycle Events + +- (void)testManagedObjectContextProcessPendingChangesAddsNewObjectsToCache +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); +} + +- (void)testManagedObjectContextProcessPendingChangesIgnoresObjectsOfDifferentEntityTypes +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"RKCloud" inManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + NSManagedObject *cloud = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.objectStore.primaryManagedObjectContext]; + [cloud setValue:@"Cumulus" forKey:@"name"]; + + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:cloud], is(equalToBool(NO))); +} + +- (void)testManagedObjectContextProcessPendingChangesAddsUpdatedObjectsToCache +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + [self.cache removeObject:human1]; + human1.name = @"Modified Name"; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); +} + +- (void)testManagedObjectContextProcessPendingChangesRemovesExistingObjectsFromCache +{ + self.cache.monitorsContextForChanges = YES; + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + [self.objectStore.primaryManagedObjectContext deleteObject:human1]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(NO))); +} + +#if TARGET_OS_IPHONE +- (void)testCacheIsFlushedOnMemoryWarning +{ + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore save:nil]; + + [self.cache addObject:human1]; + [self.cache addObject:human2]; + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification object:self]; +} +#endif + +- (void)testCreatingProcessingAndDeletingObjectsWorksAsExpected { + self.cache.monitorsContextForChanges = YES; + + RKHuman *human1 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human1.railsID = [NSNumber numberWithInteger:12345]; + RKHuman *human2 = [RKHuman createInContext:self.objectStore.primaryManagedObjectContext]; + human2.railsID = [NSNumber numberWithInteger:12345]; + [self.objectStore.primaryManagedObjectContext processPendingChanges]; + + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(YES))); + [self.objectStore.primaryManagedObjectContext deleteObject:human2]; + + // Save and reload the cache. This will result in the cached temporary + // object ID's being released during the cache flush. + [self.objectStore.primaryManagedObjectContext save:nil]; + [self.cache load]; + + assertThatBool([self.cache containsObject:human1], is(equalToBool(YES))); + assertThatBool([self.cache containsObject:human2], is(equalToBool(NO))); +} + +@end diff --git a/Tests/Logic/CoreData/RKInMemoryEntityCacheTest.m b/Tests/Logic/CoreData/RKInMemoryEntityCacheTest.m index f6736ecb..be7fefb7 100644 --- a/Tests/Logic/CoreData/RKInMemoryEntityCacheTest.m +++ b/Tests/Logic/CoreData/RKInMemoryEntityCacheTest.m @@ -19,6 +19,7 @@ // #import "RKTestEnvironment.h" +#import "RKInMemoryEntityCache.h" #import "RKHuman.h" @interface RKInMemoryEntityCache () @@ -262,6 +263,7 @@ } - (void)testThatRepeatedInvocationsOfLoadObjectDoesNotDuplicateObjects { + RKLogConfigureByName("RestKit/CoreData", RKLogLevelTrace); RKManagedObjectStore *objectStore = [RKTestFactory managedObjectStore]; objectStore.cacheStrategy = [RKInMemoryManagedObjectCache new]; RKObjectManager *objectManager = [RKTestFactory objectManager]; @@ -274,12 +276,12 @@ for (NSUInteger i = 0; i < 5; i++) { RKTestResponseLoader *responseLoader = [RKTestResponseLoader responseLoader]; + responseLoader.timeout = 1000; [objectManager loadObjectsAtResourcePath:@"/JSON/ArrayOfHumans.json" delegate:responseLoader]; [responseLoader waitForResponse]; for (RKHuman *object in [RKHuman allObjects]) { if ([object.railsID intValue] == 201) { [objectStore.managedObjectContextForCurrentThread deleteObject:object]; - [objectStore.managedObjectContextForCurrentThread save:nil]; } } diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m index 45709088..d6024f4e 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingOperationTest.m @@ -25,6 +25,7 @@ #import "RKHuman.h" #import "RKChild.h" #import "RKParent.h" +#import "RKBenchmark.h" @interface RKManagedObjectMappingOperationTest : RKTestCase { @@ -352,6 +353,7 @@ // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary // keys are not guaranteed to return in any particular order [mappingProvider setMapping:parentMapping forKeyPath:@"parents"]; + [mappingProvider setMapping:childMapping forKeyPath:@"children"]; NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"ConnectingParents.json"]; RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; @@ -418,4 +420,74 @@ assertThatInteger(childrenCount, is(equalToInteger(4))); } +- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithFetchRequestMappingCache { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKFetchRequestManagedObjectCache new]; + + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + childMapping.primaryKeyAttribute = @"childID"; + [childMapping mapAttributes:@"name", @"childID", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + [parentMapping mapAttributes:@"parentID", @"name", nil]; + parentMapping.primaryKeyAttribute = @"parentID"; + [parentMapping mapRelationship:@"children" withMapping:childMapping]; + + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; + // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary + // keys are not guaranteed to return in any particular order + [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; + + RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); + RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); + + [RKBenchmark report:@"Mapping with Fetch Request Cache" executionBlock:^{ + for (NSUInteger i=0; i<50; i++) { + [mapper performMapping]; + } + }]; + NSUInteger parentCount = [RKParent count:nil]; + NSUInteger childrenCount = [RKChild count:nil]; + assertThatInteger(parentCount, is(equalToInteger(25))); + assertThatInteger(childrenCount, is(equalToInteger(51))); +} + +- (void)testMappingAPayloadContainingRepeatedObjectsPerformsAcceptablyWithInMemoryMappingCache { + RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; + store.cacheStrategy = [RKInMemoryManagedObjectCache new]; + + RKManagedObjectMapping* childMapping = [RKManagedObjectMapping mappingForClass:[RKChild class] inManagedObjectStore:store]; + childMapping.primaryKeyAttribute = @"childID"; + [childMapping mapAttributes:@"name", @"childID", nil]; + + RKManagedObjectMapping* parentMapping = [RKManagedObjectMapping mappingForClass:[RKParent class] inManagedObjectStore:store]; + [parentMapping mapAttributes:@"parentID", @"name", nil]; + parentMapping.primaryKeyAttribute = @"parentID"; + [parentMapping mapRelationship:@"children" withMapping:childMapping]; + + RKObjectMappingProvider *mappingProvider = [RKObjectMappingProvider new]; + // NOTE: This may be fragile. Reverse order seems to trigger them to be mapped parent first. NSDictionary + // keys are not guaranteed to return in any particular order + [mappingProvider setObjectMapping:parentMapping forKeyPath:@"parents"]; + + NSDictionary *JSON = [RKTestFixture parsedObjectWithContentsOfFixture:@"benchmark_parents_and_children.json"]; + RKObjectMapper *mapper = [RKObjectMapper mapperWithObject:JSON mappingProvider:mappingProvider]; + + RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelOff); + RKLogConfigureByName("RestKit/CoreData", RKLogLevelOff); + + [RKBenchmark report:@"Mapping with In Memory Cache" executionBlock:^{ + for (NSUInteger i=0; i<50; i++) { + [mapper performMapping]; + } + }]; + NSUInteger parentCount = [RKParent count:nil]; + NSUInteger childrenCount = [RKChild count:nil]; + assertThatInteger(parentCount, is(equalToInteger(25))); + assertThatInteger(childrenCount, is(equalToInteger(51))); +} + @end diff --git a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m index 26e6cb4e..6d1dab14 100644 --- a/Tests/Logic/CoreData/RKManagedObjectMappingTest.m +++ b/Tests/Logic/CoreData/RKManagedObjectMappingTest.m @@ -32,6 +32,16 @@ @implementation RKManagedObjectMappingTest +- (void)setUp +{ + [RKTestFactory setUp]; +} + +- (void)tearDown +{ + [RKTestFactory tearDown]; +} + - (void)testShouldReturnTheDefaultValueForACoreDataAttribute { // Load Core Data RKManagedObjectStore *store = [RKTestFactory managedObjectStore]; @@ -220,13 +230,14 @@ mapping.primaryKeyAttribute = @"railsID"; [mapping addAttributeMapping:[RKObjectAttributeMapping mappingFromKeyPath:@"id" toKeyPath:@"railsID"]]; - RKHuman* human = [RKHuman object]; + RKHuman* human = [RKHuman createInContext:store.primaryManagedObjectContext]; human.railsID = [NSNumber numberWithInt:123]; [store save:nil]; assertThatBool([RKHuman hasAtLeastOneEntity], is(equalToBool(YES))); NSDictionary* data = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:123] forKey:@"id"]; - id object = [mapping mappableObjectForData:data]; + NSManagedObject *object = [mapping mappableObjectForData:data]; + assertThat([object managedObjectContext], is(equalTo(store.primaryManagedObjectContext))); assertThat(object, isNot(nilValue())); assertThat(object, is(equalTo(human))); }