All pages
Powered by GitBook
1 of 16

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Relationships

Relationships are the heart of any ORM engine. They let you interact with relational database tables in an object-oriented way.

Quick's relationship engine provides readable relationship types, extendible relations at runtime, eager loading, and much more.

Start by checking out the different relationship types.

Relationship Types

Relationship Types

belongsTo

Usage

A belongsTo relationship is a many-to-one relationship. For instance, a Post may belong to a User.

The first value passed to belongsTo is a WireBox mapping to the related entity.

Quick determines the foreign key of the relationship based on the entity name and key values. In this case, the Post entity is assumed to have a userId foreign key. You can override this by passing a foreign key in as the second argument:

If your parent entity does not use id as its primary key, or you wish to join the child entity to a different column, you may pass a third argument to the belongsTo method specifying your parent table's custom key.

The inverse of belongsTo is or .

Updating

To update a belongsTo relationship, use the associate method. associate takes the entity to associate as the only argument.

associate does not automatically save the entity. Make sure to call save when you are ready to persist your changes to the database.

Removing

To remove a belongsTo relationship, use the dissociate method.

dissociate does not automatically save the entity. Make sure to call save when you are ready to persist your changes to the database.

Relationship Setter

You can also influence the associated entities by calling "set" & relationshipName and passing in an entity or key value.

After executing this code, the post would be updated in the database to be associated with the user with an id of 1.

withDefault

BelongsTo relationships can be configured to return a default entity if no entity is found. This is done by calling withDefault on the relationship object.

Called this way will return a new unloaded entity with no data. You can also specify any default attributes data by passing in a struct of data to withDefault.

Signature

Returns a BelongsTo relationship between this entity and the entity defined by relationName.

Visualizer

hasOne

Usage

A hasOne relationship is a "one-to-one" relationship. For instance, a User entity might have an UserProfile entity attached to it.

The first value passed to hasOne is a WireBox mapping to the related entity.

Quick determines the foreign key of the relationship based on the entity name and key values. In this case, the UserProfile entity is assumed to have a userId foreign key. You can override this by passing a foreign key in as the second argument:

If your parent entity does not use id as its primary key, or you wish to join the child entity to a different column, you may pass a third argument to the hasOne method specifying your parent table's custom key.

The inverse of hasOne is . It is important to choose the right relationship for your database structure. hasOne assumes that the related model has the foreign key for the relationship.

withDefault

HasOne relationships can be configured to return a default entity if no entity is found. This is done by calling withDefault on the relationship object.

Called this way will return a new unloaded entity with no data. You can also specify any default attributes data by passing in a struct of data to withDefault.

Signature

Returns a HasOne relationship between this entity and the entity defined by relationName.

Visualizer

hasOne
hasMany
belongsTo
belongsToMany
hasManyThrough
hasOneThrough
belongsToThrough
polymorphicBelongsTo
polymorphicHasMany
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function user() {
       return belongsTo( "User" );
    }

}
return belongsTo( "User", "FK_userID" );
return belongsTo( "User", "FK_userID", "relatedPostId" );
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function posts() {
        return hasMany( "Post" );
    }

    function latestPost() {
        // remember, relationships are just queries!
        return hasOne( "Post" ).orderBy( "createdDate", "desc" );
    }

}
var post = getInstance( "Post" ).findOrFail(1);
var user = getInstance( "User" ).findOrFail(1);
post.user().associate( user );
post.save();
var post = getInstance( "Post" ).findOrFail(1);
post.user().dissociate();
post.save();
var post = getInstance( "Post" ).first();
post.setAuthor( 1 );
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function user() {
       return belongsTo( "User" ).withDefault();
    }

}
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function user() {
       return belongsTo( "User" ).withDefault( {
           "firstName": "Guest",
           "lastName": "Author"
       } );
    }

}

Name

Type

Required

Default

Description

relationName

string

true

The WireBox mapping for the related entity.

foreignKey

String | [String]

false

entityName() & keyNames()

The foreign key on the parent entity.

localKey

String | [String]

false

keyNames()

The local primary key on the parent entity.

relationMethodName

String

false

The method name called on the entity to produce this relationship.

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

DO NOT PASS A VALUE HERE UNLESS YOU KNOW WHAT YOU ARE DOING.

hasMany
hasOne
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function profile() {
       return hasOne( "UserProfile" );
    }

}
return hasOne( "UserProfile", "FK_userID" );
return hasOne( "UserProfile", "FK_userID", "profile_id" );
// UserProfile.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function user() {
        return belongsTo( "User" );
    }

}
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function profile() {
       return hasOne( "UserProfile" ).withDefault();
    }

}
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function profile() {
       return hasOne( "UserProfile" ).withDefault( {
           "showHints": true
       } );
    }

}

Name

Type

Required

Default

Description

