Completed documentation and testing for managed object cache overhaul. refs #635

This commit is contained in:
Blake Watters
2012-05-15 12:49:42 -04:00
parent 9e75eb14bd
commit 9bf89ebec1
14 changed files with 377 additions and 88 deletions

View File

@@ -8,54 +8,173 @@
#import <CoreData/CoreData.h>
// RKManagedObjectContext
// Maybe RKManagedObjectContextCache | RKEntityCache | RKEntityByAttributeCache
// TODO: Better name... RKEntityAttributeCache ??
/**
Instances of RKEntityByAttributeCache provide an in-memory caching mechanism
for managed objects instances of an entity in a managed object context with
the value of one of the object's attributes acting as the cache key. When loaded,
the cache will retrieve all instances of an entity from the store and build a
dictionary mapping values for the given cache key attribute to the managed object
ID for all objects matching the value. The cache can then be used to quickly retrieve
objects by attribute value for the cache key without executing another fetch request
against the managed object context. This can provide a large performance improvement
when a large number of objects are being retrieved using a particular attribute as
the key.
RKEntityByAttributeCache instances are used by the RKEntityCache to provide
caching for multiple entities at once.
@see RKEntityCache
*/
@interface RKEntityByAttributeCache : NSObject
///-----------------------------------------------------------------------------
/// @name Creating a Cache
///-----------------------------------------------------------------------------
/**
Initializes the receiver with a given entity, attribute, and managed object context.
@param entity The Core Data entity description for the managed objects being cached.
@param attributeName The name of an attribute within the cached entity that acts as the cache key.
@param managedObjectContext The managed object context the cache retrieves the cached
objects from
@return The receiver, initialized with the given entity, attribute, and managed object
context.
*/
- (id)initWithEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName managedObjectContext:(NSManagedObjectContext *)context;
///-----------------------------------------------------------------------------
/// @name Getting Cache Identity
///-----------------------------------------------------------------------------
/**
The Core Data entity description for the managed objects being cached.
*/
@property (nonatomic, readonly) NSEntityDescription *entity;
/**
An attribute that is part of the cached entity that acts as the cache key.
*/
@property (nonatomic, readonly) NSString *attribute;
/**
The managed object context the receiver fetches cached objects from.
*/
@property (nonatomic, readonly) NSManagedObjectContext *managedObjectContext;
/**
A Boolean value determining if the receiever monitors the managed object context
for changes and updates the cache entries using the notifications emitted.
*/
@property (nonatomic, assign) BOOL monitorsContextForChanges;
///-----------------------------------------------------------------------------
/// @name Loading and Flushing the Cache
///-----------------------------------------------------------------------------
/**
Loads the cache by finding all instances of the configured entity and building
an association between the value of the cached attribute's value and the
managed object ID for the object.
*/
- (void)load;
/**
Flushes the cache by releasing all cache attribute value to managed object ID
associations.
*/
- (void)flush;
///-----------------------------------------------------------------------------
/// @name Inspecting Cache State
///-----------------------------------------------------------------------------
/**
A Boolean value indicating if the cache has loaded associations between cache
attribute values and managed object ID's.
*/
- (BOOL)isLoaded;
/**
Returns a count of the total number of cached objects.
*/
- (NSUInteger)count;
/**
Returns the total number of cached objects with a given value for
the attribute acting as the cache key.
@param attributeValue The value for the cache key attribute to retrieve
a count of the objects with a matching value.
@return The number of objects in the cache with the given value for the cache
attribute of the receiver.
*/
- (NSUInteger)countWithAttributeValue:(id)attributeValue;
/**
Returns the number of unique attribute values contained within the receiver.
@return The number of unique attribute values within the receiver.
*/
- (NSUInteger)countOfAttributeValues;
/**
Returns a Boolean value that indicates whether a given object is present
in the cache.
@param object An object.
@return YES if object is present in the cache, otherwise NO.
*/
- (BOOL)containsObject:(NSManagedObject *)object;
/**
Returns a Boolean value that indicates whether one of more objects is present
in the cache with a given value of the cache key attribute.
@param attributeValue The value with which to check the cache for objects with
a matching value.
@return YES if one or more objects with the given value for the cache key
attribute is present in the cache, otherwise NO.
*/
- (BOOL)containsObjectWithAttributeValue:(id)attributeValue;
// Retrieve the object with the value for the attribute
/**
Returns the first object with a matching value for the cache key attribute.
@param attributeValue A value for the cache key attribute.
@return An object with the value of attribute matching attributeValue or nil.
*/
- (NSManagedObject *)objectWithAttributeValue:(id)attributeValue;
- (NSSet *)objectsWithAttributeValue:(id)attributeValue;
/**
Returns the collection of objects with a matching value for the cache key attribute.
@param attributeValue A value for the cache key attribute.
@return An array of objects with the value of attribute matching attributeValue or
an empty array.
*/
- (NSArray *)objectsWithAttributeValue:(id)attributeValue;
///-----------------------------------------------------------------------------
/// @name Managing Cached Objects
///-----------------------------------------------------------------------------
/**
Adds a managed object to the cache.
The object must be an instance of the cached entity.
@param object The managed object to add to the cache.
*/
- (void)addObject:(NSManagedObject *)object;
/**
Removes a managed object from the cache.
The object must be an instance of the cached entity.
@param object The managed object to remove from the cache.
*/
- (void)removeObject:(NSManagedObject *)object;
@end

