Loading "Extra" Queries for a Form in CFWheels
November 23, 2011 · Chris Peters
Many times, you’ll build a form where you need to load "extra" queries for things like state and country selectors. I’ll cover 3 different approaches for CFWheels with pros and cons.
Many times, you’ll see this sort of situation when writing a form, where you need to load “extra” queries for things like state and country selectors:
<!--- `views/customers/new.cfm` ---> | |
<cfparam name="customer"> | |
<cfparam name="countries" type="query"> | |
<cfparam name="states" type="query"> | |
<cfoutput> | |
#startFormTag(route="customer")# | |
#textField(objectName="customer", property="name")# | |
#textField(objectName="customer", property="email")# | |
#select(objectName="customer", property="countryId", options=countries)# | |
#select(objectName="customer", property="stateId", options=states)# | |
#endFormTag()# | |
</cfoutput> |
There are many ways that you can load the data that this view template is requiring, each with their pros and cons. I’ll cover 3 different approaches.
Approach #1: Use before filters to stay DRY
One way to solve this problem in the controller is to run before filters on the new
, create
, edit
, and update
actions:
// `controllers/Customers.cfc` | |
component extends="Controller" { | |
function init() { | |
verifies(only="edit,update,show", params="key", paramsTypes="integer"); | |
verifies(only="create,update", params="customer"); | |
filters(only="new,create,edit,update", through="$setStates,$setCountries"); | |
} | |
function new() { | |
customer = model("customer").new(); | |
} | |
function create() { | |
customer = model("customer").new(params.customer); | |
if (customer.save()) { | |
redirectTo(route="customer", key=customer.key(), success="..."); | |
} | |
else { | |
flashInsert(error="..."); | |
renderPage(action="new"); | |
} | |
} | |
function edit() { | |
customer = model("customer").findByKey(params.key); | |
} | |
function update() { | |
customer = model("customer").findByKey(params.key); | |
if (customer.update(params.customer)) { | |
redirectTo(route="customer", key=customer.key(), success="..."); | |
} | |
else { | |
flashInsert(error="..."); | |
renderPage(action="edit"); | |
} | |
} | |
function show() { | |
customer = model("customer").findByKey(params.key); | |
} | |
private function $setStates() { | |
states = model("state").findAll(order="name"); | |
} | |
private function $setCountries() { | |
countries = model("countries").findAll(order="name"); | |
} | |
} |
Notice the call to filters()
in init()
to load the $setStates()
and $setCountries()
private filter methods.
Yes, this works. But one problem is that you only need to load states
and countries
if the form needs to be shown with errors. On create
and update
, those queries may not even need to be run, but they’re running anyway. Why hit the database if you don’t need to?
Depending on your caching strategy, this may not matter if you are caching the findAll()
calls. But let’s say that you are running at least one of those queries in real time. How do you avoid running them if you don’t need them?
Approach #2: Limiting the use of filters and calling them manually in create
and update
An approach that alleviates that problem is to limit the before filters to new
and edit
and only run them when needed in create
and update
:
// `controllers/Customers.cfc` | |
component extends="Controller" { | |
function init() { | |
verifies(only="edit,update,show", params="key", paramsTypes="integer"); | |
verifies(only="create,update", params="customer"); | |
filters(only="new,create,edit,update", through="$setStates,$setCountries"); | |
} | |
function new() { | |
customer = model("customer").new(); | |
} | |
function create() { | |
customer = model("customer").new(params.customer); | |
if (customer.save()) { | |
redirectTo(route="customer", key=customer.key(), success="..."); | |
} | |
else { | |
flashInsert(error="..."); | |
renderPage(action="new"); | |
} | |
} | |
function edit() { | |
customer = model("customer").findByKey(params.key); | |
} | |
function update() { | |
customer = model("customer").findByKey(params.key); | |
if (customer.update(params.customer)) { | |
redirectTo(route="customer", key=customer.key(), success="..."); | |
} | |
else { | |
flashInsert(error="..."); | |
renderPage(action="edit"); | |
} | |
} | |
function show() { | |
customer = model("customer").findByKey(params.key); | |
} | |
private function $setStates() { | |
states = model("state").findAll(order="name"); | |
} | |
private function $setCountries() { | |
countries = model("countries").findAll(order="name"); | |
} | |
} |
Notice the modified call to filters()
in init()
and the manual calls to $setStates()
and $setCountries()
in the create
and update
actions. This is better because now the states
and countries
are only being loaded when absolutely needed. This does make things slightly less DRY, but it’s not too bad of a trade-off.
Approach #3: Using automatic data functions to load data for the form
Yet another approach is to factor out the form into a partial and use what’s called an automatic data function to load the extra queries. So the view template changes to this (which you may be doing anyway):
<!--- `views/customers/new.cfm` ---> | |
<cfoutput> | |
#startFormTag(route="customer")# | |
#includePartial("form")# | |
#endFormTag()# | |
</cfoutput> |
And now you create a partial:
<!--- `views/customers/_form.cfm` ---> | |
<cfparam name="customer"> | |
<cfparam name="arguments.countries" type="query"> | |
<cfparam name="arguments.states" type="query"> | |
<cfoutput> | |
#textField(objectName="customer", property="name")# | |
#textField(objectName="customer", property="email")# | |
#select(objectName="customer", property="countryId", options=arguments.countries)# | |
#select(objectName="customer", property="stateId", options=arguments.states)# | |
</cfoutput> |
You may now notice that the countries
and states
variables are now being referenced in the arguments
scope. That’s because we’re going to load them from an automatic data function. The controller should now look like this:
// `controllers/Customers.cfc` | |
component extends="Controller" { | |
function init() { | |
verifies(only="edit,update,show", params="key", paramsTypes="integer"); | |
verifies(only="create,update", params="customer"); | |
} | |
function new() { | |
customer = model("customer").new(); | |
} | |
function create() { | |
customer = model("customer").new(params.customer); | |
if (customer.save()) { | |
redirectTo(route="customer", key=customer.key(), success="..."); | |
} | |
else { | |
flashInsert(error="..."); | |
renderPage(action="new"); | |
} | |
} | |
function edit() { | |
customer = model("customer").findByKey(params.key); | |
} | |
function update() { | |
customer = model("customer").findByKey(params.key); | |
if (customer.update(params.customer)) { | |
redirectTo(route="customer", key=customer.key(), success="..."); | |
} | |
else { | |
flashInsert(error="..."); | |
renderPage(action="edit"); | |
} | |
} | |
function show() { | |
customer = model("customer").findByKey(params.key); | |
} | |
private struct function form() { | |
local.data.states = model("state").findAll(order="name"); | |
local.data.countries = model("countries").findAll(order="name"); | |
return local.data; | |
} | |
} |
The true points of interest in this example are the init()
and form()
. We’re now no longer running before filters in init()
to load the states
and countries
.
Also, there is a new private method named form()
that acts as an automatic data function. Every time you call a partial, Wheels checks for a method in your controller with the name of the partial, private
access, and a return type of struct
. If it finds that method, it’ll run it and merge the struct that it returns into the arguments
scope in the view.
So form()
is only being run when the form is shown via the call to #includePartial("form")#
. And the data loading is still happening in the controller where it belongs.
I’ve only begun playing around with this method and so far have been enjoying the results. I am slightly torn because I like the “self-documenting” nature of declaring filters in init()
, but that method does have the caveat that I mentioned before. The automatic data function solves this problem, but it does make the functionality a little less obvious. You need to remember the extra step of checking to see if there is a method in the controller with the same name as the partial when you run across one.
Has anyone else play with this method? If so, how has this worked out for you?