FW/1 & QB Schema Builder
November 7, 2017
Not too long ago I wrote an article about working with FW/1 and QB; a query builder DSL written by Eric Peterson.
In this post, I wanted to touch on a new feature and component released in the latest version of QB, Schema Builder, and how to make use of it in an FW/1 application.
Schema Builder presents the ability to build out your database structure using the same awesome builder/chaining format as the Query Builder object. This allows us to create table layouts from the ground up in straight CFML.
Here's an example from the docs:
schema.create('users', function (table) {
table.increments('id');
table.string('email');
table.string('password');
table.timestamp('created_date').nullable();
table.timestamp('modified_date').nullable();
});
Getting Started / Prerequisites
In my previous post, I went over how to setup and use QB in an FW/1 application. If you're unfamiliar with the setup, I highly recommend glancing through its content as I'll only be adding on to those code examples here.
So to tie everything together you'll want to have CommandBox installed, a basic FW/1 application structure using subsystems, a box.json for defining dependencies like FW/1 and QB (all explained in the linked post above) and a mapping in Application.cfc to the path where QB is kept.
Wiring Things Up With DI/1
With a functioning FW/1 application and QB set up as a subsystem, It's time to wire up QB and its components in DI/1.
Within the variables.framework.subsystems struct in Application.cfc, we'll define the QB components and their dependencies while also including the SchemaBuilder component.
qb.diLocations: ["/models"],
qb.diConfig: {
loadListener: function(di1) {
di1.declare("BaseGrammar").instanceOf("qb.models.Grammars.BaseGrammar").done()
.declare("MySQLGrammar").instanceOf("qb.models.Grammars.MySQLGrammar").done()
.declare("QueryUtils").instanceOf("qb.models.Query.QueryUtils").done()
.declare("QueryBuilder").instanceOf("qb.models.Query.QueryBuilder")
.withOverrides({
grammar: di1.getBean("MySQLGrammar"),
utils: di1.getBean("QueryUtils"),
returnFormat: "array"
}).done()
.declare("SchemaBuilder").instanceOf("qb.models.Schema.SchemaBuilder")
.asTransient()
.withOverrides({
grammar: di1.getBean("MySQLGrammar")
});
}
}
Breaking It Down
qb.diLocations: "/models" tells DI/1 to create a subsystem bean factory and gather objects in /qb/models based on the default conventions.
In qb.diConfig we define a load listener with a closure that takes the DI/1 object as an argument. This is for defining our bean factory settings. In the function block, we use DI/1's builder syntax to declare individual bean objects of QB's components. The declare() method is used to define an "alias" to the component. This is useful for having DI/1 automagically satisfy an object's constructor arguments. The declaration of "QueryBuilder" calls withOverrides() to pass in specific arguments to the object's constructor, or init method. Then we define "SchemaBuilder" as a transient object which takes the same grammar object (MySQLGrammar) as QueryBuilder.
Note: The declaration of MySQLGrammar. This is specific support in QB for the MySQL dialect. There are other options available. See the /qb/models/Grammars folder.
Now we just need to start our application from CommandBox to have a working environment.
Examples
Usage
To use the QueryBuilder from DI/1, we can call on it like so:
QueryBuilder = getBeanFactory('qb').getBean('QueryBuilder');
To use SchemaBuilder from DI/1, we call it the same as QueryBuilder:
SchemaBuilder = getBeanFactory('qb').getBean('SchemaBuilder');
Creating Tables
Let's build out a posts and authors table.
// Authors
SchemaBuilder.create('authors', function (table) {
table.increments('UserID');
table.string('FirstName', 150);
table.string('LastName', 150);
table.string('Username');
table.string('Password', 60);
table.timestamp('CreateDate');
table.timestamp('ModifyDate');
});
// Posts
SchemaBuilder.create('posts', function (table) {
table.increments('PostID');
table.string('Slug');
table.string('Title');
table.longText('Body');
table.integer('ViewCount');
table.string('Keywords');
table.string('Description');
table.timestamp('CreateDate');
table.timestamp('ModifyDate');
table.timestamp('PublishDate');
table.bit('IsDraft');
table.bit('Active');
table.unsignedInteger('UserID').references('UserID').onTable('authors');
});
The above examples demonstrate scaffolding out columns of various data types as well as defining incrementing IDs and constraints on another table.
Checking If A Table Exists
if (!SchemaBuilder.hasTable('posts')) {
SchemaBuilder.create('posts', function (table) {
// scaffold columns...
});
}
Dropping Tables
// Drop
SchemaBuilder.drop('posts');
// Drop if exists
SchemaBuilder.dropIfExists('authors');
Altering Columns
// Add
SchemaBuilder.alter('authors', function (table) {
table.addColumn(table.boolean('active'));
});
// Drop
SchemaBuilder.alter('authors', function (table) {
table.dropColumn('active');
});
// Modify
SchemaBuilder.alter('authors', function (table) {
table.modifyColumn('name', table.string('firstname'));
});
// Rename
SchemaBuilder.alter('authors', function (table) {
table.renameColumn('name', table.string('firstname'));
});
Populating
We can then use QueryBuilder to populate the created tables.
// Author
QueryBuilder.from('authors').insert(
(values = {
firstname: 'John',
lastname: 'Doe',
username: 'jdoe',
password: 'tvBvOpODv4BiPGwCcPFeenYIVFis6LuDgqX',
createdate: now(),
modifydate: now()
})
);
// Post
QueryBuilder.from('posts').insert(
(values = {
slug: 'test',
title: 'Test',
body: 'This is a test.',
viewcount: 0,
keywords: '',
description: '',
createdate: now(),
modifydate: now(),
publishdate: now(),
isdraft: 0,
active: 1,
userid: 1
})
);
Retrieving Data
QueryBuilder.from('Posts').join('Authors', 'Authors.ID', '=', 'Posts.AuthorID').get();
What's Next?
Refer to the Official QB Docs for more examples.
To further see FW/1 & QB in action, you can check out my example application on GitHub.
Cheers.