Upgrade Guide

7.0.0

QuickBuilder.newQuery() now returns QuickBuilder instances

Calling newQuery on a QuickBuilder now returns a new QuickBuilder instance instead of a new QueryBuilder instance. If you needed a QueryBuilder instance, use the newQuickQB method instead.

6.0.0

Dropped support for ColdBox 5

It's very likely that Quick v6 will still work on ColdBox 5, but it is no longer an officially supported platform.

5.1.0

KeyType now accepts the builder as a second argument to `preInsert`

This is only a breaking change if you have built your own key types. This is being released as a bug fix since the ReturningKeyType has not worked correctly since 5.0.0

/**
 * Called to handle any tasks before inserting into the database.
 * Receives the entity as the only argument.
 *
 * @entity   The entity that is being inserted.
 * @builder  The builder that is doing the inserting.
 *
 * @return   void
 */
public void function preInsert( required any entity, required any builder );

5.0.0

Dropped support for Adobe ColdFusion 2016

Please upgrade to a supported engine.

`addSubselect` calls must be accessed on the QuickBuilder

This means for scopes, you must call addSubselect off of the passed in builder object.

function scopeAddAuthorName( qb ) {
    qb.addSubselect( "authorName", "author.name" );
}

`applyGlobalScopes` functions now receive a Builder instance that should be used to call scopes

Instead of this:

function applyGlobalScopes() {
    this.addVendorName();
    this.addLocationName();
}

the scopes should be called on the passed in Builder instance:

function applyGlobalScopes( qb ) {
    qb.addVendorName();
    qb.addLocationName();
}

4.0.0

Scopes, whereHas, and whereDoesntHave are now automatically grouped when using an OR combinator

This isn't a breaking change that will affect most people. In fact, it will most likely improve your code.

Previously, when using scopes, whereHas, or whereDoesntHave, you were fully responsible for the wrapping of your where statements. For example, the following query:

var users = getInstance( "User" )
    .active()
    .search( event.getValue( "q", "" ) )
    .get();

with the following scopes defined:

function scopeActive( qb ) {
    qb.where( "active", 1 );
}

function scopeSearch( qb, term ) {
    qb.where( "username", "like", term & "%" )
        .orWhere( "email", "like", term & "%" );
}

would generate the following SQL:

SELECT *
FROM "users"
WHERE "active" = ?
    AND "username" = ?
    OR "email" = ?

The problem with this statement is that the OR can short circuit the active check.

The fix is to wrap the LIKE statements in parenthesis. This is done in qb using a function callback to where. Adding this nesting inside our scope will fix this problem.

function scopeSearch( qb, term ) {
    qb.where( function( q ) {
        q.where( "username", "like", term & "%" )
            .orWhere( "email", "like", term & "%" );
    } );
}

But this was easy to miss this. This is because you are in a completely different file than the built query. It also breaks the mental model of a scope as an encapsulated piece of SQL code.

In Quick 4.0.0, scopes, whereHas, and whereDoesntHave will automatically group added where clauses when needed. That means our original example now produces the SQL we probably expected.

SELECT *
FROM "users"
WHERE "active" = ?
    AND (
        "username" = ?
        OR "email" = ?
    )

Grouping is not needed if there is no OR combinator. In these cases no grouping is added.

var users = getInstance( "User" )
    .active()
    .isAdmin()
    .get();
function scopeActive( qb ) {
    qb.where( "active", 1 );
}

function scopeIsAdmin( qb ) {
    qb.where( "admin", 1 )
        .whereNotNull( "hireDate" );
}
SELECT *
FROM "users"
WHERE "active" = ?
    AND "admin" = ?
    AND "hireDate IS NOT NULL

If you had already wrapped your expression in a group inside the scope, whereHas, or whereDoesntHave call, nothing changes. Your code works as before. The OR combinator check only works on the top most level of added where clauses. Additionally, if you do not add any where clauses inside your scope, nothing changes.

The breaking change part is if you were relying on these statements residing at the same level without grouping. If so, you will need to group your changes into a single scope where you control the grouping or do the querying at the builder level outside of scopes.

3.0.0

Adobe ColdFusion 11 and Lucee 4.5 are no longer supported

Please migrate to a supported engine.

Virtual Inheritance support removed

