Creating New Entities
save
New Quick entities can be created and persisted to the database by creating a new entity instance, setting the attributes on the entity, and then calling the 'save' method.
var user = getInstance( "User" );
user.setUsername( "JaneDoe" );
user.setEmail( "jane@example.com" );
user.setPassword( "mypass1234" );
user.save();
When we call 'save`, the record is persisted from the database and the primary key is set to the auto-generated value (if any).
We can shortcut the setters above using a 'fill' method.
fill
Finds the first matching record or creates a new entity.
Sets attributes data from a struct of key / value pairs. This method does the following, in order:
- Guard against read-only attributes.
- Attempt to call a relationship setter.
- Calls custom attribute setters for attributes that exist.
- Throws an error if an attribute does not exist (if 'ignoreNonExistentAttributes' is 'false' which is the default).
var user = getInstance( "User" );
user.fill( {
"username": "JaneDoe",
"email": "jane@example.com",
"password": "mypass1234"
} );
user.save();
populate
Populate is simply an alias for 'fill`. Use whichever one suits you best.
create
Creates a new entity with the given attributes and then saves the entity.
var user = getInstance( "User" ).create( {
"username": "JaneDoe",
"email": "jane@example.com",
"password": "mypass1234"
} );
There is no need to call 'save' when using the 'create' method.
firstOrNew
Finds the first matching record or returns an unloaded new entity.
var user = getInstance( "User" )
.firstOrNew( { "username": rc.username } );
firstOrCreate
Finds the first matching record or creates a new entity.
var user = getInstance( "User" )
.firstOrCreate( { "username": rc.username } );
findOrNew
Returns the entity with the id value as the primary key. If no record is found, it returns a new unloaded entity.
var user = getInstance( "User" ).findOrNew(
9999,
{
"firstName" : "doesnt",
"lastName" : "exist"
}
);
findOrCreate
Returns the entity with the id value as the primary key. If no record is found, it returns a newly created entity.
var user = getInstance( "User" ).findOrCreate(
9999,
{
"username" : "doesntexist",
"firstName" : "doesnt",
"lastName" : "exist",
"password" : "secret"
}
);
updateOrCreate
Updates an existing record or creates a new record with the given attributes.
var user = getInstance( "User" ).updateOrCreate( {
"username": "newuser"
} );
Hydration Methods
Hydration is a term to describe filling an entity with a struct of data and then marking it as loaded, without doing any database queries. For example, this might be useful when hydrating a user from session data instead of doing a query every request.
hydrate
Hyrdates an entity from a struct of data. Hydrating an entity fills the entity and then marks it as loaded.
If the entity's keys are not included in the struct of data, a 'MissingHydrationKey' is thrown.
var user = getInstance( "User" ).hydrate( {
"id": 4,
"username": "JaneDoe",
"email": "jane@example.com",
"password": "mypass1234"
} );
user.isLoaded(); // true
hydrateAll
Hydrates a new collection of entities from an array of structs.
var users = getInstance( "User" ).hydrateAll( [
{
"id": 3,
"username": "JohnDoe",
"email": "john@example.com",
"password": "mypass4321"
},
{
"id": 4,
"username": "JaneDoe",
"email": "jane@example.com",
"password": "mypass1234"
}
] );
Deleting Entities
delete
You can delete an entity by calling the 'delete' method on it.
var user = getInstance( "User" ).find( 1 );
user.delete();
The entity will still exist in any variables you have stored it in, even though it has been deleted from the database.
deleteAll
Just like 'updateAll`, you can delete many records from the database by specifying a query with constraints and then calling the 'deleteAll' method.
Deletes matching entities according to the configured query.
getInstance( "User" )
.whereActive( false )
.deleteAll();
Additionally, you can pass in an array of ids to 'deleteAll' to delete only those ids. Note that any previously configured constraints will still apply.
getInstance( "User" ).deleteAll( [ 4, 10, 22 ] );
Updating Existing Entities
save
Updates are handled identically to inserts when using the 'save' method. The only difference is that instead of starting with a new entity, we start with an existing entity.
var user = getInstance( "User" ).find( 1 );
user.setPassword( "newpassword" );
user.save();
update
You can update multiple fields at once using the 'update' method. This is similar to the 'create' method for creating new entities.
var user = getInstance( "User" ).find( 1 );
user.update( {
email = "janedoe2@example.com",
password = "newpassword"
} );
There is no need to call 'save' when using the 'update' method.
By default, if you have a key in the struct that doesn't match a property in the entity the 'update' method will fail. If you add the optional argument 'ignoreNonExistentAttributes' set to 'true`, those missing keys are ignored. Now you can pass the 'rc' scope from your submitted form directly into the 'update' method and not worry about any other keys in the 'rc' like 'event' that would cause the method to fail.
var user = getInstance( "User" ).find( 1 );
user.update( rc, true );
updateOrCreate
Updates an existing record or creates a new record with the given attributes.
var user = getInstance( "User" ).updateOrCreate( {
"username": "newuser"
} );
updateAll
Updates matching entities with the given attributes according to the configured query. This is analagous to qb's update method.
getInstance( "User" )
.where( "lastLoggedIn", ">", dateAdd( "m", 3, now() ) )
.updateAll( {
"active" = 0
} );
fresh
Retrieves a new entity from the database with the same key value as the current entity. Useful for seeing any changes made to the record in the database. This function executes a query.
var user = getInstance( "User" ).findOrFail( rc.userID );
var sameUser = user.fresh();
refresh
Refreshes the attributes data for the entity with data from the database. This differs from 'fresh' in that it operates on the current entity instead of returning a new one. This function executes a query.
var user = getInstance( "User" ).findOrFail( rc.userID );
user.refresh(); // user now has updated data from the database
Working with Entities
isLoaded
Checks if the entity was loaded from the database.
A loaded entity has a tie to the database. It has either been loaded from the database or saved to the database. An unloaded entity is one created in code but not saved to the database yet.
var user = getInstance( "User" );
user.isLoaded(); // false
user.save();
user.isLoaded(); // true
Introduction
The cookbook is a place to show examples of using Quick in practice. Each cookbook entry will include a short description of the problem to solve or use case and one or more code snippets showing the code.
The cookbook is not meant to teach you the basics of Quick or show you the method signatures. That is the purpose of the Guide and API Docs, respectively. The cookbook is meant to show you advanced and specific use cases.
Additionally, this is a great place to contribute to Quick! If you have solved a particular use case, send in a pull request and add it to the cookbook! We only ask that you take the time to simplify your example as much as you can. This usually means removing any custom naming convention for your attributes, tables, and entities.
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.
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.
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
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
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
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
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();
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
.
var post = getInstance( "Post" )
.withCount( "comments" )
.findOrFail( 1 );
post.getCommentsCount();
You can alias the count attribute using the 'AS' syntax as follows:
var post = getInstance( "Post" )
.withCount( "comments AS myCommentsCount" )
.findOrFail( 1 );
post.getMyCommentsCount();
This is especially useful as you can dynamically constrain counts at runtime using the same struct syntax as eager loading with the 'with' function.
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();
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.
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 );
Collections
Collections are what are returned when calling 'get' or 'all' on an entity. By default, it returns an array. Every entity can override its 'newCollection' method and return a custom collection. This method accepts an array of entities and should return your custom collection.
QuickCollection' is a custom collection included in Quick as an extra component. It is a specialized version of ['CFCollection'](https://www.forgebox.io/view/cfcollection). It smooths over the various CFML engines to provide an extendible, reliable array wrapper with functional programming methods. You may be familiar with methods like 'map' \('ArrayMap'\), 'filter' \('ArrayFilter'\), or 'reduce' \('ArrayReduce'\). These methods work in every CFML engine with 'CFCollection
.
To use collections you need to install 'cfcollection' and configure it as your as your 'newCollection`.
Here's how you would configure an entity to return a 'QuickCollection`.
component name="User" {
function newCollection( array entities = [] ) {
return variables._wirebox.getInstance(
name = "quick.extras.QuickCollection",
initArguments = {
"collection" = arguments.entities
}
);
}
}
Collections are more powerful than plain arrays. There are many methods that can make your work easier. For instance, let's say you needed to group each active user by the first letter of their username in a list.
var users = getInstance("User").all();
users
.filter(function(user) {
return user.getActive();
})
.pluck("username")
.groupBy(function(username) {
return left(username, 1);
});
So powerful! We think you'll love it.
load
Additionally, 'QuickCollection' includes a 'load' method. 'load' lets you eager load a relationship after executing the initial query.
var posts = getInstance("Post").all();
if (someCondition) {
posts.load("user");
}
This is the same as if you had initially executed:
getInstance("Post")
.with("user")
.all();
$renderData
`QuickCollection' includes a '$renderData' method that lets you return a 'QuickCollection' directly from your handler and translates the results and the entities within to a serialized version. Check out more about it in the Serialization chapter.
Query Scopes and Subselects
What are Scopes?
Query scopes are a way to encapsulate query constraints in your entities while giving them readable names .
A Practical Example
For instance, let's say that you need to write a report for subscribers to your site. Maybe you track subscribers in a 'users' table with a boolean flag in a 'subscribed' column. Additionally, you want to see the oldest subscribers first. You keep track of when a user subscribed in a 'subscribedDate' column. Your query might look as follows:
var subscribedUsers = getInstance( "User" )
.where( "subscribed", true )
.orderBy( "subscribedDate" )
.get();
Now nothing is wrong with this query. It retrieves the data correctly and you continue on with your day.
Later, you need to retrieve a list of subscribed users for a different part of the site. So, you write a query like this:
var subscribedUsers = getInstance( "User" )
.where( "subscribed", true )
.get();
We've duplicated the logic for how to retrieve active users now. If the database representation changed, we'd have to change it in multiple places. For instance, what if instead of keeping track of a boolean flag in the database, we just checked that the 'subscribedDate' column wasn't null?
var subscribedUsers = getInstance( "User" )
.whereNotNull( "subscribedDate" )
.get();
Now we see the problem. Let's look at the solution.
The key here is that we are trying to retrieve subscribed users. Let's add a scope to our 'User' entity for 'subscribed`:
component extends="quick.models.BaseEntity" accessors="true" {
function scopeSubscribed( query ) {
return query.where( "subscribed", true );
}
}
Now, we can use this scope in our query:
var subscribedUsers = getInstance( "User" )
.subscribed()
.get();
We can use this on our first example as well, for our report.
var subscribedUsers = getInstance( "User" )
.subscribed()
.orderBy( "subscribedDate" )
.get();
We've successfully encapsulated our concept of a subscribed user!
We can add as many scopes as we'd like. Let's add one for 'longestSubscribers`.
component extends="quick.models.BaseEntity" accessors="true" {
function scopeLongestSubscribers( query ) {
return query.orderBy( "subscribedDate" );
}
function scopeSubscribed( query ) {
return query.where( "subscribed", true );
}
}
Now our query is as follows:
var subscribedUsers = getInstance( "User" )
.subscribed()
.longestSubscribers()
.get();
Best of all, we can reuse those scopes anywhere we see fit without duplicating logic.
Usage
All query scopes are methods on an entity that begin with the 'scope' keyword. You call these functions without the 'scope' keyword (as shown above).
Each scope is passed the 'query`, a reference to the current 'QuickBuilder' instance, as the first argument. Any other arguments passed to the scope will be passed in order after that.
component extends="quick.models.BaseEntity" accessors="true" {
function scopeOfType( query, type ) {
return query.where( "type", type );
}
}
var subscribedUsers = getInstance( "User" )
.ofType( "admin" )
.get();
Scopes that Return Values
All of the examples so far either returned the 'QuickBuilder' object or nothing. Doing so lets you continue to chain methods on your Quick entity. If you instead return a value, Quick will pass on that value to your code. This lets you use scopes as shortcut methods that work on a query.
For example, maybe you have a domain method to reset passwords for a group of users, and you want the count of users updated returned.
component extends="quick.models.BaseEntity" accessors="true" {
property name="id";
property name="username";
property name="password";
property name="type";
function scopeOfType( query, type = "limited" ) {
return query.where( "type", type );
}
function scopeResetPasswords( query ) {
return query.updateAll( { "password" = "" } ).result.recordcount;
}
}
getInstance( "User" ).ofType( "admin" ).resetPasswords(); // 1
Global Scopes
Occasionally, you want to apply a scope to each retrieval of an entity. An example of this is an Admin entity which is just a User entity with a type of admin. Global Scopes can be registered in the 'applyGlobalScopes' method on an entity. Inside this entity you can call any number of scopes:
component extends="User" table="users" accessors="true" {
function applyGlobalScopes() {
this.ofType( "admin" );
}
}
These scopes will be applied to the query without needing to call the scope again.
var admins = getInstance( "Admin" ).all();
// SELECT * FROM users WHERE type = 'admin'
If you have a global scope applied to an entity that you need to temporarily disable, you can disable them individually using the 'withoutGlobalScope' method:
var admins = getInstance( "Admin" ).withoutGlobalScope( [ "ofType" ] ).all();
// SELECT * FROM users
Subselects
Subselects are a useful way to grab data from related tables without having to execute the full relationship. Sometimes you just want a small piece of information like the 'lastLoginDate' of a user, not the entire 'Login' relationship. Subselects are perfect for this use case. You can even use subselects to provide the correct key for dynamic subselect relationships. We'll show how both work here.
Quick handles subselect properties (or computed or formula properties) through query scopes. This allows you to dynamically include a subselect. If you would like to always include a subselect, add it to your entity's list of global scopes.
Here's an example of grabbing the 'lastLoginDate' for a User:
component extends="quick.models.BaseEntity" accessors="true" {
/* properties */
function logins() {
return hasMany( "Login" ).latest();
}
function scopeAddLastLoginDate( query ) {
addSubselect( "lastLoginDate", newEntity( "Login" )
.select( "timestamp" )
.whereColumn( "users.id", "user_id" )
);
}
}
We'd add this subselect by calling our scope:
var user = getInstance( "User" ).addLastLoginDate().first();
user.getLastLoginDate(); // {ts 2019-05-02 08:24:51}
We can even constrain our 'User' entity based on the value of the subselect, so long as we've called the scope adding the subselect first (or made it a global scope).
var user = getInstance( "User" )
.addLastLoginDate()
.where( "lastLoginDate", ">", "2019-05-10" )
.all();
Or add a new scope to 'User' based on the subselect:
function scopeLoggedInAfter( query, required date afterDate ) {
return query.where( "lastLoginDate", ">", afterDate );
}
In this example, we are using the 'addSubselect' helper method. Here is that function signature:
You might be wondering why not use the 'logins' relationship? Or even 'logins().latest().limit( 1 ).get()`? Because that executes a second query. Using a subselect we get all the information we need in one query, no matter how many entities we are pulling back.
Using Relationships in Subselects
In most cases the values you want as subselects are values from your entity's relationships. In these cases, you can use a shortcut to define your subselect in terms of your entity's relationships represented as a dot-delimited string.
Let's re-write the above subselect for 'lastLoginDate' for a User using the existing relationship:
component extends="quick.models.BaseEntity" accessors="true" {
/* properties */
function logins() {
return hasMany( "Login" );
}
function scopeAddLastLoginDate( query ) {
addSubselect( "lastLoginDate", "logins.timestamp" );
}
}
Much simpler! In addition to be much simpler this code is also more dynamic and reusable. We have a relationship defined for logins if we need to fetch them. If we change how the 'logins' relationship is structured, we only have one place we need to change.
With the query cleaned up using existing relationships, you might find yourself adding subselects directly in your handlers instead of behind scopes. This is fine in most cases. Keep an eye on how many places you use the subselect in case you need to re-evaluate and move it behind a scope.
var user = getInstance( "User" )
.addSubselect( "lastLoginDate", "logins.timestamp" )
.first();
user.getLastLoginDate(); // {ts 2019-05-02 08:24:51}
Dynamic Subselect Relationships
Subselects can be used in conjunction with relationships to provide a dynamic, constrained relationship. In this example we will pull the latest post for a user.
component extends="quick.models.BaseEntity" accessors="true" {
/* properties */
function scopeWithLatestPost( query ) {
return addSubselect( "latestPostId", newEntity( "Post" )
.select( "id" )
.whereColumn( "user_id", "users.id" )
.orderBy( "created_date", "desc" )
).with( "latestPost" );
}
function latestPost() {
return belongsTo( "Post", "latestPostId" );
}
}
This can be executed as follows:
var users = getInstance( "User" ).withLatestPost().all();
for ( var user in users ) {
user.getLatestPost().getTitle(); // My awesome post, etc.
}
As you can see, we are loading the id of the latest post in a subquery and then using that value to eager load the 'latestPost' relationship. This sequence will only execute two queries, no matter how many records are loaded.
Virtual Attributes
Virtual attributes are attributes that are not present on the table backing the Quick entity. A Subselect is an example of a virtual attribute. Other examples could include calculated counts or 'CASE' statement results.
By default, if you add a virtual column to a Quick query, you won't see anything in the entity. This is because Quick needs to have an attribute defined to map the result to. You can create a virtual attribute in these cases.
This step is unnecessary when using the 'addSubselect' helper method.
Here's an example including the result of a 'CASE' statement as a field:
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {
function scopeAddType( qb ) {
qb.selectRaw( "
CASE
WHEN publishedDate IS NULL THEN 'unpublished'
ELSE 'published'
END AS publishedStatus
" );
appendVirtualAttribute( "publishedStatus" );
}
}
With this code, we could now access the 'publishedStatus' just like any other attribute. It will not be updated, inserted, or saved though, as it is just a virtual column.
The 'appendVirtualAttribute' method adds the given name as an attribute available in the entity.
appendVirtualAttribute
Creates a virtual attribute for the given name.
It is likely that Quick will introduce more helper methods in the future making these calls simpler.
Retrieving Entities
Once you have an entity and its associated database table you can start retrieving data from your database.
You can configure your query to retrieve entities using any qb method. It is highly recommended you become familiar with the qb documentation.
Active Record
You start every interaction with Quick with an instance of an entity. The easiest way to do this is using WireBox. 'getInstance' is available in all handlers by default. WireBox can easily be injected in to any other class you need using 'inject="wirebox"`.
Quick is backed by qb, a CFML Query Builder. With this in mind, think of retrieving records for your entities like interacting with qb. For example:
var users = getInstance( "User" ).all();
for ( var user in users ) {
writeOutput( user.getUsername() );
}
In addition to using 'for' you can utilize the 'each' function on arrays. For example:
var users = getInstance( "User" ).all();
prc.users.each( function( user ) {
writeOutput( user.getUsername() );
});
You can add constraints to the query just the same as you would using qb directly:
var users = getInstance( "User" )
.where( "active", 1 )
.orderByDesc( "username" )
.limit( 10 )
.get();
For more information on what is possible with qb, check out the qb documentation.
Quick Service
A second way to retrieve results is to use a Quick Service. It is similar to a 'VirtualEntityService' from cborm.
The easiest way to create a Quick Service is to inject it using the 'quickService:' dsl:
component {
property name="userService" inject="quickService:User"
}
If you have a existing Service, and you would like to extend the quickService, you can extend the quikc.models.BaseService and then call super.init inside of the service init function passing the name of the entity (for example your User Entity) shown below:
component singleton extends="quick.models.BaseService" {
function init(){
super.init( "User" );
}
}
Any method you can call on an entity can be called on the service. A new entity will be used for all calls to a Quick Service.
var users = userService
.where( "active", 1 )
.orderByDesc( "username" )
.limit( 10 )
.get();
Aggregates
Calling qb's aggregate methods ('count, 'max
, etc.) will return the appropriate value instead of an entity or collection of entities.
existsOrFail
Returns true if any entities exist with the configured query. If no entities exist, it throws an EntityNotFound exception.
var doesUserExist = getInstance( "User" )
.existsOrFail( rc.userID );
Retrieval Methods
all
Retrieves all the records for an entity. Calling 'all' will ignore any non-global constraints on the query.
var users = getInstance( "User" ).all();
get
Executes the configured query, eager loads any relations, and returns the entities in a new collection.
var posts = getInstance( "Post" )
.whereNotNull( "publishedDate" )
.get();
paginate
Executes the configured query, eager loads any relations, and returns the entities in the configured qb pagination struct.
var posts = getInstance( "Post" )
.whereNotNull( "publishedDate" )
.paginate( rc.page, rc.maxrows );
// default response example
{
"results": [ User#1, User#2, ... ],
"pagination": {
"totalPages": 2,
"maxRows": 25,
"offset": 0,
"page": 1,
"totalRecords": 40
}
}
simplePaginate
Executes the configured query, eager loads any relations, and returns the entities in the configured qb simple pagination struct.
var posts = getInstance( "Post" )
.whereNotNull( "publishedDate" )
.simplePaginate( rc.page, rc.maxrows );
// default response example
{
"results": [ User#1, User#2, ... ],
"pagination": {
"hasMore": true,
"maxRows": 25,
"offset": 0,
"page": 1
}
}
first
Executes the configured query and returns the first entity found. If no entity is found, returns 'null`.
var user = getInstance( "User" )
.where( "username", rc.username )
.first();
firstWhere
Adds a basic where clause to the query and returns the first result.
var user = getInstance( "User" )
.firstWhere( "username", rc.username );
firstOrFail
Executes the configured query and returns the first entity found. If no entity is found, then an 'EntityNotFound' exception is thrown with the given or default error message.
var user = getInstance( "User" )
.where( "username", rc.username )
.firstOrFail();
firstOrNew
Finds the first matching record or returns an unloaded new entity.
var user = getInstance( "User" )
.firstOrNew( { "username": rc.username } );
firstOrCreate
Finds the first matching record or creates a new entity.
var user = getInstance( "User" )
.firstOrCreate( { "username": rc.username } );
find
Returns the entity with the id value as the primary key. If no record is found, it returns null instead.
var user = getInstance( "User" )
.find( rc.userID );
findOrFail
Returns the entity with the id value as the primary key. If no record is found, it throws an 'EntityNotFound' exception.
var user = getInstance( "User" )
.findOrFail( rc.userID );
findOrNew
Returns the entity with the id value as the primary key. If no record is found, it returns a new unloaded entity.
var user = getInstance( "User" ).findOrNew(
9999,
{
"firstName" : "doesnt",
"lastName" : "exist"
}
);
findOrCreate
Returns the entity with the id value as the primary key. If no record is found, it returns a newly created entity.
var user = getInstance( "User" ).findOrCreate(
9999,
{
"username" : "doesntexist",
"firstName" : "doesnt",
"lastName" : "exist",
"password" : "secret"
}
);
Hydration Methods
Hydration is a term to describe filling an entity with a struct of data and then marking it as loaded, without doing any database queries. For example, this might be useful when hydrating a user from session data instead of doing a query every request.
hydrate
Hyrdates an entity from a struct of data. Hydrating an entity fills the entity and then marks it as loaded.
If the entity's keys are not included in the struct of data, a 'MissingHydrationKey' is thrown.
var user = getInstance( "User" ).hydrate( {
"id": 4,
"username": "JaneDoe",
"email": "jane@example.com",
"password": "mypass1234"
} );
user.isLoaded(); // true
hydrateAll
Hydrates a new collection of entities from an array of structs.
var users = getInstance( "User" ).hydrateAll( [
{
"id": 3,
"username": "JohnDoe",
"email": "john@example.com",
"password": "mypass4321"
},
{
"id": 4,
"username": "JaneDoe",
"email": "jane@example.com",
"password": "mypass1234"
}
] );
Custom Collections
If you would like collections of entities to be returned as something besides an array, you can override the 'newCollection' method. It receives the array of entities. You can return any custom collection you desire.
newCollection
Returns a new collection of the given entities. It is expected to override this method in your entity if you need to specify a different collection to return. You can also call this method with no arguments to get an empty collection.
// Post.cfc
component extends="quick.models.BaseEntity" accessors="true" {
function newCollection( array entities = [] ) {
return variables._wirebox.getInstance(
name = "extras.QuickCollection",
initArguments = {
"collection" = arguments.entities
}
);
}
}
Eager Loading
The Problem
Let's imagine a scenario where you are displaying a list of posts. You fetch the posts:
prc.posts = getInstance( "Post" ).limit( 25 ).get():
And start looping through them:
<cfoutput>
<h1>Posts</h1>
<ul>
<cfloop array="#prc.posts#" item="post">
<li>#post.getTitle()# by #post.getAuthor().getUsername()#</li>
</cfloop>
</ul>
</cfoutput>
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 N+1 problem 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.
prc.posts = getInstance( "Post" )
.with( "author" )
.limit( 25 )
.get();
`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:
// Post.cfc
component extends="quick.models.BaseEntity" {
function author() {
return belongsTo( "User" );
}
}
To eager load the User in the snippet above you would call pass 'author' to the 'with' method.
getInstance( "Post" ).with( "author" ).get();
For this operation, only two queries will be executed:
SELECT * FROM 'posts' LIMIT 25
SELECT * FROM 'users' WHERE 'id' IN (1, 2, 3, 4, 5, 6, ...)
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.
// User.cfc
component extends="quick.models.BaseEntity" {
function country() {
return belongsTo( "User" );
}
}
getInstance( "Post" ).with( "author.country" );
You can eager load multiple relationships by passing an array of relation names to 'with' or by calling 'with' multiple times.
getInstance( "Post" ).with( [ "author.country", "tags" ] );
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.
// User.cfc
component {
function posts() {
return hasMany( "Post" );
}
function publishedPosts() {
return hasMany( "Post" ).published(); // published is a query scope on Post
}
}
You can eager load either option.
getInstance( "User" ).with( "posts" ).get();
getInstance( "User" ).with( "publishedPosts" ).get();
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:
getInstance( "User" ).with( { "posts" = function( query ) {
} } ).latest().get();
If you need to load nested relationships with constraints you can call 'with' in your constraint callback to continue eager loading relationships.
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();
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 Collections.
Custom Getters & Setters
Sometimes you want to use a different value in your code than is stored in your database. Perhaps you want to enforce that setting a password always is hashed with BCrypt. Maybe you have a Date value object that you want wrapping each of your dates. You can accomplish this using custom getters and setters.
A custom getter or setter is simply a function in your entity.
To retrieve the attribute value fetched from the database, call 'retrieveAttribute' passing in the name of the attribute.
To set an attribute for saving to the database, call 'assignAttribute' passing in the name and the value.
component extends="quick.models.BaseEntity" accessors="true" {
property name="bcrypt" inject="@BCrypt";
function setPassword( value ) {
return assignAttribute( "password", bcrypt.hashPassword( value ) );
}
function getCreatedDate( value ) {
return dateFormat( retrieveAttribute( "createdDate" ), "DD MMM YYYY" );
}
}
Custom getters and setters with not be called when hydrating a model from the database. For that use case, use 'casts'.
Debugging
Debugging a Single Query
toSQL
Returns the SQL that would be executed for the current query.
var userQuery = getInstance( "User" )
.where( "active", "=", 1 );
writeOutput( userQuery.toSQL() );
The bindings for the query are represented by question marks ('?') just as when using 'queryExecute. qb can replace each question mark with the corresponding 'cfqueryparam
-compatible struct by passing 'showBindings = true' to the method.
var userQuery = getInstance( "User" )
.where( "active", "=", 1 );
writeOutput( userQuery.toSQL( showBindings = true ) );
tap
Executes a callback with the current entity passed to it. The return value from 'tap' is ignored and the current entity is returned.
While not strictly a debugging method, 'tap' makes it easy to see the changes to an entity after each call without introducing temporary variables.
getInstance( "User" )
.tap( function( e ) {
writeOutput( e.toSQL() & "<br>" );
} )
.where( "active", "=", 1 )
.tap( function( e ) {
writeOutput( e.toSQL() & "<br>" );
} );
Debugging All Queries
cbDebugger
Starting in cbDebugger 2.0.0 you can view all your Quick and qb queries for a request. This is the same output as using qb standalone. This is enabled by default if you have qb installed. Make sure your debug output is configured correctly and scroll to the bottom of the page to find the debug output.
Additionally, with Quick installed you will see number of loaded entities for the request. This can help identify places that are missing pagination or relationships that could be tuned or converted to a subselect.
LogBox Appender
Quick is set to log all queries to a debug log out of the box via qb. To enable this behavior, configure LogBox to allow debug logging from qb's grammar classes.
qb can be quite chatty when executing many database queries. Make sure that this logging is only enabled for your development environments using ColdBox's environment controls.
Interception Points
ColdBox Interception Points can also be used for logging, though you may find it easier to use LogBox. See the documentation for qb's Interception Points or Quick's own interception points for more information.
Interception Points
Quick allows you to hook in to multiple points in the entity lifecycle. If the event is on the component, you do not need to prefix it with 'quick`. If you are listening to an interception point, include 'quick' at the beginning.
If you create your own Interceptors, they will not fire if you define them in your Main application. 'quick' will be loaded AFTER your interceptors, so the 'quick' interception points will not be registered with your interceptor. This can be solved by moving your interceptors to a module with a dependency on 'quick`, of by also registering the 'quick' custom interception points in your main coldbox configuration.
quickInstanceReady
Fired after dependency injection has been performed on the entity and the metadata has been inspected.
`interceptData' structure
quickPreLoad
Fired before attempting to load an entity from the database.
This method is only called for 'find' actions.
`interceptData' structure
quickPostLoad
Fired after loading an entity from the database.
`interceptData' structure
quickPreSave
Fired before saving an entity to the database.
This method is called for both insert and update actions.
`interceptData' structure
quickPostSave
Fired after saving an entity to the database.
This method is called for both insert and update actions.
`interceptData' structure
quickPreInsert
Fired before inserting an entity into the database.
`interceptData' structure
quickPostInsert
Fired after inserting an entity into the database.
`interceptData' structure
quickPreUpdate
Fired before updating an entity in the database.
`interceptData' structure
quickPostUpdate
Fired after updating an entity in the database.
`interceptData' structure
quickPreDelete
Fired before deleting a entity from the database.
`interceptData' structure
quickPostDelete
Fired after deleting a entity from the database.
`interceptData' structure
Serialization
getMemento
The memento pattern is an established pattern in ColdBox apps. A 'memento' in this case is a simple representation of your entity using arrays, structs, and simple values.
For instance, the following example shows a User entity and its corresponding memento:
component extends="quick.models.BaseEntity" accessors="true" {
property name="id";
property name="username";
property name="email";
property name="password";
property name="createdDate";
property name="modifiedDate";
}
{
"id" = 1,
"username" = "JaneDoe",
"email" = "jane@example.com",
"password" = "$2a$04$2nVI5rPOfl6.hrflkhBWOObO5Z7lXGJpi1vlosY74NrL/CKdpWqZS"
"createdDate" = "{ts '2018-03-12 16:14:10'}",
"modifiedDate" = "{ts '2018-03-12 16:14:10'}"
}
Quick bundles in the excellent Mementifier library to handle converting entities to mementos. This gives you excellent control over serialization using a 'this.memento' struct on the entity and passing in arguments to the 'getMemento' function.
this.memento
By default, Quick includes all defined attributes as 'includes`. You can change this or add other Mementifier options by defining your own 'this.memento' struct on your entity. Your custom 'this.memento' struct will be merged with Quick's default, so you can only define what changes you need.
Here is the default Quick memento struct. It is inside the 'instanceReady()' lifecycle method in this example because 'retrieveAttributeNames()' relies on the entity being wired (though not loaded); it is not otherwise necessary to put 'this.memento' inside 'instanceReady()`.
function instanceReady() {
this.memento = {
"defaultIncludes" : retrieveAttributeNames( withVirtualAttributes = true ),
"defaultExcludes" : [],
"neverInclude" : [],
"defaults" : {},
"mappers" : {},
"trustedGetters" : true,
"ormAutoIncludes" : false
};
}
getMemento Arguments
You can also control the serialization of a memento at call time using Mementifier's 'getMemento' arguments.
struct function getMemento(
includes = "", // or []
excludes = "", // or []
struct mappers = {},
struct defaults = {},
boolean ignoreDefaults = false,
boolean trustedGetters
)
Custom getMemento
If this does not give you the control you need, you can further modify the memento by overriding the 'getMemento' function on your entity. In this case, a '$getMemento' function will be available which is the Mementifier function.
component extends="quick.models.BaseEntity" accessors="true" {
property name="id";
property name="username";
property name="email";
property name="password";
property name="createdDate";
property name="modifiedDate";
function getMemento() {
return {
id = getId(),
username = getUsername(),
email = getEmail(),
createdDate = dateFormat( getCreatedDate(), "MM/DD/YYYY" ),
// can also use getAttribute if you want to bypass a custom getter
modifiedDate = dateFormat( retrieveAttribute( "modifiedDate" ), "MM/DD/YYYY" )
};
}
}
{
"id" = 1,
"username" = "JaneDoe",
"email" = "jane@example.com",
"createdDate" = "03/12/2018",
"modifiedDate" = "03/12/2018"
}
asMemento
Sometimes when retrieving entities or executing a Quick query, you already know you want mementos back. You can skip the step of calling 'getMemento' yourself or mapping over the array of results returned by calling 'asMemento' before executing the query. 'asMemento' takes the same arguments that 'getMemento' does. It will pass those arguments on and convert your entities to mementos after executing the query. This works for all the query execution methods - 'find, 'first
, 'get, 'paginate
, etc.
$renderData
The '$renderData' method is a special method for ColdBox. When returning a model from a handler, this method will be called and the value returned will be used as the serialized response. This let's you simply return an entity from a handler for your API. By default this will call 'getMemento()`.
component {
// /users/:id
function show( event, rc, prc ) {
return getInstance( "User" ).findOrFail( rc.id );
}
}
{
"id" = 1,
"username" = "JaneDoe",
"email" = "jane@example.com",
"createdDate" = "03/12/2018",
"modifiedDate" = "03/12/2018"
}
`QuickCollection' also defines a '$renderData' method, which will delegate the call to each entity in the collection and return the array of serialized entities.
Automatically serializing a returned collection only works when using the 'QuickCollection' as your entity's 'newCollection`.
component {
function index( event, rc, prc ) {
return getInstance( "User" ).all();
}
}
[
{
"id" = 1,
"username" = "JaneDoe",
"email" = "jane@example.com",
"createdDate" = "03/12/2018",
"modifiedDate" = "03/12/2018"
},
{
"id" = 2,
"username" = "JohnDoe",
"email" = "john@example.com",
"createdDate" = "03/14/2018",
"modifiedDate" = "03/15/2018"
}
]