It's finally here, the first alpha of Quick 3.0.0. This release is jam packed with features and improvements to make your development take off. You can install it via CommandBox and ForgeBox with box install quick@3.0.0-alpha.1

The alpha is geared toward existing Quick users. The docs are underway and a beta release will accompany the completion of the documentation for 3.0.0. For now, this blog post, the git history, and the source code will be your documentation. Great care has been taken to update all of the docblocks to be accurate and informative. This should help you in your testing.

Breaking Changes

This is a breaking release of Quick, though less has changed than you might be worried about. Here are the following breaking changes:

  • Adobe ColdFusion 11 and Lucee 4.5 are no longer supported platforms
  • Quick no longer appends @qb to the end of grammars defined in settings. This is in line with a change in qb 7.0.0.
  • HasManyThrough has been retooled. It no longer accepts any entities or columns. Rather, it accepts an array of relationships to walk "through" to end up at the desired entity. More details can be found later in this blog post.
  • Many method signatures have changed as a result of support composite primary and foreign keys. While the list is extensive, most of you will not need to make any changes to your code unless you have extended the changed components. The full list is available at the end of this blog post.

What's New?

While this is not an exhaustive list, here are the headline features of Quick 3.0.0

Powerful Through Relationships

Quick has already enjoyed the hasManyThrough relationship - a way to get an array of entities related to an entity through another table. For instance, you could get all of the Blog Posts for a Country through a Country's Users. In Quick 3.0.0, HasManyThrough is joined by HasOneThrough and BelongsToThrough to complete the family. Additionally, instead of jumping through one entity or table, these relationships can now traverse any number of intermediate relationships to reach the intended entity. You define this relationship by passing an array of intermediate relationships. The relationship above would be written like so:

// Country.cfc
function posts() {
    return hasManyThrough( [ "users", "posts" ] );
}

It is important that Country has a relationship method called users and that User (or the entity returned by calling users() on Country) has a relationship called posts (and so forth).

This can be used for as many levels as you need:

// Country.cfc
function comments() {
    return hasManyThrough( [ "users", "posts", "comments" ] );
}

It can be used going up and down relationships as well:

// Team.cfc
component extends="quick.models.BaseEntity" {
    function members() {
        return hasMany( "User" );
    }
}

// User.cfc
component extends="quick.models.BaseEntity" {
    function team() {
        return belongsTo( "Team" );
    }

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

This has the potential to clean up or even eliminate many additional queries you are currently running.

Subselects

Quick 2.0.0 added the ability to add a subselect to a query to avoid having to run additional queries and load additional entities for one piece of data. This also enabled the ability to have relationships built on subselects. In Quick 3.0.0, defining subselects is even simpler. The old methods still work, but a simple way to define a subselect is to utilize existing relationships. For example:

// User.cfc
component extends="quick.models.BaseEntity" {
    function posts() {
        return hasMany( "Post" ).orderByDesc( "publishedDate" );
    }
}

// handlers/Users.cfc
function index( event, rc, prc ) {
    prc.users = getInstance( "User" )
        .addSubselect( "latestPostId", "posts.id" ) )
        .get();
}

The relationship shortcut walks through a dot-delimited string using the last segment as the attribute to select. This can accept any number of nested relationships as well.

// Country.cfc
component extends="quick.models.BaseEntity" {
    property name="name";
}

// User.cfc
component extends="quick.models.BaseEntity" {
    function country() {
        return belongsTo( "Country" );
    }
}

// Post.cfc
component extends="quick.models.BaseEntity" {
    function author() {
        return belongsTo( "User" );
    }
}

// handlers/Posts.cfc
function show( event, rc, prc ) {
    prc.post = getInstance( "Post" )
        .addSubselect( "countryName", "author.country.name" )
        .findOrFail( rc.id );
}

As shown in the methods above, this approach makes it more ergonomic to add subselects directly to your queries. You may still define them inside scopes as was previously supported, though we recommend prefixing those scopes with add instead of with to avoid confusing subselects with eager loading.

Ordering by Relationships

In the same vein as adding subselects using existing relationships, you can now order by a relationship column. This is done using the same dot-delimited string.

// handlers/Posts.cfc
function index( event, rc, prc ) {
    prc.posts = getInstance( "Post" )
        .has( "author" )
        .orderBy( "author.firstName" )
        .orderBy( "post_pk" )
        .get();
}

This string can include nested relationships and can be ordered either ascending or descending.

// handlers/Posts.cfc
function index( event, rc, prc ) {
    prc.posts = getInstance( "Post" )
        .orderByDesc( "author.country.name" )
        .get();
}

Querying Relationships

Last feature to introduce that uses the now-familiar dot-delimited relationship syntax: querying relationships. This comes in two flavors.

First, you can check for the existence or absence of related entities. This is done using the has or doesntHave methods.

