i Know Kung Foo Consulting

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:

  1. Rename the version 1 mach-ii.xml file to mach-ii.01.xml
  2. 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

<!--- /m2/views/contacts/02/contacts.cfm --->
<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.

<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>

<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.

<cffunction name="init" access="public" output="false" returntype="ContactGateway" hint="constructor">

  • 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>

<cfset var qContacts = "" />
<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.

<!--- /m2/model/contacts/03/ContactGateway.cfc --->
<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.

<cffunction name="init" access="public" output="false" returntype="ContactGateway" hint="constructor">
or
<cffunction name="init" access="public" output="false" returntype="model.contacts.ContactGateway" hint="constructor">

In order to pass existing variables into a component, you can inject those values into the object using the variables scope.

<cfargument name="DSN" type="string" required="true" hint="datasource" />
<cfset variables.DSN = arguments.DSN />

Finally, return the object.

<cfreturn this />

getAllContacts()

This runs the query "qContacts" and returns the recordset. There are three things to note about this function:

  1. uses the returntype "query"
  2. the name of the query is var scoped
  3. the cfquery uses the datasource "#variables.DSN#", which was defined by the init() method

Object Instantiation

To create an instance of the Gateway object:

<cfset contactGateway = createObject("component", "model.contacts.ContactGateway").init( session.DSN ) />

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

<cfset qContacts = createObject("component", "model.contacts.ContactGatway").init( request.DSN ) />

<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).


Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Kin Wong's Gravatar Thanks for your very informative blog entries. I can understand "injecting" the dsn into the variables scope via the init function, but what about arguments that you want to change for every call? For example, I want to do something like this:
<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.
# Posted By Kin Wong | 3/22/07 5:06 AM
Eric Knipp's Gravatar Kin

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
# Posted By Eric Knipp | 3/30/07 7:40 AM
Adrian J. Moreno's Gravatar Sorry I hadn't had a chance to get back to this.

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.
# Posted By Adrian J. Moreno | 3/30/07 7:12 PM
Kin Wong's Gravatar I was following the ContactManager example from Matt Woodward's site in which he instantiates the ContactGateway inside the configure method of the ContactListener:

<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
# Posted By Kin Wong | 4/2/07 1:59 PM
Adrian J. Moreno's Gravatar Kin, if you read Step 5, you'll see that the configure() method has the same function as an init() method, to initialize the component, but it is specific to a Mach-II Listener CFC.

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.
# Posted By Adrian J. Moreno | 4/2/07 9:11 PM