View File

@@ -73,6 +73,11 @@
}
- (NSUInteger)count
{
return [[[self.attributeValuesToObjectIDs allValues] valueForKeyPath:@"@sum.@count"] integerValue];
}
- (NSUInteger)countOfAttributeValues
{
return [self.attributeValuesToObjectIDs count];
}
@@ -84,8 +89,6 @@
- (BOOL)shouldCoerceAttributeToString:(NSString *)attributeValue
{
return NO;
if ([attributeValue isKindOfClass:[NSString class]] || [attributeValue isEqual:[NSNull null]]) {
return NO;
}
@@ -140,7 +143,8 @@
- (NSManagedObject *)objectWithAttributeValue:(id)attributeValue
{
return [[self objectsWithAttributeValue:attributeValue] anyObject];
NSArray *objects = [self objectsWithAttributeValue:attributeValue];
return ([objects count] > 0) ? [objects objectAtIndex:0] : nil;
}
- (NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID {
@@ -160,13 +164,12 @@
return object;
}
- (NSSet *)objectsWithAttributeValue:(id)attributeValue
- (NSArray *)objectsWithAttributeValue:(id)attributeValue
{
attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue;
NSMutableSet *set = [self.attributeValuesToObjectIDs objectForKey:attributeValue];
if (set) {
NSSet *objectIDs = [NSSet setWithSet:set];
NSMutableSet *objects = [NSMutableSet setWithCapacity:[objectIDs count]];
NSMutableArray *objectIDs = [self.attributeValuesToObjectIDs objectForKey:attributeValue];
if (objectIDs) {
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[objectIDs count]];
for (NSManagedObjectID *objectID in objectIDs) {
NSManagedObject *object = [self objectWithID:objectID];
if (object) [objects addObject:object];
@@ -175,7 +178,7 @@
return objects;
}
return [NSSet set];
return [NSArray array];
}
- (void)addObject:(NSManagedObject *)object
@@ -186,15 +189,17 @@
attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue;
if (attributeValue) {
NSManagedObjectID *objectID = [object objectID];
NSMutableSet *set = [self.attributeValuesToObjectIDs objectForKey:attributeValue];
if (set) {
[set addObject:objectID];
NSMutableArray *objectIDs = [self.attributeValuesToObjectIDs objectForKey:attributeValue];
if (objectIDs) {
if (! [objectIDs containsObject:objectID]) {
[objectIDs addObject:objectID];
}
} else {
set = [NSMutableSet setWithObject:objectID];
objectIDs = [NSMutableArray arrayWithObject:objectID];
}
if (nil == self.attributeValuesToObjectIDs) self.attributeValuesToObjectIDs = [NSMutableDictionary dictionary];
[self.attributeValuesToObjectIDs setValue:set forKey:attributeValue];
[self.attributeValuesToObjectIDs setValue:objectIDs forKey:attributeValue];
} else {
RKLogWarning(@"Unable to add object with nil value for attribute '%@': %@", self.attribute, object);
}
@@ -208,9 +213,9 @@
attributeValue = [self shouldCoerceAttributeToString:attributeValue] ? [attributeValue stringValue] : attributeValue;
if (attributeValue) {
NSManagedObjectID *objectID = [object objectID];
NSMutableSet *set = [self.attributeValuesToObjectIDs objectForKey:attributeValue];
if (set) {
[set removeObject:objectID];
NSMutableArray *objectIDs = [self.attributeValuesToObjectIDs objectForKey:attributeValue];
if (objectIDs && [objectIDs containsObject:objectID]) {
[objectIDs removeObject:objectID];
}
} else {
RKLogWarning(@"Unable to remove object with nil value for attribute '%@': %@", self.attribute, object);

View File

@@ -21,7 +21,9 @@
*/
@interface RKEntityCache : NSObject
///-----------------------------------------------------------------------------
/// @name Initializing the Cache
///-----------------------------------------------------------------------------
/**
Initializes the receiver with a managed object context containing the entity instances to be cached.
@@ -31,9 +33,14 @@
*/
- (id)initWithManagedObjectContext:(NSManagedObjectContext *)context;
/**
The managed object context with which the receiver is associated.
*/
@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
///-----------------------------------------------------------------------------
/// @name Caching Objects by Attribute
///-----------------------------------------------------------------------------
/**
Caches all instances of an entity using the value for an attribute as the cache key.
@@ -64,7 +71,7 @@
- (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.
Retrieves 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.
@@ -72,20 +79,55 @@
@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;
- (NSArray *)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.
Retrieves the underlying entity attribute cache for a given entity and attribute.
@param entity The entity to retrieve the entity attribute cache object for.
@param attributeName The attribute to retrieve the entity attribute cache object for.
@return The entity attribute cache for the given entity and attribute, or nil if none was found.
*/
- (RKEntityByAttributeCache *)attributeCacheForEntity:(NSEntityDescription *)entity attribute:(NSString *)attributeName;
/**
Retrieves all entity attributes caches for a given entity.
@param entity The entity to retrieve the collection of entity attribute caches for.
@return An array of entity attribute cache objects for the given entity or an empty array if none were found.
*/
- (NSArray *)attributeCachesForEntity:(NSEntityDescription *)entity;
///-----------------------------------------------------------------------------
// @name Managing the Cache
///-----------------------------------------------------------------------------
/**
Flushes the entity cache by sending a flush message to each entity attribute cache
contained within the receiver.
@see [RKEntityByAttributeCache flush]
*/
- (void)flush;
/**
Adds a given object to all entity attribute caches for the object's entity contained
within the receiver.
@param object The object to add to the appropriate entity attribute caches.
*/
- (void)addObject:(NSManagedObject *)object;
/**
Removed a given object from all entity attribute caches for the object's entity contained
within the receiver.
@param object The object to remove from the appropriate entity attribute caches.
*/
- (void)removeObject:(NSManagedObject *)object;
@end

View File

@@ -77,7 +77,7 @@
return nil;
}
- (NSSet *)objectsForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue
- (NSArray *)objectsForEntity:(NSEntityDescription *)entity withAttribute:(NSString *)attributeName value:(id)attributeValue
{
NSAssert(entity, @"Cannot retrieve cached objects with a nil entity");
NSAssert(attributeName, @"Cannot retrieve cached objects by a nil entity");
@@ -123,7 +123,7 @@
- (void)addObject:(NSManagedObject *)object
{
NSAssert(object, @"Cannot add a nil object to the cache");
NSSet *attributeCaches = [self attributeCachesForEntity:object.entity];
NSArray *attributeCaches = [self attributeCachesForEntity:object.entity];
for (RKEntityByAttributeCache *cache in attributeCaches) {
[cache addObject:object];
}
@@ -132,7 +132,7 @@
- (void)removeObject:(NSManagedObject *)object
{
NSAssert(object, @"Cannot remove a nil object from the cache");
NSSet *attributeCaches = [self attributeCachesForEntity:object.entity];
NSArray *attributeCaches = [self attributeCachesForEntity:object.entity];
for (RKEntityByAttributeCache *cache in attributeCaches) {
[cache removeObject:object];
}

View File

@@ -19,14 +19,18 @@
@implementation RKFetchRequestManagedObjectCache
- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity
withPrimaryKeyValue:(id)primaryKeyValue inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext {
- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity
withPrimaryKeyAttribute:(NSString *)primaryKeyAttribute
value:(id)primaryKeyValue
inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
NSAssert(entity, @"Cannot find existing managed object without a target class");
NSAssert(primaryKeyAttribute, @"Cannot find existing managed object instance without mapping that defines a primaryKeyAttribute");
NSAssert(primaryKeyValue, @"Cannot find existing managed object by primary key without a value");
NSAssert(managedObjectContext, @"Cannot find existing managed object with a context");
id searchValue = primaryKeyValue;
Class type = [[RKObjectPropertyInspector sharedInspector] typeForProperty:entity.primaryKeyAttribute ofEntity:entity];
Class type = [[RKObjectPropertyInspector sharedInspector] typeForProperty:primaryKeyAttribute ofEntity:entity];
if (type && ([type isSubclassOfClass:[NSString class]] && NO == [primaryKeyValue isKindOfClass:[NSString class]])) {
searchValue = [NSString stringWithFormat:@"%@", primaryKeyValue];
} else if (type && ([type isSubclassOfClass:[NSNumber class]] && NO == [primaryKeyValue isKindOfClass:[NSNumber class]])) {
@@ -35,7 +39,14 @@
}
}
NSPredicate *predicate = [entity predicateForPrimaryKeyAttributeWithValue:searchValue];
// Use cached predicate if primary key matches
NSPredicate *predicate = nil;
if ([entity.primaryKeyAttribute isEqualToString:primaryKeyAttribute]) {
predicate = [entity predicateForPrimaryKeyAttributeWithValue:searchValue];
} else {
// Parse a predicate
predicate = [NSPredicate predicateWithFormat:@"%K = %@", primaryKeyAttribute, searchValue];
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = entity;
fetchRequest.fetchLimit = 1;

View File

@@ -37,22 +37,23 @@ static NSString * const RKInMemoryObjectManagedObjectCacheThreadDictionaryKey =
[entityCache release];
}
if (! [entityCache isEntity:entity cachedByAttribute:entity.primaryKeyAttribute]) {
RKLogInfo(@"Caching instances of Entity '%@' by primary key attribute '%@'", entity.name, entity.primaryKeyAttribute);
[entityCache cacheObjectsForEntity:entity byAttribute:entity.primaryKeyAttribute];
RKEntityByAttributeCache *attributeCache = [entityCache attributeCacheForEntity:entity attribute:entity.primaryKeyAttribute];
RKLogTrace(@"Cached %d objects", [attributeCache count]);
}
return entityCache;
}
- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity
withPrimaryKeyValue:(id)primaryKeyValue
withPrimaryKeyAttribute:(NSString *)primaryKeyAttribute
value:(id)primaryKeyValue
inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
RKEntityCache *entityCache = [self cacheForEntity:entity inManagedObjectContext:managedObjectContext];
return [entityCache objectForEntity:entity withAttribute:entity.primaryKeyAttribute value:primaryKeyValue];
RKEntityCache *entityCache = [self cacheForEntity:entity inManagedObjectContext:managedObjectContext];
if (! [entityCache isEntity:entity cachedByAttribute:primaryKeyAttribute]) {
RKLogInfo(@"Caching instances of Entity '%@' by primary key 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];
}
- (void)didFetchObject:(NSManagedObject *)object
@@ -63,16 +64,12 @@ static NSString * const RKInMemoryObjectManagedObjectCacheThreadDictionaryKey =
- (void)didCreateObject:(NSManagedObject *)object
{
if (! object.entity.primaryKeyAttribute) return;
RKEntityCache *entityCache = [self cacheForEntity:object.entity inManagedObjectContext:object.managedObjectContext];
[entityCache addObject:object];
}
- (void)didDeleteObject:(NSManagedObject *)object
{
if (! object.entity.primaryKeyAttribute) return;
RKEntityCache *entityCache = [self cacheForEntity:object.entity inManagedObjectContext:object.managedObjectContext];
[entityCache removeObject:object];
}

View File

@@ -23,19 +23,38 @@
the primary key attribute and value for the desired object.
@param entity The Core Data entity for the type of object to be retrieved from the cache.
@param primaryKeyAttribute The name of the attribute that acts as the primary key for the entity.
@param primaryKeyValue The value for the primary key attribute of the object to be retrieved from the cache.
@param mmanagedObjectContext The managed object context to be searched for a matching instance.
@return A managed object that is an instance of the given entity with a primary key and value matching
the specified parameters, or nil if no object was found.
*/
- (NSManagedObject *)findInstanceOfEntity:(NSEntityDescription *)entity
withPrimaryKeyValue:(id)primaryKeyValue
withPrimaryKeyAttribute:(NSString *)primaryKeyAttribute
value:(id)primaryKeyValue
inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;
@optional
/**
Tells the receiver that an object was fetched and should be added to the cache.
@param object The object that was fetched from a managed object context.
*/
- (void)didFetchObject:(NSManagedObject *)object;
/**
Tells the receiver that an object was created and should be added to the cache.
@param object The object that was created in a managed object context.
*/
- (void)didCreateObject:(NSManagedObject *)object;
/**
Tells the receiver that an object was deleted and should be removed to the cache.
@param object The object that was deleted from a managed object context.
*/
- (void)didDeleteObject:(NSManagedObject *)object;
@end

View File

@@ -168,8 +168,10 @@
// If we have found the primary key attribute & value, try to find an existing instance to update
if (primaryKeyAttribute && primaryKeyValue && NO == [primaryKeyValue isEqual:[NSNull null]]) {
object = [self.objectStore.cacheStrategy findInstanceOfEntity:entity
withPrimaryKeyValue:primaryKeyValue inManagedObjectContext:[self.objectStore managedObjectContextForCurrentThread]];
object = [self.objectStore.cacheStrategy findInstanceOfEntity:entity
withPrimaryKeyAttribute:primaryKeyAttribute
value:primaryKeyValue
inManagedObjectContext:[self.objectStore managedObjectContextForCurrentThread]];
if (object && [self.objectStore.cacheStrategy respondsToSelector:@selector(didFetchObject:)]) {
[self.objectStore.cacheStrategy didFetchObject:object];