// handlers/Posts.cfc
function index( event, rc, prc ) {
    // only retrieve users that have at least one post
    prc.usersWithPosts = getInstance( "User" )
        .has( "posts" )
        .get();

    prc.usersWithoutPosts = getInstance( "User" )
        .doesntHave( "posts" )
        .get();
}

These methods can take an optional constraint clause if you need to check for the number of related entities.

// handlers/Posts.cfc
function index( event, rc, prc ) {
    // only retrieve users that have two or more posts
    prc.usersWithMultiplePosts = getInstance( "User" )
        .has( "posts", ">=", 2 )
        .get();

    // only retrieve users that have less than two posts
    prc.usersWithoutMultiplePosts = getInstance( "User" )
        .doesntHave( "posts", ">=", 2 )
        .get();
}

If you need more detailed constraints on the relationship, you can use the whereHas or whereDoesntHave methods. These methods take a dot-delimited relationship string and a callback function to further constrain the query.

// handlers/Posts.cfc
function index( event, rc, prc ) {
    // Only retrieve users that have at least one post, that have at least one comment, and that match the search term.
    // Users without posts will not be returned.
    param rc.search = "";
    prc.users = getInstance( "User" )
        .whereHas( "posts.comments", function( q ) {
            q.where( "body", "like", "%#rc.search#%" );
        } )
        .get();
}

With these new query methods, you will have no problem drilling down to just the entities you need.

Pagination

Quick now uses the paginate method and pagination helpers defined in qb 7.0.0. Pagination is usually the first way to handle performance issues with large queries, especially since your users don't really need 10,000 records at once. Read more about this on qb's documentation.

Mementifier

Quick 3.0.0 now bundles Mementifier for its memento transformations. If you do not define a this.memento struct, Quick will generate one for you that includes all of the attributes by default. If you do define a this.memento struct, Quick will merge it into the defaults (with your custom struct overwriting, of course). For more information on what you can do with this, check out the Mementifier docs.

Additionally, Quick 3.0.0 now includes an asMemento method. You call this method with any arguments you would pass to getMemento. Call this method before executing your query (get, find, paginate, etc.) and when you do execute your query, your entities will automatically be transformed into mementos using any arguments you passed to asMemento. This works for all the query execution methods, whether it returns a single entity or an array of entities. This is great for API work, specifically for index and show actions.

// handlers/Users.cfc
function index( event, rc, prc ) {
    return getInstance( "User" )
        .asMemento(
            includes = rc.includes,
            excludes = rc.excludes
        )
        .get();
}

function show( event, rc, prc ) {
    return getInstance( "User" )
        .asMemento()
        .findOrFail( rc.id );
}

Composite Keys

Quick 3.0.0 introduced the ability to use composite keys as the primary key for an entity or the foreign key for a relationship. Nothing has changed in using single keys and this is still the norm. You can, however, now pass an array of attributes. This will be used as the key for entity.

function registration() {
    return belongsTo(
        "Registration",
        [ "seasonId", "childId" ], // foreign keys in this entity
        [ "seasonId", "childId" ] // local keys in the related entity
    );
}

Custom Casts

Occasionally you want to deal with attributes in your entity as different types than are stored in the database. A simple example of this is storing booleans as 0's and 1's in the database and using real booleans in your entity. Another example is interacting with JSON data. You may store this as a string in your database (for databases that don't support JSON column types), but want an array or struct of data in your entity. The way to accomplish this are custom casts.

casts is an attribute you can specify on a property alongside name, column, and others. The value is a WireBox mapping to a component that implements the CastsAttribute interface. (The implements keyword is optional.) The Caster component is responsible for translating a value from the database to the specific cast type and back.

Here is the BooleanCast@quick:

// BooleanCast.cfc
component implements="CastsAttribute" {

	public any function get(
		required any entity,
		required string key,
		any value
	) {
		return isNull( arguments.value ) ? false : booleanFormat( arguments.value );
	}

    public any function set(
		required any entity,
		required string key,
		any value
	) {
		return arguments.value ? 1 : 0;
	}

}

Casters can also reference columns that are not themselves persisted to the database but are composed of one or more columns that are. An example here is an Address value object made up of multiple fields in the database. Consider the following:

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

    property name="address" casts="AddressCast" persistent="false";
    property name="streetOne";
    property name="streetTwo";
    property name="city";
    property name="state";
    property name="zip";

}

// AddressCast
component implements="quick.models.Casts.CastsAttribute" {

    property name="wirebox" inject="wirebox";

    public any function get(
        required any entity,
        required string key,
        any value
    ) {
        return wirebox.getInstance( dsl = "Address" )
            .setStreetOne( entity.retrieveAttribute( "streetOne" ) )
            .setStreetTwo( entity.retrieveAttribute( "streetTwo" ) )
            .setCity( entity.retrieveAttribute( "city" ) )
            .setState( entity.retrieveAttribute( "state" ) )
            .setZip( entity.retrieveAttribute( "zip" ) );
    }

    public any function set(
        required any entity,
        required string key,
        any value
    ) {
        return {
            "streetOne": arguments.value.getStreetOne(),
            "streetTwo": arguments.value.getStreetTwo(),
            "city": arguments.value.getCity(),
            "state": arguments.value.getState(),
            "zip": arguments.value.getZip()
        };
    }

}

