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
andisNot
to compare entities - Allow hydrating entities from serialized data.
- Allow returning default entities for null relations. (
withDefault
) exists
andexistsOrFail
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 ofkeys
as the second argumentgetQualifiedLocalKey : String
->getQualifiedLocalKeys : [String]
getExistenceCompareKey : String
->getExistenceCompareKeys : [String]
BelongsTo.cfc
init
arguments have changedforeignKey : String
->foreignKeys : [String]
ownerKey : String
->ownerKeys : [String]
getQualifiedLocalKey : String
->getQualifiedLocalKeys : [String]
getExistenceCompareKey : String
->getExistenceCompareKeys : [String]
BelongsToMany.cfc
init
arguments have changedforeignPivotKey : 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 changedforeignKey : String
->foreignKeys : [String]
localKey : String
->localKeys : [String]
getParentKey : any
->getParentKeys : [any]
getQualifiedLocalKey : String
->getQualifiedLocalKeys : [String]
getQualifiedForeignKeyName : String
->getQualifiedForeignKeyNames : [String]
PolymorphicBelongsTo.cfc
init
arguments have changedforeignKey : String
->foreignKeys : [String]
ownerKey : String
->ownerKeys : [String]
PolymorphicHasOneOrMany.cfc
init
arguments have changedid : String
->ids : [String]
localKey : String
->localKeys : [String]
Add Your Comment