What to expect

  • An overview of ColdBox and Quick
  • Why you would use this library
  • Example app to explore how it works
  • Lots of live coding!

What not to expect

  • A short session
  • How to get started with CommandBox or ColdBox
  • A deep dive into any related or used modules

What is Quick and Why would I use it?

What is Quick?

  • Map database tables to components
  • Create relationships between components
  • Query and manipulate data
  • Persist changes to your database

Why use an ORM Engine?

  • Skip writing repetitive SQL
  • Automate tasks like eager loading
  • Encourage Object-Oriented code

Why Quick?

  • Not tied to CFML engine releases
  • Better error messages
  • Lightweight
  • Adoptable
  • Contribute-able

Setup

// config/ColdBox.cfc
component {

    function configure() {
        moduleSettings = {
            "quick" = {
                "defaultGrammar" = "AutoDiscover@qb"
            }
        };
    }

}
box install quick

Installation

Defining Entities

Mapping an Entity

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

    property name="id";

    property name="email"
        update="false"
        insert="true";

    property name="password";

    property name="createdDate"
        column="created_date";

    property name="modifiedDate"
        column="modified_date";

    property name="lastLoggedIn"
        column="last_logged_in"
        nullValue="REALLY_NULL";

    property name="number"
        sqltype="cf_sql_varchar"
        convertToNull="false";

}
// models/User.cfc
component extends="quick.models.BaseEntity" accessors="true" {
  
    property name="bcrypt" inject="@BCrypt" persistent="false";
    property name="password";
    property name="createdDate" column="created_date";

    /* ... */

    function setPassword( password ) {
        assignAttribute(
            "password",
            bcrypt.hashPassword( password )
        );
        return this;
    }

    function getCreatedDate() {
        return dateFormat(
            retrieveAttribute( "createdDate" ),
            "mm/dd/yyyy"
        );
    }
  
}

Custom Getters and Setters

Fetching

Active Entity Pattern

// handlers/users.cfc
component {
    function index( event, rc, prc ) {
        var user = getInstance( "User" );
        /* ... */
    }
}

Virtual Service Pattern

// handlers/users.cfc
component {
    property name="userService" inject="quickService:User";

    /* ... */
}

Fetch Methods

  • all
  • get
  • find{orFail}
  • first{orFail}

Basic Fetch Methods

// handlers/users.cfc
component {
    
    property name="userService" inject="quickService:User";

    // GET /users
    function index( event, rc, prc ) {
        prc.users = userService.all();
    }

    // GET /users/:id
    function show( event, rc, prc ) {
        prc.user = getInstance( "User" ).findOrFail( rc.id );
    }

}

Advanced Fetch Methods

// handlers/users.cfc
component {
    
    property name="userService" inject="quickService:User";

    // GET /users/search
    function search( event, rc, prc ) {
        prc.users = userService
            .whereBetween( "createdDate", rc.startDate, rc.endDate )
            .when( rc.search != "", function( q ) {
                q.where( "username", "like", "%#rc.search#%" );
            } );
            .get();
    }

    // GET /users/:username
    function show( event, rc, prc ) {
        prc.user = userService
            .where( "username", rc.username )
            .firstOrFail();
    }

}

Persistence

Creating and Updating

// handlers/users.cfc
component {
    property name="userService" inject="quickService:User";

    // POST /users
    function store( event, rc, prc ) {
        userService.create( {
            "email" = rc.email,
            "password" = rc.password,
            "createdDate" = now(),
            "modifiedDate" = now()
        } );
        relocate( "users" );
    }

    // PUT /users/:id
    function update( event, rc, prc ) {
        var user = userService.findOrFail( rc.id );
        user.update( {
            "email" = rc.email,
            "password" = rc.password,
            "modifiedDate" = now()
        } );
        relocate( "users.#user.getId()#" );
    }
}

Key Types

  • NullKeyType
  • AutoIncrementingKeyType
  • ReturningKeyType
  • RowIDKeyType
  • UUIDKeyType
  • Custom Key Types

Key Types

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

    function keyType() {
        return variables._wirebox
            .getInstance( "NullKeyType@quick" );
    }

}

Deleting

// handlers/users.cfc
component {

    property name="userService" inject="quickService:User";

    // DELETE /users/:id
    function delete( event, rc, prc ) {
        var user = userService.findOrFail( rc.id );
        user.delete();
        // OR
        userService.deleteAll( rc.id );
    }

}

Mass Update and Deletes

// handlers/inactiveUsers.cfc
component {

    property name="userService" inject="quickService:User";

    // PATCH /inactive-users
    function update( event, rc, prc ) {
        userService
            .where( "last_logged_in", "<", dateAdd( "m", -3, now() ) )
            .updateAll( {
                "active" = 0
            } );
    }

    // DELETE /inactive-users
    function delete( event, rc, prc ) {
        userService
            .where( "active", 0 )
            .deleteAll();
    }

}

Scopes and Relationships

Query Scopes

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

    property name="id";

    property name="createdDate"
        column="created_date";
    
    /* ... */

    function scopeLatest( query ) {
        return query.orderBy( "createdDate", "desc" );
    }

}
// handlers/users.cfc
component {

    function index( event, rc, prc ) {
        prc.users = getInstance( "User" )
            .latest()
            .get();
    }

}

Dynamic Query Scopes

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

    property name="id";

    property name="createdDate"
        column="created_date";
    
    function scopeCreatedInYear( query, year ) {
        return query.whereBetween(
            "createdDate",
            createDate( year, 1, 1 ),
            createDate( year, 12, 31 )
        );
    }

}
// handlers/users.cfc
component {

    function index( event, rc, prc ) {
        prc.users = getInstance( "User" )
            .createdInYear( 2018 )
            .get();
    }

}