Virtual Inheritance allowed you to use a quick annotation on your entity instead of extending quick.models.BaseEntity. This was hardly used and didn't offer any benefit to extending using traditional inheritance. Additionally, removing the support allows us to clean up the code base by removing duplicate code paths.

If any of your entities are using the quick annotation, instead have them extends="quick.models.BaseEntity".

`accessors="true"` required for all entities

From the early days of Quick, developers have wanted to have accessors="true" on their entities. Because of this, Quick supported defining entities both with and without accessors. However, just as with virtual inheritance, it created two code paths that could hide bugs and make it hard to follow the code. In Quick 3.0.0, accessors="true" is required on all entities. If it is omitted, a helpful error message is thrown to remind you. This will help immensely in simplifying the code base. (In fact, just introducing this requirement helped find two bugs that were only present when using accessors.)

Ensure all entities have accessors="true" in their component metadata.

AssignedKey removed

Use NullKeyType instead.

Boolean casts updates to hook in to new Cast system

Previously, the only valid cast type was casts="boolean". In introducing the new Casts system, the boolean cast was refactored to use the same system. For this reason, any casts="boolean" needs to be changed to casts="BooleanCast@quick"

defaultGrammar updated to be the full WireBox mapping

In previous versions, the value passed to defaultGrammar was used to look up a mapping in the @qb namespace. This made it difficult to add or use grammars that weren't part of qb. (You could get around this be registering your custom grammar in the @qb namespace, but doing so seemed strange.)

To migrate this code, change your defaultGrammar to be the full WireBox mapping in your moduleSettings:

moduleSettings = {
    "quick": {
        "defaultGrammar": "SqlServerGrammar@qb"
    }
};

HasManyThrough relationship re-tooled to be based on relationships

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.

Here's how the old syntax would be used to define the relationship between Country and Post.

component extends="quick.models.BaseEntity" {

    function posts() {
        return hasManyThrough( "Post", "User", "country_id", "user_id" );
    }

}

This same relationship now needs to be defined in terms of other relationships, like so.

component extends="quick.models.BaseEntity" {

    function posts() {
        return hasManyThrough( [ "users", "posts" ] );
    }

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

}

This approach does require a relationship defined for each level, but it works up and down any number of relationships to get to your desired entity.

`associate` cannot be called on unloaded entities

To update the foreign key of a belongsTo relationship you use the associate method. In the past, it was possible to associate a new, unsaved child entity to its parent using this method.

var newChild = getInstance( "Child" );
newChild.fill( fields );
newChild.parent().associate( parent );
newChild.save();

In an attempt to provide more helpful error messages, this behavior is no longer possible. You can achieve the same effect in one of two ways.

The first is to manually assign the foreign keys:

var newChild = getInstance( "Child" );
newChild.fill( fields );
newChild.setParentKey( parent.getId() );
newChild.save();

While this works, it breaks the encapsulation provided by the relationship.

The second approach is to use the hasOne or hasMany side of the relationship to create the new child entity:

var newChild = getInstance( "Child" );
newChild.fill( fields );
var parent = getInstance( "Parent" ).findOrFail( id );
parent.children().save( newChild );

If you have all the data handy in a struct, you can use the create method for a more concise syntax.

var parent = getInstance( "Parent" ).findOrFail( id );
var newChild = parent.children().create( fields );

This is the recommended way of creating these components.

assignAttributesData argument renamed to `attributes`

This brings the API in line with the other methods referencing attributes.

Method changes due to compound key support

Compound key support required some method and parameter name changes. Although the list seems extensive, you will likely not need to change anything in your code unless you have extended built-in Quick components. (You will see many relationship parameter name changes. Note that the function you call to define a relationship is a function on the BaseEntity and has not changed its signature.)

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]

    • localKey : String -> localKeys : [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]

    • localKey : String -> localKeys : [String]

PolymorphicHasOneOrMany.cfc

  • init arguments have changed

    • id : String -> ids : [String]

    • localKey : String -> localKeys : [String]

2.0.0

Quick 2.0 brings with it a lot of changes to make things more flexible and more performant. This shouldn't take too long — maybe 2-5 minutes per entity.

Internal properties renamed

There were some common name clashes between internal Quick properties and custom attributes of your entities (the most common being fullName). All Quick internals have been obfuscated to avoid this situation. If you relied on these properties, please consult the following table below for the new property names.

If you are renaming your primary keys in your entities, you will have to change your key definition from variables.key = "user_id"; to variables._key = "user_id"; See Defining an Entity for details.

