Skip to content
Open
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
100 changes: 91 additions & 9 deletions models/QuickBuilder.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ component accessors="true" transientCache="false" {

function init() {
variables._eagerLoad = [];
variables._parallelEagerLoading = false;
variables._globalScopesApplied = false;
variables._globalScopeExcludeAll = false;
variables._asMemento = false;
Expand Down Expand Up @@ -528,9 +529,11 @@ component accessors="true" transientCache="false" {
* @relationName A single relation name or array of relation
* names to eager load.
*
* @parallel If true, eager loads top-level relationships concurrently.
*
* @return QuickBuilder
*/
public any function with( required any relationName ) {
public any function with( required any relationName, boolean parallel = false ) {
if ( isSimpleValue( arguments.relationName ) && arguments.relationName == "" ) {
return this;
}
Expand All @@ -540,6 +543,7 @@ component accessors="true" transientCache="false" {
arrayWrap( arguments.relationName ),
true
);
variables._parallelEagerLoading = variables._parallelEagerLoading || arguments.parallel;

return this;
}
Expand Down Expand Up @@ -575,17 +579,95 @@ component accessors="true" transientCache="false" {
}
}

structEach( denestEagerLoads( variables._eagerLoad ), function( relationName, nestedEagerLoads ) {
entities = eagerLoadRelation(
relationName,
nestedEagerLoads,
entities
);
} );
var eagerLoads = denestEagerLoads( variables._eagerLoad );
if ( variables._parallelEagerLoading && eagerLoads.count() > 1 && supportsParallelEagerLoading() ) {
eagerLoadRelationsInParallel( eagerLoads, arguments.entities );
} else {
structEach( eagerLoads, function( relationName, nestedEagerLoads ) {
entities = eagerLoadRelation(
relationName,
nestedEagerLoads,
entities
);
} );
}

return arguments.entities;
}

/**
* Eager loads independent top-level relationships on separate threads.
*/
private void function eagerLoadRelationsInParallel( required struct eagerLoads, required array entities ) {
var threadNames = [];
var threadRelations = {};
var targetEntities = arguments.entities;

for ( var relationName in arguments.eagerLoads ) {
var threadName = "quick_eager_#replace( createUUID(), "-", "", "all" )#";
threadNames.append( threadName );
threadRelations[ threadName ] = relationName;
cfthread(
action = "run",
name = threadName,
relationName = relationName,
eagerLoadConfig = arguments.eagerLoads[ relationName ],
entities = targetEntities
) {
thread.entities = eagerLoadRelation(
attributes.relationName,
attributes.eagerLoadConfig,
attributes.entities
);
}
}

cfthread(
action = "join",
name = threadNames.toList(),
timeout = 60000
);

threadNames.each( function( threadName ) {
if ( cfthread[ threadName ].status == "TERMINATED" ) {
var threadError = cfthread[ threadName ].error;
throw(
type = "QuickParallelEagerLoadingException",
message = threadError.keyExists( "message" ) ? threadError.message : "A parallel eager-loading thread failed.",
extendedInfo = serializeJSON( threadError )
);
}
if ( cfthread[ threadName ].status != "COMPLETED" ) {
throw(
type = "QuickParallelEagerLoadingTimeout",
message = "Parallel eager loading did not complete within 60 seconds."
);
}

var relationName = threadRelations[ threadName ];
var eagerLoadedEntities = cfthread[ threadName ].entities;
for ( var i = 1; i <= targetEntities.len(); i++ ) {
if ( structKeyExists( targetEntities[ i ], "isQuickEntity" ) ) {
var relationshipValue = eagerLoadedEntities[ i ].retrieveRelationship( relationName );
if ( isNull( relationshipValue ) ) {
targetEntities[ i ].assignRelationship( relationName );
} else {
targetEntities[ i ].assignRelationship( relationName, relationshipValue );
}
} else if ( eagerLoadedEntities[ i ].keyExists( relationName ) ) {
targetEntities[ i ][ relationName ] = eagerLoadedEntities[ i ][ relationName ];
}
}
} );
}

/**
* Adobe ColdFusion loses CFC private-method resolution inside cfthread.
*/
private boolean function supportsParallelEagerLoading() {
return !findNoCase( "ColdFusion", server.coldfusion.productName );
}

private struct function denestEagerLoads( required array eagerLoads ) {
// this comes in as an array of items which can be:
// 1. dot-delimited strings (e.g., "videos.tags")
Expand Down Expand Up @@ -731,7 +813,7 @@ component accessors="true" transientCache="false" {
* @doc_generic quick.models.BaseEntity | struct
* @return [quick.models.BaseEntity] | [struct]
*/
private array function eagerLoadRelation(
public array function eagerLoadRelation(
required string relationName,
required struct eagerLoadConfig,
required array entities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,45 @@ component extends="tests.resources.ModuleIntegrationSpec" {
}
} );

it( "can eager load top-level relationships in parallel", function() {
var callingThread = createObject( "java", "java.lang.Thread" ).currentThread().getName();
var eagerThreads = {};
var posts = getInstance( "Post" )
.with(
[
{
"author" : function( relationship ) {
eagerThreads.author = createObject( "java", "java.lang.Thread" )
.currentThread()
.getName();
}
},
{
"comments" : function( relationship ) {
eagerThreads.comments = createObject( "java", "java.lang.Thread" )
.currentThread()
.getName();
}
}
],
true
)
.get();

expect( posts[ 1 ].getAuthor() ).toBeInstanceOf( "app.models.User" );
expect( posts[ 1 ].getComments() ).toBeArray();
expect( eagerThreads ).toHaveKey( "author" );
expect( eagerThreads ).toHaveKey( "comments" );
if ( findNoCase( "ColdFusion", server.coldfusion.productName ) ) {
expect( eagerThreads.author ).toBe( callingThread );
expect( eagerThreads.comments ).toBe( callingThread );
} else {
expect( eagerThreads.author ).notToBe( callingThread );
expect( eagerThreads.comments ).notToBe( callingThread );
expect( eagerThreads.author ).notToBe( eagerThreads.comments );
}
} );

it( "can eager load a belongs to relationship using a composite key", function() {
var compositeChildren = getInstance( "CompositeChild" ).with( "parent" ).get();
expect( compositeChildren ).toBeArray();
Expand Down
Loading