Relationships

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

    property name="id";
    property name="title";
    property name="userId" column="user_id";
    
    /* ... */

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

}
<!-- views/post/show.cfm -->
<cfoutput>
    <h3>#prc.post.getTitle()#</h3>
    <h4>By #prc.post.getAuthor().getEmail()#</h4>
    <!-- ... -->
</cfoutput>

Constraining Relationships

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

    property name="id";
    property name="slug";
    
    /* ... */

    function comments() {
        return hasMany( "Comment" );
    }

}
// handlers/posts.cfc
component {

    // GET /posts/:slug
    function show( event, rc, prc ) {
        prc.post = getInstance( "Post" ).whereSlug( slug ).findOrFail();
        prc.comments = prc.post.comments().approved().get();
    }

}

Constraining Relationships

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

    property name="id";
    property name="slug";
    
    /* ... */

    function comments() {
        return hasMany( "Comment" );
    }

    function approvedComments() {
        return comments().approved();
    }

}
// handlers/posts.cfc
component {

    // GET /posts/:slug
    function show( event, rc, prc ) {
        prc.post = getInstance( "Post" ).whereSlug( slug ).findOrFail();
        prc.comments = prc.post.getApprovedComments();
    }

}

Serialization

Memento

getInstance( "User" ).findOrFail( 1 ).getMemento();

{
    "id" = 1,
    "email" = "john@example.com",
    "createdDate" = "{ts '2019-04-13 02:33:44'}",
    "modifiedDate" = "{ts '2019-04-13 02:33:44'}"
}
component extends="quick.models.BaseEntity" {

    property name="id";
    property name="email";
    property name="createdDate" column="created_date";
    property name="modifiedDate" column="modified_date";

}

Mementifier

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

    property name="id";
    property name="username";
    property name="email";
    property name="password";
    property name="createdDate";
    property name="modifiedDate";

}
var memento = user.getMemento(
    excludes = [ "password" ]
);

/*
{
    "id" = 1,
    "username" = "JaneDoe",
    "email" = "jane@example.com",
    "createdDate" = "{ts '2018-03-12 16:14:10'}",
    "modifiedDate" = "{ts '2018-03-12 16:14:10'}"
}
*/

asMemento

var users = getInstance( "User" )
    .asMemento( excludes = [ "password" ] )
    .get()

/*
[
    {
        "id" = 1,
        "username" = "JaneDoe",
        "email" = "jane@example.com",
        "createdDate" = "{ts '2018-03-12 16:14:10'}",
        "modifiedDate" = "{ts '2018-03-12 16:14:10'}"
    },
    {
        "id" = 2,
        "username" = "JohnDoe",
        "email" = "john@example.com",
        "createdDate" = "{ts '2018-03-12 16:14:10'}",
        "modifiedDate" = "{ts '2018-03-12 16:14:10'}"
    },
    ...
]
*/

asQuery

var users = getInstance( "User" )
    .select( [ "id", "email" ] )
    .asQuery()
    .get()

/*
[
    {
        "id" = 1,
        "email" = "jane@example.com"
    },
    {
        "id" = 2,
        "email" = "john@example.com"
    },
    ...
]
*/

Subselects

Subselects

var post = getInstance( "Post" )
    .addSubselect( "authorEmail", function( qb ) {
        return qb.select( "email" )
            .from( "users" )
            .whereColumn( "users.id", "posts.userID" );
    } )
    .firstOrFail();

post.getAuthorEmail(); // john@example.com

Subselects via Relationships

component displayname="Post" extends="quick.models.BaseEntity" accessors="true" {
  
    property name="id";
    property name="title";
    property name="body";
    property name="createdDate";
    property name="modifiedDate";
  
    function author() {
        return belongsTo( "User" );
    }
  
}
var post = getInstance( "Post" )
    .addSubselect( "authorEmail", "author.email" )
    .firstOrFail();

post.getAuthorEmail(); // john@example.com
component displayname="User" extends="quick.models.BaseEntity" accessors="true" {
  
    property name="id";
    property name="email";
    property name="createdDate";
    property name="modifiedDate";
  
}

Relationship Counts

var post = getInstance( "Post" )
    .addSubselect( "commentsCount", function( qb ) {
        qb.selectRaw( "COUNT(*)" )
            .from( "comments" )
            .whereColumn( "comments.postID", "posts.id" );
    } )
    .firstOrFail();

post.getCommentsCount(); // 5

Relationship Counts

component displayname="Post" extends="quick.models.BaseEntity" accessors="true" {
  
    property name="id";
    property name="title";
    property name="body";
    property name="createdDate";
    property name="modifiedDate";
  
    function comments() {
        return hasMany( "Comment" );
    }
  
}
var post = getInstance( "Post" )
    .withCount( [ "comments" ] )
    .firstOrFail();

post.getCommentsCount(); // 5

Debugging

toSQL

var sql = getInstance( "Post" )
    .latest()
    .toSQL();

/*
SELECT *
FROM `posts`
ORDER BY `posts`.createdDate` DESC
*/

dump

var posts = getInstance( "Post" )
    .dump( label = "before latest scope" )
    .latest()
    .dump(
        showBindings = "inline",
        label = "after latest scope"
    )
    .get();

LogBox Debug Logs

// config/ColdBox.cfc
component {
  
    function configure() {
        logbox = {
            debug = [ "qb.models.Grammars" ]
        };  
    }
  
}

cbDebugger

Now, to the code!

ITB 2023 — Quick in 100 Minutes

By Eric Peterson

ITB 2023 — Quick in 100 Minutes

  • 302