Mach-II Primer : 4 : Using Gateways to manage record sets
So far we've covered defining events and piecing together the user interface through the controller (mach-ii.xml). Now we need to move queries and business logic out of the View and into the Model. To get started, we'll move queries that return more than one record into Gateway objects using Coldfusion Components.
Preparation
We'll begin with the Version 2 files.
If you didn't switch to the version 2 files at the end of part 3:
- Rename the version 1 mach-ii.xml file to mach-ii.01.xml
- Rename mach-ii.02.xml to mach-ii.xml
Mach-II should be loading version 2 of mach-ii.xml using the "/02/" folders.
As we step through this process, we'll begin seeing version 3 files.
Terms to know:
- A function is also known as a method.
- A Coldfusion Component (CFC) will often be referred to as an Object.
Getting organized
Open up version 2 of the contact list. It's pretty simple:
- HTML
- <cfquery>
- <cfoutput> looping over more HTML to display the query records
<p><a href="contact_form.cfm?CONTACT_ID=0">Add a Contact</a></p>
<cfquery name="qContacts" datasource="#request.DSN#">
SELECT
CONTACT_ID,
CONTACT_FIRST_NAME,
CONTACT_LAST_NAME
FROM
CF_CONTACTS
ORDER BY
CONTACT_LAST_NAME, CONTACT_FIRST_NAME
</cfquery>
<ul>
<cfoutput query="qContacts">
<li>
[ <a href="contact_form.cfm?CONTACT_ID=#qContacts.CONTACT_ID#">Edit</a> ]
<a href="contact_detail.cfm?CONTACT_ID=#qContacts.CONTACT_ID#">#qContacts.CONTACT_LAST_NAME#, #qContacts.CONTACT_FIRST_NAME#</a>
</li>
</cfoutput>
</ul>
Let's first do something simple and organize the code so that all queries (and potentially any business logic) is run at the top of the page.
SELECT
CONTACT_ID,
CONTACT_FIRST_NAME,
CONTACT_LAST_NAME
FROM
CF_CONTACTS
ORDER BY
CONTACT_LAST_NAME, CONTACT_FIRST_NAME
</cfquery>
<p><a href="contact_form.cfm?CONTACT_ID=0">Add a Contact</a></p>
<ul>
<cfoutput query="qContacts">
<li>
[ <a href="contact_form.cfm?CONTACT_ID=#qContacts.CONTACT_ID#">Edit</a> ]
<a href="contact_detail.cfm?CONTACT_ID=#qContacts.CONTACT_ID#">#qContacts.CONTACT_LAST_NAME#, #qContacts.CONTACT_FIRST_NAME#</a>
</li>
</cfoutput>
</ul>
contacts.cfm is part of the presentation layer, so we need to move the query over to the Model. Since this query returns a list of records it should be moved into what's called a Gateway object.
CFFunction Overview
If you've not used <cffunction> before, lets go over its attributes.
- name: The name of the function. No two functions in a CFC can have the same name.
- access:
- A public function can be called from inside or outside the CFC.
- private functions can only be called by other functions in the same file.
- package access allows calls from functions in the same file and functions from other CFC files in the same folder (package).
- remote functions are used as Web Services, but that's a discussion for another primer
- output: since we're only working with data and logic, always set this to false
- returntype: the datatype returned from the function - string, numeric, struct, query, component name, etc.
Now take a look at getAllContacts(). Notice we've set a variable that's the same name as the <cfquery>
<cfquery name="qContacts" datasource="#variables.DSN#">
This creates the variable "qContacts" in the var scope, which makes it a function local variable. Now "qContacts" is only available to the function getAllContacts() and cannot be read or have its value changed by another function. This is important when you have more than one function using variables with the same name.
Always var scope names of queries, cfloop indexes and other tag-defined variables
varScoper is a tool designed by Mike Schierberl "to identify variables created within a cffunction that don't have a corresponding (cfset var) statement."
Coldfusion Components
Here is ContactGateway.cfc which contains two functions.
<cfcomponent name="ContactGateway" output="false" hint="Defines Gateway functions for Contacts">
<cffunction name="init" access="public" output="false" returntype="ContactGateway" hint="constructor">
<cfargument name="DSN" type="string" required="true" hint="datasource" />
<cfset variables.DSN = arguments.DSN />
<cfreturn this />
</cffunction>
<cffunction name="getAllContacts" access="public" output="false" returntype="query" hint="returns a query recordset of contacts">
<cfset var qContacts = "" />
<cfquery name="qContacts" datasource="#variables.DSN#">
SELECT
CONTACT_ID,
CONTACT_FIRST_NAME,
CONTACT_LAST_NAME
FROM
CF_CONTACTS
ORDER BY
CONTACT_LAST_NAME, CONTACT_FIRST_NAME
</cfquery>
<cfreturn qContacts />
</cffunction>
</cfcomponent>
init()
This is the constructor method. This method will initialize the component, set data into the variables scope of the component and return an instance of the component.
The returntype attribute should specify the CFC file name with or without the dot separated path to the component.
In order to pass existing variables into a component, you can inject those values into the object using the variables scope.
<cfset variables.DSN = arguments.DSN />
Finally, return the object.
getAllContacts()
This runs the query "qContacts" and returns the recordset. There are three things to note about this function:
- uses the returntype "query"
- the name of the query is var scoped
- the cfquery uses the datasource "#variables.DSN#", which was defined by the init() method
Object Instantiation
To create an instance of the Gateway object:
You want to avoid calling shared scoped variables (i.e. application.*, session.*) from inside a CFC, so make sure to inject them into the component through the init() method.
If the value of session.DSN is changed outside of the CFC, the value "inside" the CFC (in the variables scope) is unaffected.
However, if the session variable is a non-simple datatype like a struct or another component (i.e. a Bean), changing the value inside the CFC will change the value of the actual session variable outside the CFC. See There are no Pointers in ColdFusion for more information.
Calling the Model from the View
Let's go back to contacts.cfm and get the contact records from the Gateway object
<p><a href="contact_form.cfm?CONTACT_ID=0">Add a Contact</a></p>
<ul>
<cfoutput query="qContacts">
<li>
[ <a href="contact_form.cfm?CONTACT_ID=#qContacts.CONTACT_ID#">Edit</a> ]
<a href="contact_detail.cfm?CONTACT_ID=#qContacts.CONTACT_ID#">#qContacts.CONTACT_LAST_NAME#, #qContacts.CONTACT_FIRST_NAME#</a>
</li>
</cfoutput>
</ul>
You can see all the Gateway objects at this point under the version 3 folders.
Next step
We've started moving queries out of the View and into the Model, but now we're creating objects in the View. Anytime the object definition changes, we may need to update all the View files that reference it and that's what we were trying to avoid with the queries.
Using Mach-II Listeners we'll remove object instantiation from the View and reference objects and methods from the Controller (mach-ii.xml).