relationName

string

true

The WireBox mapping for the related entity.

foreignKey

String | [String]

false

entityName() & keyNames()

The foreign key on the parent entity.

localKey

String | [String]

false

keyNames()

The local primary key on the parent entity.

relationMethodName

String

false

The method name called on the entity to produce this relationship.

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

<b></b>

DO NOT PASS A VALUE HERE UNLESS YOU KNOW WHAT YOU ARE DOING.

belongsTo

polymorphicBelongsTo

Usage

A polymorphicBelongsTo relationship is a many-to-one relationship. This relationship is used when an entity can belong to multiple types of entities. The classic example for this type of relationship is Posts, Videos, and Comments. For instance, a Comment may belong to a Post or a Video.

// Comment.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function post() {
        return polymorphicBelongsTo( "commentable" );
    }

}

The only value passed to polymorphicBelongsTo is a prefix for the polymorphic type. A common convention where is to add able to the end of the entity name, though this is not automatically done. In our example, this prefix is commentable. This tells quick to look for a commentable_type and a commentable_id column in our Comment entity. It stores our entity's mapping as the _type and our entity's primary key value as the _id.

When retrieving a polymorphicBelongsTo relationship the _id is used to retrieve a _type from the database.

The inverse of polymorphicBelongsTo is also polymorphicHasMany. It is important to choose the right relationship for your database structure. hasOne assumes that the related model has the foreign key for the relationship.

// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function comments() {
       return polymorphicHasMany( "Comment", "commentable" );
    }

}
// Video.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function comments() {
        return polymorphicHasMany( "Comment", "commentable" );
    }

}

Signature

Name

Type

Required

Default

Description

name

String

false

relationMethodName

The name given to the polymorphic relationship.

type

String

false

name & "_type"

The column name that defines the type of the polymorphic relationship.

id

String

false

name & "_id"

The column name that defines the id of the polymorphic relationship.

localKey

String | [String]

false

related.keyNames()

The column name on the realted entity that is referred to by the foreignKey of the parent entity.

relationMethodName

String

false

The method name called on the entity to produce this relationship.

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

DO NOT PASS A VALUE HERE UNLESS YOU KNOW WHAT YOU ARE DOING.

Returns a polymorphicBelongsTo relationship between this entity and the entity defined by relationName.

Visualizer

hasOneThrough

Usage

A hasOneThrough relationship is either a many-to-one or a one-to-one relationship. It is used when you want to access a related entity through one or more intermediate entities. For instance, a Team may have one latest Post through a User.

The only value needed for hasOneThrough is an array of relationship function names to walk through to get to the related entity. The first relationship function name in the array must exist on the current entity. Each subsequent function name must exist on the related entity of the previous relationship result. For our previous example, members must be a relationship function on Team. posts must then be a relationship function on the related entity resulting from calling Team.members(). This returns a hasMany relationship where the related entity is User. So, User must have a posts relationship function. That is the end of the relationship function names array, so the related entity resulting from calling User.posts() is our final entity which is Post.

This approach can scale to as many related entities as you need. For instance, let's expand the previous example to include an Office that houses many Teams.

withDefault

HasOneThrough relationships can be configured to return a default entity if no entity is found. This is done by calling withDefault on the relationship object.

Called this way will return a new unloaded entity with no data. You can also specify any default attributes data by passing in a struct of data to withDefault.

Signature

polymorphicHasMany

Usage

A polymorphicHasMany relationship is a one-to-many relationship. This relationship is used when an entity can belong to multiple types of entities. The classic example for this type of relationship is Posts, Videos, and Comments.

The first value passed to polymophicHasMany is a WireBox mapping to the related entity.

The second value is a prefix for the polymorphic type. A common convention where is to add able to the end of the entity name, though this is not automatically done. In our example, this prefix is commentable. This tells quick to look for a commentable_type and a commentable_id column in our Comment entity. It stores our entity's mapping as the _type and our entity's primary key value as the _id.

The inverse of polymophicHasMany is polymorphicBelongsTo.

Signature

Returns a polymorphicHasMany relationship between this entity and the entity defined by relationName.

Visualizer

// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function latestPost() {
        return hasOneThrough( [ "members", "posts" ] )
            .orderByDesc( "publishedDate" );
    }
    
    function members() {
        return hasMany( "User" );
    }

}
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function posts() {
        return hasMany( "Post" );
    }
    
    function team() {
        return belongsTo( "Team" );
    }

}
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function author() {
        return belongsTo( "Post" );
    }

}
hasOneThrough( [ "members", "posts" ] );

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| Team           | members                   | User           |
+----------------+---------------------------+----------------+
| User           | posts                     | Post           |
+----------------+---------------------------+----------------+
// Office.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function latestPost() {
        return hasOneThrough( [ "teams", "members", "posts" ] )
            .orderByDesc( "publishedDate" );
    }
    
    function teams() {
        return hasMany( "Team" );
    }

}
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function members() {
        return hasMany( "User" );
    }
    
    function office() {
        return belongsTo( "Office" );
    }

}
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function team() {
        return belongsTo( "Team" );
    }
    
    function posts() {
        return hasMany( "Post" );
    }

}
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function author() {
        return belongsTo( "User" );
    }

}
hasOneThrough( [ "teams", "members", "posts" ] )

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| Office         | teams                     | Team           |
+----------------+---------------------------+----------------+
| Team           | members                   | User           |
+----------------+---------------------------+----------------+
| User           | posts                     | Post           |
+----------------+---------------------------+----------------+
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function latestPost() {
        return hasOneThrough( [ "members", "posts" ] )
            .orderByDesc( "publishedDate" )
            .withDefault();
    }
    
    function members() {
        return hasMany( "User" );
    }

}
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function latestPost() {
        return hasOneThrough( [ "members", "posts" ] )
            .orderByDesc( "publishedDate" )
            .withDefault( {
                "title": "Your next great post!"
            } );
    }
    
    function members() {
        return hasMany( "User" );
    }

}

Name

Type

Required

Default

Description

relationships

array

true

An array of relationship function names. The relationships are resolved from left to right. Each relationship will be resolved from the previously resolved relationship, starting with the current entity.

relationMethodName

string

false

Current Method Name

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

Retrieving Relationships

Relationships can be used in two ways.

The first is as a getter. Calling user.getPosts() will execute the relationship, cache the result, and return it.

var posts = user.getPosts();

The second is as a relationship. Calling user.posts() returns a Relationship instance to retrieve the posts that can be further constrained. A Relationship is backed by qb as well, so feel free to call any qb method to further constrain the relationship.

var newestPosts = user
    .posts()
    .orderBy( "publishedDate", "desc" )
    .get();

You can also call the other Quick fetch methods: first, firstOrFail, find, and findOrFail are all supported. This is especially useful to constrain the entities available to a user by using the user's relationships:

// This will only find posts the user has written.
var post = user.posts().findOrFail( rc.id );

Ordering By Relationships

To order by a relationship field, you will use a dot-delimited syntax.

getInstance( "Post" ).orderBy( "author.name" );

The last item in the dot-delimited string should be an attribute on the related entity.

Nested relationships are also supported. Continue to chain relationships in your dot-delimited string until arriving at the desired entity. Remember to end with the attribute to order by.

getInstance( "Post" ).orderBy( "author.team.name" );

If you desire to be explicit, you can use the orderByRelated method, which is what is being called under the hood.

Name

Type

Required

Default

Description

relationshipName

String | [String]

true

A dot-delimited string of relationship names or an array of relationship names to traverse to get to the related entity to order by.

columnName

string

true

The column name in the final relationship to order by.

direction

string

false

"asc"

The direction to sort, asc or desc.

getInstance( "Post" ).orderByRelated( "author.team", "name" );
// or
getInstance( "Post" ).orderByRelated( [ "author", "team" ], "name" );

You might prefer the explicitness of this method, but it cannot handle normal orders like orderBy. Use whichever method you prefer.

// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function comments() {
       return polymorphicHasMany( "Comment", "commentable" );
    }

}
// Video.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function comments() {
        return polymorphicHasMany( "Comment", "commentable" );
    }

}
// Comment.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function post() {
        return polymorphicBelongsTo( "commentable" );
    }

}

Name

Type

Required

Default

Description

relationName

String

true

The WireBox mapping for the related entity.

name

String

true

The name given to the polymorphic relationship.

type

String

false

name & "_type"

The column name that defines the type of the polymorphic relationship.

id

String

false

name & "_id"

The column name that defines the id of the polymorphic relationship.

localKey

String | [String]

false

keyNames()

The local primary key on the parent entity.

relationMethodName

String

false

The method name called on the entity to produce this relationship.

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

DO NOT PASS A VALUE HERE UNLESS YOU KNOW WHAT YOU ARE DOING.

hasMany

Usage

A hasMany relationship is a one-to-many relationship. For instance, a User may have multiple Posts.

// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function posts() {
       return hasMany( "Post" );
    }

}

The first value passed to hasMany is a WireBox mapping to the related entity.

Quick determines the foreign key of the relationship based on the entity name and key values. In this case, the Post entity is assumed to have a userId foreign key. You can override this by passing a foreign key in as the second argument:

return hasMany( "Post", "FK_userID" );

If your parent entity does not use id as its primary key, or you wish to join the child entity to a different column, you may pass a third argument to the hasMany method specifying your parent table's custom key.

return hasMany( "Post", "FK_userID", "relatedPostId" );

The inverse of hasMany is belongsTo.

// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function user() {
        return belongsTo( "User" );
    }

}

Inserting & Updating

There are two ways to add an entity to a hasMany relationship. Both mirror the insert API for entities.

save

You can call the save method on the relationship passing in an entity to relate.

var post = getInstance( "Post" ).create( {
    "title" = "My Post",
    "body" = "Hello, world!"
} );

var user = getInstance( "User" ).findOrFail( 1 );

user.posts().save( post );
// OR use the keyValue
user.posts().save( post.keyValue() );

This will add the User entity's id as a foreign key in the Post and save the Post to the database.

Note: the save method is called on the posts relationship, not the getPosts collection.

saveMany

You can also add many entities in a hasMany relationship by calling saveMany. This method takes an array of key values or entities and will associate each of them with the base entity.

create

Use the create method to create and save a related entity directly through the relationship.

var user = getInstance( "User" ).findOrFail( 1 );

user.posts().create( {
    "title" = "My Post",
    "body" = "Hello, world!"
} );

This example will have the same effect as the previous example.

Removing

Removing a hasMany relationship is handled in two ways: either by using the dissociate method on the belongsTo side of the relationship or by deleting the belongsTo side of the relationship.

Relationship Setter

You can also influence the associated entities by calling "set" & relationshipName and passing in an array of entities or key values.

var postA = getInstance( "Post" ).findOrFail( 2 );
user.setPosts( [ postA, 4 ] );

After running this code, this user would only have two posts, the posts with ids 2 and 4. Any other posts would now be disassociated with this user. Likely your database will be guarding against creating these orphan records. Admittedly, this method is not as likely to be used as the others, but it does exist if it solves your use case.

Signature

Name

Type

Required

Default

Description

relationName

string

true

The WireBox mapping for the related entity.

foreignKey

String | [String]

false

entityName() & keyNames()

The foreign key on the parent entity.

localKey

String | [String]

false

keyNames()

The local primary key on the parent entity.

relationMethodName

String

false

The method name called on the entity to produce this relationship.

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

<b></b>

DO NOT PASS A VALUE HERE UNLESS YOU KNOW WHAT YOU ARE DOING.

Returns a HasMany relationship between this entity and the entity defined by relationName.

Visualizer

belongsToMany

Usage

A belongsToMany relationship is a many-to-many relationship. For instance, a User may have multiple Permissions while a Permission can belong to multiple Users.

The first value passed to belongsToMany is a WireBox mapping to the related entity.

belongsToMany makes some assumptions about your table structure. To support a many-to-many relationship, you need a pivot table. This is, at its simplest, a table with each of the foreign keys as columns.

As you can see, Quick uses a convention of combining the entity table names in alphabetical order with an underscore (_) to create the new pivot table name. If you want to override this convention, you can do so by passing the desired table name as the second parameter or the table parameter.

Quick determines the foreign key of the relationship based on the entity name and key values. In this case, the User entity is assumed to have a userId foreign key and the Permission entity a permissionId foreign key. You can override this by passing a foreignKey in as the third argument and a relatedKey as the fourth argument:

Finally, if you are not joining on the primary keys of the current entity or the related entity, you can specify those keys using the last two parameters:

The inverse of belongsToMany is also belongsToMany. The foreignKey and relatedKey arguments are swapped on the inverse side of the relationship.

If you find yourself needing to interact with the pivot table (permissions_users) in the example above, you can create an intermediate entity, like UserPermission. You will still be able to access the end of the relationship chain using the hasManyThrough relationship type.

attach

Use the attach method to relate two belongsToMany entities together. attach can take a single id, a single entity, or an array of ids or entities (even mixed and matched) to associate.

detach

Use the detach method to remove an existing entity from a belongsToMany relationship. detatch can also take a single id, a single entity, or an array of ids or entities (even mixed and matched) to remove.

sync

Sometimes you just want the related entities to be a list you give it. For these situations, use the sync method.

Now, no matter what relationships existed before, this Post will only have three tags associated with it.

Relationship Setter

You can also influence the associated entities by calling "set" & relationshipName and passing in an entity or key value.

This code calls sync on the relationship. After executing this code, the post would be updated in the database to be associated with the tags passed in (4, 12, and 2). Any tags that were previously associated with this post would no longer be and only the tags passed in would be associated now.

Signature

Returns a BelongsToMany relationship between this entity and the entity defined by relationName.

Visualizer

Relationship Counts

One common type of subselect field is the count of related entites. For instance, you may want to load a Post or a list of Posts with the count of Comments on each Post. You can reuse your existing relationship definitions and add this count using the withCount method.

withCount

Adds a count of related entities as a subselect property. Relationships can be constrained at runtime by passing a struct where the key is the relationship name and the value is a function to constrain the query.

By default, you will access the returned count using the relationship name appended with Count, i.e. comments will be available under commentsCount.