Additionally, some method names have also changed to avoid clashing with automatically generated getters and setters. Please consult the table below for method changes.

Lastly, the following properties and methods have been removed:

Key Types

Defining Key Types

Key Types are the way to define setting and retrieving a primary key in Quick. In Quick 1.0 these were injected in to the component. This made reusability hard for simple things like sequence names. In order to allow for more flexible key types, key types are no longer injected. Instead, they should be returned from a keyType method.

function keyType() {
    return variables._wirebox.getInstance( "Sequence@quick" )
        .setSequenceName( "seq_users" );
}

The keyType is lazily created and cached on the component, so this is both a more flexible approach as well as being more performant. If you are injecting custom key types in your entities you will need to move them to the method syntax.

Changed Key Types

A few key types have been renamed and will need to be updated in your codebase:

New Key Types

In additional to the changes to defining key types, there is a few new key types introduced in Quick 2.0.

ReturningKeyType

Used with grammars that return their primary key in the query response when inserting to the database. An example of this is NEWSEQUENTIALID in Microsoft SQL Server.

Scopes

The way arguments are passed to scopes have been updated to allow for default arguments. query is still the first argument. Other arguments will be passed in order after that. The args struct is no longer passed.

Relationships

The relationship methods are still named the same but some of the arguments have been changed to fix bugs and support better eager loading performance. Please check the relationship docs for more details.

Additionally, the alternative syntax for defining relationships on a relationships struct has been removed. It created an unnecessary code path that had it's own share of bugs. All relationships should be defined as methods on the entity.

Removing CFCollection

CFCollection was included in Quick 1.0 as both a way to lazily eager load a relationship and as a compatibility layer for older CF versions. The compatibility that CFCollection provides, however, comes with a performance cost. Additionally, the majority of users wanted to use plain arrays as the return format. For those reasons, arrays are now the default return format for collections. CFCollections can still be used by specifying a different return format in the module settings.

Converting to Null

Null is a tricky thing in CFML. The same goes for interacting with nulls in a database. By default, we will support the CFML convention of using an empty string to represent null. When interacting with the database empty strings will be converted to nulls. You can adjust this behavior on the property level with two new annotations:

  • convertToNull - Determines if the property will be automatically checked to convert to null at all. Defaults to true.

  • nullValue - This is the value that is equivalent to null for this property. Defaults to an empty string.

Returning Null instead of Unloaded Entities

In an effort to avoid dealing with CFML's version of null, Quick originally returned unloaded entities. You could check if an entity was loaded using the isLoaded method. This doesn't make as much sense as null however and even made it more difficult to interact with other libraries. Now Quick will return null when it encounters an empty query result either from a retrieval or from a belongsTo or hasOne relationship. Any instances that you were checking isLoaded should be updated. isLoaded will continue to exist for when you are creating a new entity not from the database.

AutoDiscover Grammar

The default grammar for Quick is now AutoDiscover. This provides a better first run experience. The grammar can still be set in the moduleSettings.

BaseService

As a new way to interact with Quick, you can use Quick Services to interact with your entities in a service-oriented fashion. These are equivalent to VirtualEntityServices in cborm.

The easiest way to use a Quick Service is to use the quickService: injection dsl.

component {
    property name="userService" inject="quickService:User";
}

All methods available on the Quick entity are available on the service.

Eager Loading

Eager loading is now supported for nested relationships using a dot-separated syntax. Additionally, constraints can be added to an eager loaded relationship. See the docs on eager loading for more information.

Column Aliases in Queries

Column aliases can now be used in queries. They will be transformed to columns before executing the query.

Quick entities in Setters

If you pass a Quick entity to a setter method the entity's keyValue value will be passed.

Update and Insert Guards

Columns can be prevented from being inserted or updated using property attributes — insert="false" and update="false".

cbvalidation removed as a default dependency

Quick no longer automatically validates entities before saving them. Having cbvalidation baked in made it hard to extend it. If desired, validation can be added back in using Quick's lifecycle hooks.

instanceReady Lifecycle Method

Quick now announces an instanceReady event after the entity has gone through dependency injected and had its metadata inspected. This can be used to hook in other libraries, like cbvalidation and mementifier.

Automatic Attribute Casting

You can automatically cast a property to a boolean value while retrieving it from the database and back to a bit value when serializing to the database by setting casts="boolean" on the property.