<cffunction name="getContactByLastName" access="public" ...>
<cfargument name="last_name" type="string" ...>
<cfset var lastName = arguments.last_name />
However, when I call this function repeatedly, arguments.last_name doesn't change. It retains the value of the first call. I know you have to var scope your internal function variables, but it's weird that the argument itself is not changing. Am I doing something wrong?
Thanks.
When you say the "argument is not changing" I am not sure what you mean. Are you passing a different parameter to the method when you call it? I don't see how else you could get the same value over and over again.
Eric
Is the problem you're having related to this post on the Mach-II Google Groups? If so, then I left an answer there.
http://groups.google.com/group/mach-ii-for-coldfus...
The problem may lie with how you're sending data into the CFC and not how the CFC is handling the data.
<cfcomponent displayname="ContactListener" output="false" extends="MachII.framework.Listener"
hint="ContactListener for the Mach-II Contact Manager sample application">
<!--- this configure method is called by Mach-II automatically when the application loads --->
<cffunction name="configure" access="public" output="false" returntype="void"
hint="Configures this listener as part of the Mach-II framework">
<!--- We'll need access to our dataobjects (gateway and DAO) in this Listener. We get the datasource
name from the Mach-II properties we declared in the XML config file by using the getProperty function. --->
<cfset variables.contactGateway = CreateObject("component", "ContactGateway").init(getProperty("dsn")) />
<cfset variables.contactDAO = CreateObject("component", "ContactDAO").init(getProperty("dsn")) />
</cffunction>
<!--- GATEWAY-RELATED METHODS (gateways deal with *multiple* records) --->
<!--- get all contacts - returns a query object containing all the contacts --->
<cffunction name="getAllContacts" access="public" output="false" returntype="query"
hint="Returns a query object containing all the contacts">
<cfreturn variables.contactGateway.getAllContacts() />
</cffunction>
<!--- get "recent" contacts, which we'll define as the last three added --->
<cffunction name="getRecentContacts" access="public" output="false" returntype="query"
hint="Returns an query object containing recent contacts">
<cfreturn variables.contactGateway.getRecentContacts() />
</cffunction>
...
</cfcomponent>
In his example, he is returning all contacts or recent contacts, but when I adapted his example, I created a method that requires an argument to search my database, i.e., getContactByLastName(lastName).
<cffunction name="getContactsByLastName" access="public" output="false" returntype="query"
hint="Returns an query object containing recent contacts">
<cfargument name="lastName" type="string" required="yes" />
<cfreturn variables.contactGateway.getContactsByLastName(#argument.lastName#) />
</cffunction>
However, when I called getContactsByLastName repeatedly, I got the same record each time. I got it to work properly by instantiating the Gateway inside my method instead of in the configure method as follows:
<cffunction name="getContactsByLastName" access="public" output="false" returntype="query"
hint="Returns an query object containing recent contacts">
<cfargument name="lastName" type="string" required="yes" />
<cfset var contactGateway = CreateObject("component", "ContactGateway").init(getProperty("dsn")) />
<cfreturn contactGateway.getContactsByLastName(#argument.lastName#) />
</cffunction>
I'm not sure I understand why it wasn't working by instantiating the Gateway in the configure method. Perhaps I was doing something else wrong, but I verified by dumping the argument lastName before it was passed into getContactByLastName that lastName was changing, but the record returned by the method was the same for every call.
Also, what is the difference between using "configure" and "init" for the constructor?
Thanks,
Kin
At first, I thought the problem you're having was in your getContactsByLastName() method. In this line:
<cfreturn contactGateway.getContactsByLastName(#argument.lastName#) />
maybe it's a typo in your post, but the correct scope is "arguments", not "argument". Also, you don't need the "#" around a variable when you're passing it into a function:
<cfreturn contactGateway.getContactsByLastName( arguments.lastName ) />
After seeing that you could get the correct result by _not_ creating the Gateway in the configure(), it is most likely the case that there is s problem in your ContactGateway.cfc file and not in the Listener.
When using configure(), the Gateway object is created once, then referenced as needed. By instantiating it in getContactsByLastName(), you're creating a new instance every time you call that function. It seems as if something other than the DSN is being set into the variables scope whenever you call the ContactGateway.init() method. That variable is then being used by getContactsByLastName() as a parameter in the query.
In Step 5, I don't define objects in the configure() method as Matt's example does because I plan to cover that concept in a later step.