You can alias the count attribute using the AS syntax as follows:

This is especially useful as you can dynamically constrain counts at runtime using the same struct syntax as eager loading with the with function.

Note that where possible it is cleaner and more readable to create a dedicated relationship instead of using dynamic constraints. In the above example, the Post entity could have pendingComments and approvedComments relationships. Dynamic constraints are more useful when applying user-provided data to the constraints like searching.

Name

Type

Required

Default

Description

relation

any

true

A single relation name or array of relation names to load counts.

var post = getInstance( "Post" )
	.withCount( "comments" )
	.findOrFail( 1 );
	
post.getCommentsCount();
var post = getInstance( "Post" )
	.withCount( "comments AS myCommentsCount" )
	.findOrFail( 1 );
	
post.getMyCommentsCount();
var post = getInstance( "Post" )
	.withCount( [
	    "comments AS allCommentsCount",
	    { "comments AS pendingCommentsCount": function( q ) {
	        q.where( "approved", 0 );
	    } },
	    { "comments AS approvedCommentsCount": function( q ) {
	        q.where( "approved", 1 );
	    } }
	] )
	.findOrFail( 1 );

post.getAllCommentsCount();	
post.getPendingCommentsCount();
post.getApprovedCommentsCount();

hasManyThrough

Usage

A hasManyThrough relationship is either a one-to-many or a many-to-many relationship. It is used when you want to access related entities through one or more intermediate entities. The most common example for this is through a pivot table. For instance, a User may have multiple Permissions via a UserPermission entity. This allows you to store additional data on the UserPermission entity, like a createdDate .

// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function permissions() {
        return hasManyThrough( [ "userPermissions", "permission" ] );
    }
    
    function userPermissions() {
        return hasMany( "UserPermission" );
    }

}
// Permission.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function users() {
       return hasManyThrough( [ "userPermissions", "user" ] );
    }
    
    function userPermissions() {
        return hasMany( "UserPermission" );
    }

}
// UserPermission.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function user() {
        return belongsTo( "User" );
    }

    function permission() {
        return belongsTo( "Permission" );
    }

}

The only value needed for hasManyThrough is an array of relationship function names to walk through to get to the related entity. The first relationship function name in the array must exist on the current entity. Each subsequent function name must exist on the related entity of the previous relationship result. For our previous example, userPermissions must be a relationship function on User. permission must then be a relationship function on the related entity resulting from calling User.userPermissions(). This returns a hasMany relationship where the related entity is UserPermission. So, UserPermission must have a permission relationship function. That is the end of the relationship function names array, so the related entity resulting from calling UserPermission.permission() is our final entity which is Permission.

hasManyThrough( [ "userPermissions", "permission" ] );

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| User           | userPermissions           | UserPermission |
+----------------+---------------------------+----------------+
| UserPermission | permission                | Permission     |
+----------------+---------------------------+----------------+

Let's take a look at another example. HasManyThrough relationships can go up and down the relationship chain. Here's an example of finding a User's teammates

The inverse of hasManyThrough is either a belongsToThrough or a hasManyThrough relationship.

// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function team() {
        return belongsTo( "Team" );
    }
    
    function teammates() {
        return hasManyThrough( [ "team", "members" ] );
    }

}
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function members() {
       return hasMany( "User" );
    }

}
hasManyThrough( [ "team", "members" ] );

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| User           | team                      | Team           |
+----------------+---------------------------+----------------+
| Team           | members                   | User           |
+----------------+---------------------------+----------------+

This approach can scale to as many related entities as you need. For instance, let's expand the previous example to include an Office that houses many Teams.

// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function team() {
        return belongsTo( "Team" );
    }
    
    function teammates() {
        return hasManyThrough( [ "team", "members" ] );
    }
    
    function officemates() {
        return hasManyThrough( [ "team", "office", "teams", "members" ] );
    }

}
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function members() {
       return hasMany( "User" );
    }
    
    function office() {
       return belongsTo( "Office" );
    }

}
// Office.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function teams() {
       return hasMany( "Team" );
    }

}
hasManyThrough( [ "team", "office", "teams", "members" ] );

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| User           | team                      | Team           |
+----------------+---------------------------+----------------+
| Team           | office                    | Office         |
+----------------+---------------------------+----------------+
| Office         | teams                     | Team           |
+----------------+---------------------------+----------------+
| Team           | members                   | User           |
+----------------+---------------------------+----------------+

This next example can get a little gnarly - you can include other hasManyThrough relationships in a hasManyThrough relationship function names array. You can rewrite the officemates relationship like so:

// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function team() {
        return belongsTo( "Team" );
    }
    
    function teammates() {
        return hasManyThrough( [ "team", "members" ] );
    }
    
    function officemates() {
        return hasManyThrough( [ "team", "office", "members" ] );
    }

}
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function members() {
        return hasMany( "User" );
    }
    
    function office() {
       return belongsTo( "Office" );
    }

}
// Office.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function teams() {
        return hasMany( "Team" );
    }
    
    function members() {
        return hasManyThrough( [ "teams", "members" ] );
    }

}
hasManyThrough( [ "team", "office", "members" ] );

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| User           | team                      | Team           |
+----------------+---------------------------+----------------+
| Team           | office                    | Office         |
+----------------+---------------------------+----------------+
| Office         | members                   | (below)        |
+----------------+---------------------------+----------------+---+
    | Office         | teams                     | Team           |
    +----------------+---------------------------+----------------+
    | Team           | members                   | User           |
    +----------------+---------------------------+----------------+

As you can see, this is a very powerful relationship type that can save you many unnecessary queries to get the data you need.

Signature

Name

Type

Required

Default

Description

relationships

array

true

An array of relationship function names. The relationships are resolved from left to right. Each relationship will be resolved from the previously resolved relationship, starting with the current entity.

relationMethodName

string

false

Current Method Name

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function permissions() {
       return belongsToMany( "Permission" );
    }

}
// Permission.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function users() {
       return belongsToMany( "User" );
    }

}
permissions_users
- permissionId
- userId
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function permissions() {
       return belongsToMany( "Permission", "user_permission_map" );
    }

}
return belongsToMany(
    "Permission",
    "user_permission_map",
    "FK_UserId",
    "FK_PermissionID"
);
return belongsToMany(
    "Permission",
    "user_permission_map",
    "FK_UserId",
    "FK_PermissionID",
    "user_id",
    "permission_id"
);
// Permission.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function user() {
        belongsToMany( "User", "user_permission_map", "FK_PermissionID", "FK_UserId" );
    }

}
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function userPermissions() {
        return hasMany( "UserPermission" );
    }

    function permissions() {
        return hasManyThrough( [ "UserPermissions", "Permission" ] );
    }

}
var post = getInstance( "Post" ).findOrFail( 1 );
var tag = getInstance( "Tag" ).create( { "name": "miscellaneous" });

// pass an id
post.tags().attach( tag.getId() );
// or pass an entity
post.tags().attach( tag );
var post = getInstance( "Post" ).findOrFail( 1 );
var tag = getInstance("Tag").firstWhere( "name", "miscellaneous" );

// pass an id
post.tags().detach( tag.getId() );
// or pass an entity
post.tags().detach( tag );
var post = getInstance( "Post" ).findOrFail( 1 );