Casts can improve the readability and understanding of your code while maintaining a clean and consistent database structure.

Many other improvements and fixes.

There are so many more new features and fixes! Too many to list here in this introductory blog post. Here's a small list of some other features not highlighted above:

  • Add testing of coldbox@be to CI
  • Introducing QuickBuilder as a super type of QueryBuilder
  • Add an optional id check to exists and existsOrFail
  • Allow custom error messages for *orFail methods
  • Ensure loadRelationship doesn't reload existing relationships
  • Add multiple retrieve or new/create methods (firstOrNew, firstOrCreate, etc.)
  • Add is and isNot to compare entities
  • Allow hydrating entities from serialized data.
  • Allow returning default entities for null relations. (withDefault)
  • exists and existsOrFail checks
  • Apply sql types for columns to wheres
  • Add a better error message if onMissingMethod fails
  • Only retrieve columns for defined attributes
  • Cache entity metadata in CacheBox

And that's not even the full list! As you can see, Quick 3.0.0 is truly a major release.

I hope this gets you excited to upgrade or try Quick for the first time. If you are brave enough to try the alpha, first off - thank you. You are helping make this upgrade an easier and simpler experience for all the rest of us. Second, please reach out, either on Slack or on the Quick repo itself.

Thanks, and see you at Into the Box in May!

Changed method signatures as a result of adding composite key support

As a reminder, this list is exhaustive, but will likely not require many if any changes on your part. Relationships, for instance, are called through helper functions on the BaseEntity.cfc and have not been changed. Review your code base for usage of these methods and adjust accordingly.

BaseEntity.cfc:

  • retrieveQualifiedKeyName : String -> retrieveQualifiedKeyNames : [String]
  • keyName : String -> keyNames : [String]
  • keyColumn : String -> keyColumns : [String]
  • keyValue : String -> keyValues : [String]

AutoIncrementingKeyType.cfc

  • This cannot be used with composite primary keys

BaseRelationship.cfc

  • getKeys now takes an array of keys as the second argument
  • getQualifiedLocalKey : String -> getQualifiedLocalKeys : [String]
  • getExistenceCompareKey : String -> getExistenceCompareKeys : [String]

BelongsTo.cfc

  • init arguments have changed
    • foreignKey : String -> foreignKeys : [String]
    • ownerKey : String -> ownerKeys : [String]
  • getQualifiedLocalKey : String -> getQualifiedLocalKeys : [String]
  • getExistenceCompareKey : String -> getExistenceCompareKeys : [String]

BelongsToMany.cfc

  • init arguments have changed
    • foreignPivotKey : String -> foreignPivotKeys : [String]
    • relatedPivotKey : String -> relatedPivotKeys : [String]
    • parentKey : String -> parentKeys : [String]
    • relatedKey : String -> relatedKeys : [String]
  • getQualifiedRelatedPivotKeyName : String -> getQualifiedRelatedPivotKeyNames : [String]
  • getQualifiedForeignPivotKeyName : String -> getQualifiedForeignPivotKeyNames : [String]
  • getQualifiedForeignKeyName : String -> getQualifiedForeignKeyNames : [String]`

HasManyThrough.cfc

  • This component now extends quick.models.Relationships.HasOneOrManyThrough
  • init arguments are now as follows:
    • related: The related entity instance.
    • relationName: The WireBox mapping for the related entity.
    • relationMethodName: The method name called to retrieve this relationship.
    • parent: The parent entity instance for the relationship.
    • relationships: An array of relationships between the parent entity and the related entity.
    • relationshipsMap: A dictionary of relationship name to relationship component.
  • The following methods no longer exist:
    • getQualifiedFarKeyName
    • getQualifiedForeignKeyName
    • getQualifiedFirstKeyName
    • getQualifiedParentKeyName

HasOneOrMany.cfc

  • init arguments have changed
    • foreignKey : String -> foreignKeys : [String]
    • localKey : String -> localKeys : [String]
  • getParentKey : any -> getParentKeys : [any]
  • getQualifiedLocalKey : String -> getQualifiedLocalKeys : [String]
  • getQualifiedForeignKeyName : String -> getQualifiedForeignKeyNames : [String]

PolymorphicBelongsTo.cfc

  • init arguments have changed
    • foreignKey : String -> foreignKeys : [String]
    • ownerKey : String -> ownerKeys : [String]

PolymorphicHasOneOrMany.cfc

  • init arguments have changed
    • id : String -> ids : [String]
    • localKey : String -> localKeys : [String]