Skip to content

Commit

Permalink
Added CBLModel support for inverse relations [API CHANGE]
Browse files Browse the repository at this point in the history
* -[CBLModel findInverseOfRelation:fromClass:] explicitly computes an
  inverse relation.
* An array-of-models-valued property can be declared as a computed
  inverse relation by overriding +inverseRelationForArrayProperty or by
  implementing +{property}InverseRelation. (See doc-comments)

Fixes #606
  • Loading branch information
snej committed Feb 20, 2015
1 parent d14c96f commit 5af201e
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 18 deletions.
33 changes: 30 additions & 3 deletions Source/API/CBLModel+Properties.m
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ - (void) setArray: (NSArray*)array
}


+ (BOOL) hasRelation: (NSString*)relation {
objc_property_t property = class_getProperty(self, relation.UTF8String);
if (!property)
return NO;
char* dyn = property_copyAttributeValue(property, "D");
if (!dyn)
return NO;
free(dyn);
return YES;
}


#pragma mark - DYNAMIC METHOD GENERATORS:


Expand All @@ -208,9 +220,24 @@ + (IMP) impForGetterOfProperty: (NSString*)property ofClass: (Class)propertyClas
};
} else if ([itemClass isSubclassOfClass: [CBLModel class]]) {
// Array of models (a to-many relation):
impBlock = ^id(CBLModel* receiver) {
return [receiver getArrayRelationProperty: property withModelClass: itemClass];
};
NSString* inverse = [[self class] inverseRelationForArrayProperty: property];
if (inverse) {
// This is a computed (queried) inverse relation:
LogTo(CBLModel, @"%@.%@ is a query-based inverse of %@.%@",
self, property, itemClass, inverse);
Assert([itemClass hasRelation: inverse],
@"%@.%@ specified as inverse of %@.%@, which is not a valid relation",
self, property, itemClass, inverse);
impBlock = ^id(CBLModel* receiver) {
return [receiver findInverseOfRelation: inverse fromClass: itemClass];
};
} else {
// This is an explicit array of docIDs:
LogTo(CBLModel, @"%@.%@ is an explicit array of %@", self, property, itemClass);
impBlock = ^id(CBLModel* receiver) {
return [receiver getArrayRelationProperty: property withModelClass: itemClass];
};
}
} else {
// Typed array of scalar class:
ValueConverter itemConverter = valueConverterToClass(itemClass);
Expand Down
31 changes: 31 additions & 0 deletions Source/API/CBLModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ NS_REQUIRES_PROPERTY_DEFINITIONS // Don't let compiler auto-synthesize properti
ofProperty: (NSString*)property;


/** Follows an _inverse_ relationship: returns the other models in the database that have a
property named `inverseProperty` that points to this object. For example, if model class
ListItem has a property 'list' that's a relation to a List model, then calling this method
on a List instance, with relation 'list', will return all the ListItems that refer to this List.
Specifically, what this does is run a CBLQuery that finds documents whose `relation`
property value is equal to the document ID of the receiver. (And if `fromClass` is given,
it's restricted to documents whose `type` property is one of the ones mapped to `fromClass`
in the CBLModelFactory.)
@param relation The property name to look at
@param fromClass (Optional) The CBLModel subclass to restrict the search to.
@return An array of model objects found, or nil on error. */
- (NSArray*) findInverseOfRelation: (NSString*)relation
fromClass: (nullable Class)fromClass;


/** The names of all attachments (array of strings).
This reflects unsaved changes made by creating or deleting attachments. */
@property (readonly, nullable) NSArray* attachmentNames;
Expand Down Expand Up @@ -188,6 +204,21 @@ NS_REQUIRES_PROPERTY_DEFINITIONS // Don't let compiler auto-synthesize properti
than overriding this one. */
+ (nullable Class) itemClassForArrayProperty: (NSString*)property;

/** General method for declaring the that an array-of-models-valued property is a computed inverse
of a relation from another class.
Given the property name, the override should return the name of the relation property in the
item class (the one returned by +itemClassForArrayProperty:). If it returns nil, then this
property will be interpreted as an explicit JSON property whose value is an array of strings
corresponding to the other models.
The default implementation of this method checks for the existence of a class method with
selector of the form +propertyInverseRelation where 'property' is replaced by the actual
property name. If such a method exists it is called, and must return a string.
In general you'll find it easier to implement the '+propertyInverseRelation' method(s) rather
than overriding this one. */
+ (nullable NSString*) inverseRelationForArrayProperty: (NSString*)property;

/** The type of document. This is optional, but is commonly used in document databases
to distinguish different types of documents. CBLModelFactory can use this property to
determine what CBLModel subclass to instantiate for a document. */
Expand Down
53 changes: 53 additions & 0 deletions Source/API/CBLModel.m
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,15 @@ + (Class) itemClassForArrayProperty: (NSString*)property {
return Nil;
}

+ (NSString*) inverseRelationForArrayProperty: (NSString*)property {
SEL sel = NSSelectorFromString([property stringByAppendingString: @"InverseRelation"]);
if ([self respondsToSelector: sel]) {
return (NSString*)objc_msgSend(self, sel);
}
return nil;
}



- (CBLDatabase*) databaseForModelProperty: (NSString*)property {
// This is a hook for subclasses to override if they need to, i.e. if the property
Expand Down Expand Up @@ -523,6 +532,50 @@ - (CBLModel*) modelWithDocID: (NSString*)docID
}


// Queries to find the value of a model-valued array property that's an inverse relation.
- (NSArray*) findInverseOfRelation: (NSString*)relation
fromClass: (Class)fromClass
{
CBLModelFactory* factory = self.database.modelFactory;
CBLQueryBuilder* builder = [factory queryBuilderForClass: fromClass property: relation];
if (!builder) {
NSPredicate* pred;
if (fromClass) {
NSArray* types = [self.database.modelFactory documentTypesForClass: fromClass];
Assert(types.count > 0, @"Class %@ is not registered for any document types",
fromClass);
pred = [NSPredicate predicateWithFormat: @"type in %@ and %K = $DOCID",
types, relation];
} else {
pred = [NSPredicate predicateWithFormat: @"%K = $DOCID", relation];
}
NSError* error;
builder = [[CBLQueryBuilder alloc] initWithDatabase: self.database
select: nil
wherePredicate: pred
orderBy: nil
error: &error];
Assert(builder, @"Couldn't create query builder: %@", error);
[factory setQueryBuilder: builder forClass: fromClass property:relation];
}

CBLQuery* q = [builder createQueryWithContext: @{@"DOCID": self.document.documentID}];
NSError* error;
CBLQueryEnumerator* e = [q run: &error];
if (!e) {
Warn(@"Querying for inverse of %@.%@ failed: %@", fromClass, relation, error);
return nil;
}
NSMutableArray* docIDs = $marray();
for (CBLQueryRow* row in e)
[docIDs addObject: row.documentID];
return [[CBLModelArray alloc] initWithOwner: self
property: nil
itemClass: fromClass
docIDs: docIDs];
}


#pragma mark - ATTACHMENTS:


Expand Down
8 changes: 4 additions & 4 deletions Source/API/CBLModelArray.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
/** Initializes a model array from an array of document ID strings.
Returns nil if docIDs contains items that are non-strings, or invalid document IDs. */
- (instancetype) initWithOwner: (CBLModel*)owner
property: (NSString*)property
itemClass: (Class)itemClass
property: (nullable NSString*)property
itemClass: (nullable Class)itemClass
docIDs: (NSArray*)docIDs;

/** Initializes a model array from an array of CBLModels. */
- (instancetype) initWithOwner: (CBLModel*)owner
property: (NSString*)property
itemClass: (Class)itemClass
property: (nullable NSString*)property
itemClass: (nullable Class)itemClass
models: (NSArray*)models;

@property (readonly) NSArray* docIDs;
Expand Down
13 changes: 8 additions & 5 deletions Source/API/CBLModelFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
If the document's modelObject property is set, it returns that value.
If the document's "type" property has been registered, instantiates the associated class.
Otherwise returns nil. */
- (id) modelForDocument: (CBLDocument*)document __attribute__((nonnull));
- (id) modelForDocument: (CBLDocument*)document;

/** Associates a value of the "type" property with a CBLModel subclass.
When a document with this type value is loaded as a model, the given subclass will be
Expand All @@ -39,18 +39,21 @@
@param classOrName Either a CBLModel subclass, or its class name as an NSString.
@param type The value value of a document's "type" property that should indicate this class. */
- (void) registerClass: (id)classOrName
forDocumentType: (NSString*)type __attribute__((nonnull(2)));
forDocumentType: (NSString*)type;

/** Returns the appropriate CBLModel subclass for this document.
The default implementation just passes the document's "type" property value to -classForDocumentType:, but subclasses could override this to use different properties (or even the document ID) to decide. */
- (nullable Class) classForDocument: (CBLDocument*)document __attribute__((nonnull));
- (nullable Class) classForDocument: (CBLDocument*)document;

/** Looks up the CBLModel subclass that's been registered for a document type. */
- (Class) classForDocumentType: (NSString*)type __attribute__((nonnull));
- (Class) classForDocumentType: (NSString*)type;

/** Looks up the document type for which the given class has been registered.
If it's unregistered, or registered with multiple types, returns nil. */
- (nullable NSString*) documentTypeForClass: (Class)modelClass __attribute__((nonnull));
- (nullable NSString*) documentTypeForClass: (Class)modelClass;

/** Looks up the document types for which the given class has been registered. */
- (NSArray*) documentTypesForClass: (Class)modelClass;

@end

Expand Down
31 changes: 27 additions & 4 deletions Source/API/CBLModelFactory.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@implementation CBLModelFactory
{
NSMutableDictionary* _typeDict;
NSMutableDictionary* _queryBuilders;
}


Expand Down Expand Up @@ -62,13 +63,16 @@ - (Class) classForDocumentType: (NSString*)type {
return klass;
}

- (NSString*) documentTypeForClass: (Class)modelClass {
- (NSArray*) documentTypesForClass: (Class)modelClass {
NSArray *keys = [_typeDict allKeysForObject:modelClass];
if (keys.count == 0)
keys = [_typeDict allKeysForObject: NSStringFromClass(modelClass)];
if (keys.count != 1)
return nil; // Either not found, or ambiguous (multiple types registered)
return keys.firstObject;
return keys;
}

- (NSString*) documentTypeForClass: (Class)modelClass {
NSArray *keys = [self documentTypesForClass: modelClass];
return keys.count == 1 ? keys.firstObject : nil;
}

- (Class) classForDocument: (CBLDocument*)document {
Expand All @@ -85,4 +89,23 @@ - (id) modelForDocument: (CBLDocument*)document {
}


- (void) setQueryBuilder: (CBLQueryBuilder*)builder
forClass: (Class)klass
property: (NSString*)property
{
id key = [[NSArray alloc] initWithObjects: property, klass, nil]; // klass might be nil
if (!_queryBuilders)
_queryBuilders = [[NSMutableDictionary alloc] init];
_queryBuilders[key] = builder;
}

- (CBLQueryBuilder*) queryBuilderForClass: (Class)klass
property: (NSString*)property
{
id key = [[NSArray alloc] initWithObjects: property, klass, nil]; // klass might be nil
return _queryBuilders[key];
}



@end
1 change: 1 addition & 0 deletions Source/API/CBLQuery.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ @implementation CBLQuery

// A nil view refers to 'all documents'
- (instancetype) initWithDatabase: (CBLDatabase*)database view: (CBLView*)view {
Assert(database);
self = [super init];
if (self) {
_database = database;
Expand Down
5 changes: 4 additions & 1 deletion Source/API/CBLView.m
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ - (CBLQueryIteratorBlock) _queryWithOptions: (CBLQueryOptions*)options
iterator = [_storage reducedQueryWithOptions: options status: outStatus];
else
iterator = [_storage regularQueryWithOptions: options status: outStatus];
LogTo(Query, @"Query %@: Returning iterator", _name);
if (iterator)
LogTo(Query, @"Query %@: Returning iterator", _name);
else
LogTo(Query, @"Query %@: Failed with status %d", _name, *outStatus);
return iterator;
}

Expand Down
9 changes: 9 additions & 0 deletions Source/API/CouchbaseLitePrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,12 @@ NSString* CBLKeyPathForQueryRow(NSString* keyPath); // for testing
pull: (BOOL)pull __attribute__((nonnull));
@property (nonatomic, readonly) NSDictionary* properties;
@end


@interface CBLModelFactory ()
- (CBLQueryBuilder*) queryBuilderForClass: (Class)klass
property: (NSString*)property;
- (void) setQueryBuilder: (CBLQueryBuilder*)builder
forClass: (Class)klass
property: (NSString*)property;
@end
58 changes: 57 additions & 1 deletion Unit-Tests/Model_Tests.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ @interface TestModel : CBLModel
@property int Capitalized;

@property unsigned reloadCount;

@property NSArray* otherModels; // inverse of TestOtherModel.model
@end


Expand All @@ -67,6 +69,13 @@ @interface CBL_TestAwakeInitModel : CBLModel
@property BOOL didAwake;
@end


@interface TestOtherModel : CBLModel
@property int number;
@property TestModel* model;
@end


#pragma mark - TEST CASES:


Expand Down Expand Up @@ -558,6 +567,41 @@ - (void) test00_AwakeFromInitializer {
Assert([model save: &error], @"Save of new model object failed: %@", error);
}


- (void) test00_InverseRelation {
// Create two TestModels as targets for the 'model' relation:
[db.modelFactory registerClass: [TestModel class] forDocumentType: @"test"];
[db.modelFactory registerClass: [TestOtherModel class] forDocumentType: @"other"];
TestModel* model1 = [TestModel modelForNewDocumentInDatabase: db];
model1.number = 1;
TestModel* model2 = [TestModel modelForNewDocumentInDatabase: db];
model2.number = 2;

// Create 100 TestOtherModels whose 'model' properties point to the above TestModels:
for (int i = 0; i < 50; i++) {
TestOtherModel* other = [TestOtherModel modelForNewDocumentInDatabase: db];
other.number = i;
other.model = (i % 2) ? model1 : model2;
}

NSError* error;
Assert([db saveAllModels: &error], @"Save failed: %@", error);

// Now query:
NSArray* result1 = model1.otherModels;
AssertEq(result1.count, 25u);
for (TestOtherModel* m in result1) {
AssertEq([m class], [TestOtherModel class]);
AssertEq(m.number % 2, 1);
}
NSArray* result2 = model2.otherModels;
AssertEq(result2.count, 25u);
for (TestOtherModel* m in result2) {
AssertEq([m class], [TestOtherModel class]);
AssertEq(m.number % 2, 0);
}
}

@end


Expand All @@ -566,7 +610,7 @@ @implementation TestModel
@dynamic number, uInt, sInt16, uInt16, sInt8, uInt8, nsInt, nsUInt, sInt32, uInt32;
@dynamic sInt64, uInt64, boolean, boolObjC, floaty, doubly, dict;
@dynamic str, data, date, decimal, url, other, strings, dates, others, Capitalized;
@dynamic subModel, subModels, mutableSubModel;
@dynamic subModel, subModels, mutableSubModel, otherModels;
@synthesize reloadCount;

- (void) didLoadFromDocument {
Expand All @@ -586,6 +630,14 @@ + (Class) subModelsItemClass {
return [TestSubModel class];
}

+ (Class) otherModelsItemClass {
return [TestOtherModel class];
}

+ (NSString*) otherModelsInverseRelation {
return @"model";
}

@end


Expand Down Expand Up @@ -655,4 +707,8 @@ - (void) awakeFromInitializer {

@end

@implementation TestOtherModel
@dynamic number, model;
@end


0 comments on commit 5af201e

Please sign in to comment.