Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions models/QuickQB.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,105 @@ component
return super.whereExists( argumentCollection = arguments );
}

/**
* Constrains the query to entities belonging to one or more related entities.
* The relationship name defaults to the lower-camel-cased related entity name.
*
* @relationshipName The belongsTo relationship to use.
* A related entity can be passed here as a shortcut that infers the relationship name.
* @related A related Quick entity, an array of entities, or a collection of entities.
* @combinator The boolean combinator for the clause. Default: "and".
*
* @return quick.models.QuickQB
*/
public QuickQB function whereBelongsTo(
required any relationshipName,
any related,
string combinator = "and"
) {
if ( isNull( arguments.related ) ) {
arguments.related = arguments.relationshipName;
arguments.delete( "relationshipName" );
}

var relatedEntities = isArray( arguments.related )
? arguments.related
: (
isStruct( arguments.related ) && structKeyExists( arguments.related, "isQuickEntity" )
? [ arguments.related ]
: arguments.related.get()
);

if ( relatedEntities.isEmpty() ) {
throw(
type = "QuickInvalidWhereBelongsTo",
message = "whereBelongsTo requires at least one related entity."
);
}

if ( !arguments.keyExists( "relationshipName" ) || isNull( arguments.relationshipName ) ) {
var relatedEntityName = relatedEntities[ 1 ].entityName();
arguments.relationshipName = lCase( left( relatedEntityName, 1 ) ) & removeChars( relatedEntityName, 1, 1 );
}
var resolvedRelationshipName = arguments.relationshipName;

var relation = getEntity().ignoreLoadedGuard( function() {
return getEntity().withoutRelationshipConstraints( resolvedRelationshipName, function() {
return invoke( getEntity(), resolvedRelationshipName );
} );
} );

if ( relation.relationshipClass != "BelongsTo" ) {
throw(
type = "QuickInvalidWhereBelongsTo",
message = "Relationship [#resolvedRelationshipName#] must be a belongsTo relationship."
);
}

var relatedMapping = relation.getRelated().mappingName();
var foreignKeys = relation.getForeignKeys();
var localKeys = relation.getLocalKeys();
relatedEntities.each( function( relatedEntity ) {
if (
!isStruct( relatedEntity ) ||
!structKeyExists( relatedEntity, "isQuickEntity" ) ||
relatedEntity.mappingName() != relatedMapping
) {
throw(
type = "QuickInvalidWhereBelongsTo",
message = "All whereBelongsTo entities must match [#relatedMapping#]."
);
}
} );

return where(
column = function( q ) {
relatedEntities.each( function( relatedEntity ) {
q.orWhere( function( q2 ) {
for ( var i = 1; i <= foreignKeys.len(); i++ ) {
q2.where( foreignKeys[ i ], relatedEntity.retrieveAttribute( localKeys[ i ] ) );
}
} );
} );
},
combinator = arguments.combinator
);
}

/**
* Adds a whereBelongsTo constraint using an OR combinator.
*
* @relationshipName The belongsTo relationship to use.
* A related entity can be passed here as a shortcut that infers the relationship name.
* @related A related Quick entity, an array of entities, or a collection of entities.
*
* @return quick.models.QuickQB
*/
public QuickQB function orWhereBelongsTo( required any relationshipName, any related ) {
arguments.combinator = "or";
return whereBelongsTo( argumentCollection = arguments );
}

/**
* Checks for the existence of a relationship when executing the query.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,60 @@ component extends="tests.resources.ModuleIntegrationSpec" {

function run() {
describe( "Querying Relationships Spec", function() {
describe( "whereBelongsTo", function() {
it( "constrains a query using a named belongsTo relationship", function() {
var author = getInstance( "User" ).findOrFail( 1 );
var posts = getInstance( "Post" ).whereBelongsTo( "author", author ).get();

expect( posts ).toHaveLength( 2 );
expectAll( posts ).toSatisfy( function( post ) {
return post.getUser_Id() == author.getId();
} );
} );

it( "infers a conventional belongsTo relationship name", function() {
var country = getInstance( "Country" ).findOrFail( "02B84D66-0AA0-F7FB-1F71AFC954843861" );
var users = getInstance( "User" ).whereBelongsTo( country ).get();

expect( users ).toHaveLength( 2 );
expectAll( users ).toSatisfy( function( user ) {
return user.getCountry_Id() == country.getId();
} );
} );

it( "constrains a query to an array of related entities", function() {
var authors = getInstance( "User" ).whereIn( "id", [ 1, 4 ] ).get();
var posts = getInstance( "Post" ).whereBelongsTo( "author", authors ).get();

expect( posts ).toHaveLength( 3 );
expectAll( posts ).toSatisfy( function( post ) {
return arrayFind( [ 1, 4 ], post.getUser_Id() ) > 0;
} );
} );

it( "supports composite belongsTo relationships", function() {
var parent = getInstance( "Composite" )
.where( "a", 1 )
.where( "b", 2 )
.firstOrFail();
var children = getInstance( "CompositeChild" ).whereBelongsTo( "parent", parent ).get();

expect( children ).toHaveLength( 1 );
expect( children[ 1 ].getComposite_A() ).toBe( 1 );
expect( children[ 1 ].getComposite_B() ).toBe( 2 );
} );

it( "supports an OR combinator", function() {
var author = getInstance( "User" ).findOrFail( 1 );
var posts = getInstance( "Post" )
.where( "post_pk", 7777 )
.orWhereBelongsTo( "author", author )
.get();

expect( posts ).toHaveLength( 3 );
} );
} );

describe( "has", function() {
describe( "hasMany", function() {
it( "can find only entities that have one or more related entities", function() {
Expand Down
Loading