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
82 changes: 75 additions & 7 deletions models/BaseEntity.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1504,17 +1504,71 @@ component accessors="true" {
}

/**
* Retrieves the result of a loaded relationship.
* If there is no data, returns null instead.
* Retrieves the result of a loaded relationship. If the relationship has not
* been loaded, initializes and returns its relationship type default without
* executing a query. An explicit default value can be supplied instead.
*
* @name The relationship name to retrieve.
* @name The relationship name to retrieve.
* @defaultValue An optional value to assign and return when the relationship
* has not been loaded.
*
* @return quick.models.BaseEntity | [quick.models.BaseEntity]
*/
public any function retrieveRelationship( required string name ) {
return variables._relationshipsData.keyExists( arguments.name ) ? variables._relationshipsData[ arguments.name ] : javacast(
"null",
""
public any function retrieveRelationship( required string name, any defaultValue ) {
if ( !hasRelationship( arguments.name ) ) {
throwRelationshipNotFound( arguments.name );
}
if ( variables._relationshipsData.keyExists( arguments.name ) ) {
return variables._relationshipsData[ arguments.name ];
}
if ( isRelationshipLoaded( arguments.name ) ) {
return javacast( "null", "" );
}

var relationship = resolveRelationship( arguments.name );
if ( arguments.keyExists( "defaultValue" ) ) {
assignRelationship( arguments.name, arguments.defaultValue );
return arguments.defaultValue;
}

relationship.initRelation( [ this ], arguments.name );
return variables._relationshipsData.keyExists( arguments.name )
? variables._relationshipsData[ arguments.name ]
: javacast( "null", "" );
}

/**
* Resolves and validates a relationship definition by name.
*
* @name The relationship method name to resolve.
*
* @throws RelationshipNotFound
*
* @return quick.models.Relationships.BaseRelationship
*/
private any function resolveRelationship( required string name ) {
var relationshipName = arguments.name;
var relationship = ignoreLoadedGuard( function() {
return invoke( this, relationshipName );
} );
if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) {
throwRelationshipNotFound( arguments.name );
}
relationship.setRelationMethodName( arguments.name );
return relationship;
}

/**
* Throws a consistent exception for an unknown relationship name.
*
* @name The unknown relationship name.
*
* @throws RelationshipNotFound
*/
private void function throwRelationshipNotFound( required string name ) {
throw(
type = "RelationshipNotFound",
message = "The [#arguments.name#] relationship was not found on the [#entityName()#] entity."
);
}

Expand Down Expand Up @@ -2504,6 +2558,20 @@ component accessors="true" {
return;
}

if ( !isRelationshipLoaded( relationshipName ) && !isLoaded() ) {
var relationshipArguments = arguments.missingMethodArguments;
var unloadedRelationship = ignoreLoadedGuard( function() {
return invoke(
this,
relationshipName,
relationshipArguments
);
} );
unloadedRelationship.setRelationMethodName( relationshipName );
unloadedRelationship.initRelation( [ this ], relationshipName );
return retrieveRelationship( relationshipName );
}

if ( !isRelationshipLoaded( relationshipName ) && variables._preventLazyLoading ) {
variables._lazyLoadingViolationCallback( this, relationshipName );
}
Expand Down
17 changes: 17 additions & 0 deletions models/Relationships/BaseRelationship.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ component accessors="true" implements="IRelationship" {
return this;
}

/**
* Initializes a relationship to its unloaded default value.
* To-one relationships default to null. Collection relationships override
* this method to initialize an empty array.
*
* @entities The entities on which to initialize the relationship.
* @relation The relationship name to initialize.
*
* @return [quick.models.BaseEntity]
*/
public array function initRelation( required array entities, required string relation ) {
return arguments.entities.map( function( entity ) {
arguments.entity.assignRelationship( arguments.relation, javacast( "null", "" ) );
return arguments.entity;
} );
}

/**
* Retrieves the entities for eager loading.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,57 @@ component extends="tests.resources.ModuleIntegrationSpec" {
variables.queries = [];
} );

it( "returns empty relationship values for new entities", function() {
var post = getInstance( "Post" );
var user = getInstance( "User" );

expect( post.getAuthor() ).toBeNull();
expect( user.getLatestPost() ).toBeNull();
expect( user.getPosts() ).toBeArray().toBeEmpty();
expect( post.getTags() ).toBeArray().toBeEmpty();
expect( variables.queries ).toBeEmpty();
} );

it( "includes empty relationships in mementos for new entities", function() {
var memento = getInstance( "User" ).getMemento( includes = [ "latestPost", "posts" ] );

expect( memento.latestPost ).toBe( "" );
expect( memento.posts ).toBeArray().toBeEmpty();
expect( variables.queries ).toBeEmpty();
} );

it( "retrieves and caches the relationship type default without querying", function() {
var user = getInstance( "User" );
var post = getInstance( "Post" );

expect( user.retrieveRelationship( "posts" ) ).toBeArray().toBeEmpty();
expect( post.retrieveRelationship( "author" ) ).toBeNull();
expect( post.retrieveRelationship( "authorWithEmptyDefault" ) ).toBeInstanceOf( "User" );
expect( user.isRelationshipLoaded( "posts" ) ).toBeTrue();
expect( post.isRelationshipLoaded( "author" ) ).toBeTrue();
expect( variables.queries ).toBeEmpty();
} );

it( "accepts a default relationship value", function() {
var user = getInstance( "User" );
var seededPost = getInstance( "Post" ).fill( { "body" : "seeded" } );

var posts = user.retrieveRelationship( "posts", [ seededPost ] );

expect( posts ).toHaveLength( 1 );
expect( posts[ 1 ].getBody() ).toBe( "seeded" );
expect( user.isRelationshipLoaded( "posts" ) ).toBeTrue();
expect( variables.queries ).toBeEmpty();
} );

it( "throws when retrieving an unknown relationship", function() {
var post = getInstance( "Post" );

expect( function() {
post.retrieveRelationship( "missingRelationship" );
} ).toThrow( "RelationshipNotFound" );
} );

describe( "Eager Loading Spec", function() {
it( "can load a relationship for an entity", function() {
var elpete = getInstance( "User" ).where( "username", "elpete" ).firstOrFail();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ component extends="tests.resources.ModuleIntegrationSpec" {

function run() {
describe( "WithDefault Spec", function() {
it( "will throw an exception when retrieving a relation on an unloaded entity", function() {
var post = getInstance( "Post" );
it( "returns a configured default for a relation on an unloaded entity", function() {
var post = getInstance( "Post" );
var author = post.getAuthorWithEmptyDefault();

expect( function() {
post.getAuthor();
} ).toThrow( message = "Retrieving an unloaded entity should throw an exception" );
expect( author ).toBeInstanceOf( "User" );
expect( author.isLoaded() ).toBeFalse( "A default model is not loaded" );
expect( author.retrieveAttributesData() ).toBeEmpty();
} );

it( "can load a entity and return a default entity if there is no owning entity", function() {
Expand Down
9 changes: 1 addition & 8 deletions tests/specs/integration/GoodErrorMessagesSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,13 @@ component extends="tests.resources.ModuleIntegrationSpec" {
} ).toThrow( type = "QuickEntityDefaultedKey" );
} );

it( "throws a helpful error message when trying to access relationships on unloaded entities", function() {
it( "throws a helpful error message when trying to query relationships on unloaded entities", function() {
expect( function() {
getInstance( "User" ).posts();
} ).toThrow(
type = "QuickEntityNotLoaded",
regex = "This instance is not loaded so it cannot access the \[posts\] relationship\. Either load the entity from the database using a query executor \(like \`first\`\) or base your query off of the \[Post\] entity directly and use the \`has\` or \`whereHas\` methods to constrain it based on data in \[User\]\."
);

expect( function() {
getInstance( "User" ).getPosts();
} ).toThrow(
type = "QuickEntityNotLoaded",
regex = "This instance is not loaded so it cannot access the \[posts\] relationship\. Either load the entity from the database using a query executor \(like \`first\`\) or base your query off of the \[Post\] entity directly and use the \`has\` or \`whereHas\` methods to constrain it based on data in \[User\]\."
);
} );

it(
Expand Down
Loading