post.tags().sync( [ 2, 3, 6 ] );
var someTag = getInstance( "Tag" ).findOrFail( 2 );
var post = getInstance( "Post" ).first();
post.setTags( [ 4, 12, someTag );

Name

Type

Required

Default

Description

relationName

string

true

The WireBox mapping for the related entity.

table

String

false

Table names in alphabetical order separated by an underscore.

The table name used as the pivot table for the relationship. A pivot table is a table that stores, at a minimum, the primary key values of each side of the relationship as foreign keys.

foreignPivotKey

String | [String]

false

keyNames()

The name of the column on the pivot table that holds the value of the parentKey of the parent entity.

relatedPivotKey

String | [String]

false

The name of the column on the pivot table that holds the value of the relatedKey of the ralated entity.

parentKey

String | [String]

false

The name of the column on the parent entity that is stored in the foreignPivotKey column on table.

relatedKey

String | [String]

false

The name of the column on the related entity that is stored in the relatedPivotKey column on table.

relationMethodName

String

false

The method name called on the entity to produce this relationship.

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

<b></b>

DO NOT PASS A VALUE HERE UNLESS YOU KNOW WHAT YOU ARE DOING.

belongsToThrough

Usage

A belongsToThrough relationship is the one side of a many-to-one relationship with an intermediate entity. It is used when you want to access a related, owning entity through one or more intermediate entities. For instance, a Post may belong to a Team via a User.

The only value needed for belongsToThrough is an array of relationship function names to walk through to get to the related entity. The first relationship function name in the array must exist on the current entity. Each subsequent function name must exist on the related entity of the previous relationship result. For our previous example, author must be a relationship function on Post. team must then be a relationship function on the related entity resulting from calling Post.author(). This returns a belongsTo relationship where the related entity is User. So, User must have a team relationship function. That is the end of the relationship function names array, so the related entity resulting from calling User.team() is our final entity which is Team.

This approach can scale to as many related entities as you need. For instance, let's expand the previous example to include an Office that houses many Teams.

withDefault

HasOneThrough relationships can be configured to return a default entity if no entity is found. This is done by calling withDefault on the relationship object.

Called this way will return a new unloaded entity with no data. You can also specify any default attributes data by passing in a struct of data to withDefault.

Signature

Eager Loading

The Problem

Let's imagine a scenario where you are displaying a list of posts. You fetch the posts:

And start looping through them:

When you visit the page, though, you notice it takes a while to load. You take a look at your SQL console and you've executed 26 queries for this one page! What?!?

Turns out that each time you loop through a post to display its author's username you are executing a SQL query to retreive that author. With 25 posts this becomes 25 SQL queries plus one initial query to get the posts. This is where the gets its name.

So what is the solution? Eager Loading.

Eager Loading means to load all the needed users for the posts in one query rather than separate queries and then stitch the relationships together. With Quick you can do this with one method call.

The Solution

with

You can eager load a relationship with the with method call.

with takes one parameter, the name of the relationship to load. Note that this is the name of the function, not the entity name. For example:

To eager load the User in the snippet above you would call pass author to the with method.

For this operation, only two queries will be executed:

Quick will then stitch these relationships together so when you call post.getAuthor() it will use the fetched relationship value instead of going to the database.

Nested Relationships

You can eager load nested relationships using dot notation. Each segment must be a valid relationship name.

You can eager load multiple relationships by passing an array of relation names to with or by calling with multiple times.

Constraining Eager Loaded Relationships

In most cases when you want to constrain an eager loaded relationship, the better approach is to create a new relationship.

You can eager load either option.

Occassionally that decision needs to be dynamic. For example, maybe you only want to eager load the posts created within a timeframe defined by a user. To do this, pass a struct instead of a string to the with function. The key should be the name of the relationship and the value should be a function. This function will accept the related entity as its only argument. Here is an example:

If you need to load nested relationships with constraints you can call with in your constraint callback to continue eager loading relationships.

load

Finally, you can postpone eager loading until needed by using the load method on QuickCollection. load has the same function signature as with. QuickCollection is the object returned for all Quick queries that return more than one record. Read more about it in .

// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function team() {
        return belongsToThrough( [ "author", "team" ] );
    }

    function author() {
        return belongsTo( "Post" );
    }

}
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function posts() {
        return hasMany( "Post" );
    }

    function team() {
        return belongsTo( "Team" );
    }

}
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function members() {
        return hasMany( "User" );
    }

}
belongsToThrough( [ "author", "team" ] );

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| Post           | author                    | User           |
+----------------+---------------------------+----------------+
| User           | team                      | Team           |
+----------------+---------------------------+----------------+
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function office() {
        return belongsToThrough( [ "author", "team", "office" ] );
    }

    function author() {
        return belongsTo( "Post" );
    }

}
// User.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function posts() {
        return hasMany( "Post" );
    }

    function team() {
        return belongsTo( "Team" );
    }

}
// Team.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function members() {
        return hasMany( "User" );
    }

    function office() {
        return belongsTo( "Office" );
    }

}
// Office.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function teams() {
        return hasMany( "Team" );
    }

}
belongsToThrough( [ "author", "team", "office" ] );

+----------------+---------------------------+----------------+
| Current Entity | Relationship Method Names | Related Entity |
+================+===========================+================+
| Post           | author                    | User           |
+----------------+---------------------------+----------------+
| User           | team                      | Team           |
+----------------+---------------------------+----------------+
| Team           | office                    | Office         |
+----------------+---------------------------+----------------+
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function team() {
        return belongsToThrough( [ "author", "team" ] ).withDefault();
    }

    function author() {
        return belongsTo( "Post" );
    }

}
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {

    function team() {
        return belongsToThrough( [ "author", "team" ] ).withDefault( {
            "name": "No Team"
        } );
    }

    function author() {
        return belongsTo( "Post" );
    }

}

Name

Type

Required

Default

Description

relationships

array

true

An array of relationship function names. The relationships are resolved from left to right. Each relationship will be resolved from the previously resolved relationship, starting with the current entity.

relationMethodName

string

false

Current Method Name

The method name called to retrieve this relationship. Uses a stack backtrace to determine by default.

prc.posts = getInstance( "Post" ).limit( 25 ).get():
<cfoutput>
    <h1>Posts</h1>
    <ul>
        <cfloop array="#prc.posts#" item="post">
            <li>#post.getTitle()# by #post.getAuthor().getUsername()#</li>
        </cfloop>
    </ul>
</cfoutput>
prc.posts = getInstance( "Post" )
    .with( "author" )
    .limit( 25 )
    .get();
// Post.cfc
component extends="quick.models.BaseEntity" {

    function author() {
        return belongsTo( "User" );
    }

}
getInstance( "Post" ).with( "author" ).get();
SELECT * FROM `posts` LIMIT 25

SELECT * FROM `users` WHERE `id` IN (1, 2, 3, 4, 5, 6, ...)
// User.cfc
component extends="quick.models.BaseEntity" {

    function country() {
        return belongsTo( "User" );
    }

}
getInstance( "Post" ).with( "author.country" );
getInstance( "Post" ).with( [ "author.country", "tags" ] );
// User.cfc
component {

    function posts() {
        return hasMany( "Post" );
    }

    function publishedPosts() {
        return hasMany( "Post" ).published(); // published is a query scope on Post
    }

}
getInstance( "User" ).with( "posts" ).get();
getInstance( "User" ).with( "publishedPosts" ).get();
getInstance( "User" ).with( { "posts" = function( query ) {

} } ).latest().get();
getInstance( "User" ).with( { "posts" = function( q1 ) {
    return q1
        .whereBetween( "published_date", rc.startDate, rc.endDate )
        .with( { "comments" = function( q2 ) {
            return q2.where( "body", "like", rc.search );
        } } );
} } ).latest().get();
N+1 problem
Collections

Querying Relationships

When querying an entity, you may want to restrict the query based on the existence or absence of a related entity. You can do that using the following four methods:

has

Name

Type

Required

Default

Description

relationshipName

String

true

The relationship to check. Can also be a dot-delimited list of nested relationships.

operator

String

false

An optional operator to constrain the check. See qb for a list of valid operators.

count

numeric

false

An optional count to constrain the check.

negate

boolean

false

false

If true, checks for the the absence of the relationship instead of its existence.

Checks for the existence of a relationship when executing the query.

By default, a has constraint will only return entities that have one or more of the related entity.

getInstance( "User" ).has( "posts" ).get();

An optional operator and count can be added to the call.

getInstance( "User" ).has( "posts", ">", 2 ).get();

Nested relationships can be checked by passing a dot-delimited string of relationships.

getInstance( "User" ).has( "posts.comments" ).get();

doesntHave

Name

Type

Required

Default

Description

relationshipName

String

true

The relationship to check. Can also be a dot-delimited list of nested relationships.

operator

String

false

An optional operator to constrain the check. See qb for a list of valid operators.

count

numeric

false

An optional count to constrain the check.

Checks for the absence of a relationship when executing the query.

By default, a doesntHave constraint will only return entities that have zero of the related entity.

getInstance( "User" ).doesntHave( "posts" ).get();

An optional operator and count can be added to the call.

getInstance( "User" ).doesntHave( "posts", "<=", 1 ).get();

Nested relationships can be checked by passing a dot-delimited string of relationships.

getInstance( "User" ).doesntHave( "posts.comments" ).get();

whereHas

Name

Type

Required

Default

Description

relationshipName

String

true

The relationship to check. Can also be a dot-delimited list of nested relationships.

closure

Function

true

A closure to constrain the relationship check.

operator

String

false

An optional operator to constrain the check. See qb for a list of valid operators.

count

numeric

false

An optional count to constrain the check.

negate

boolean

false

false

If true, checks for the the absence of the relationship instead of its existence.

When you need to have more control over the relationship constraint, you can use whereHas. This method operates similarly to has but also accepts a callback to configure the relationship constraint.

The whereHas callback is passed a builder instance configured according to the relationship. You may call any entity or query builder methods on it as usual.

getInstance( "User" )
    .whereHas( "posts", function( q ) {
	      q.where( "body", "like", "%different%" );
    } )
		.get();

When you specify a nested relationship, the builder instance is configured for the last relationship specified.

getInstance( "User" )
    .whereHas( "posts.comments", function( q ) {
	      q.where( "body", "like", "%great%" );
	  } )
	  .get();

An optional operator and count can be added to the call, as well.

getInstance( "User" )
    .whereHas( "posts.comments", function( q ) {
	      q.where( "body", "like", "%great%" );
	  }, ">", 2 )
	  .get();

whereDoesntHave

Name

Type

Required

Default

Description

relationshipName

String

true

The relationship to check. Can also be a dot-delimited list of nested relationships.

closure

Function

true

A closure to constrain the relationship check.

operator

String

false

An optional operator to constrain the check. See qb for a list of valid operators.

count

numeric

false

An optional count to constrain the check.

The whereDoesntHave callback is passed a builder instance configured according to the relationship. You may call any entity or query builder methods on it as usual.

getInstance( "User" )
    .whereDoesntHave( "posts", function( q ) {
	      q.where( "body", "like", "%different%" );
    } )
		.get();

When you specify a nested relationship, the builder instance is configured for the last relationship specified.

getInstance( "User" )
    .whereDoesntHave( "posts.comments", function( q ) {
	      q.where( "body", "like", "%great%" );
	  } )
	  .get();

An optional operator and count can be added to the call, as well.

getInstance( "User" )
    .whereDoesntHave( "posts.comments", function( q ) {
	      q.where( "body", "like", "%great%" );
	  }, ">", 2 )
	  .get();