i Know Kung Foo Consulting

Mach-II Primer : 6.1 : Getting the details with Beans and DAOs

We last discussed how to manage recordsets with Mach-II. Now it's (finally) time to discuss how to work with individual records using Beans and Data Access Objects (DAOs).

Object Overview

The Bean Object

A bean encapulates related data into a single "record". The data can come from a single table row or from multiple sources.

The Data Access Object

Generally speaking, a DAO abstracts interactions with a data source. In working with a Bean Object, it handles Create, Read, Update and Delete functions.

The Listener Object

A Listener connects the Mach-II framework to the application's Model.

Required Reading

If you're not familiar with these objects, I suggest you read the following posts before continuing:

  1. Object Oriented Coldfusion : 6 : The Bean Object
  2. Object Oriented Coldfusion : 7.1 : A Basic Data Access Object (DAO)
  3. Object Oriented Coldfusion : 7.2 : Basic DAO Example
  4. Mach-II Primer : 5 : Mediating events with Listeners

The Devil is in the Details

Even the grandest project depends on the success of the smallest components. - Le Corbusier

In version 3 of mach-ii.xml, we call the event showCompanies to get a list of Companies in our database.

mach-ii.03.xml
<event-handler event="showCompanies" access="public">

   <event-arg name="pageTitle" value="Company List" />

   <notify listener="CompanyListener" method="getCompanyList" resultArg="qCompanies" />

   <view-page name="header" />
   <view-page name="lhsMenu" contentArg="sidebar" />
   <view-page name="companyList" contentArg="mainContent" />
   <view-page name="template" />
   <view-page name="footer" />

</event-handler>

You can see that we're notifying the Listener CompanyListener, calling its method getCompanyList and creating the event argument qCompanies populated by its result.

model/companies/03/CompanyListener.cfc
<cfcomponent name="CompanyListener" displayname="CompanyListener" output="false" extends="MachII.framework.Listener" hint="CompanyListener for CF Contact Manager Demo">

   <cffunction name="configure" access="public" output="false" returntype="void" hint="Configures this listener as part of the Mach-II framework">
      <!--- do nothing for now --->
   </cffunction>

   <cffunction name="getCompanyList" access="public" output="false" returntype="query" hint="returns a query recordset from the CompanyGateway">
      <cfset var companyGateway = createObject("component", "CompanyGateway").init( DSN = request.DSN ) />
      <cfreturn companyGateway.getAllCompanies() />
   </cffunction>

</cfcomponent>

The CompanyListener calls the CompanyGateway to get and return the recordset.

model/companies/03/CompanyGateway.cfc
<cfcomponent name="CompanyGateway" output="false" hint="Defines Gateway functions for Companies">

   <cffunction name="init" access="public" output="false" returntype="CompanyGateway" hint="constructor">
      <cfargument name="DSN" type="string" required="true" hint="datasource" />
      <cfset variables.DSN = arguments.DSN />
      <cfreturn this />
   </cffunction>

   <cffunction name="getAllCompanies" access="public" output="false" returntype="query" hint="returns a query recordset of companies">
      <cfset var qCompanies = "" />
      <cfquery name="qCompanies" datasource="#variables.DSN#">
         SELECT
            COMPANY_ID,
            COMPANY_NAME,
            COMPANY_ADDRESS_ONE,
            COMPANY_ADDRESS_TWO,
            COMPANY_CITY,
            COMPANY_STATE,
            COMPANY_ZIP,
            COMPANY_PHONE_MAIN
         FROM
            CF_COMPANIES
         ORDER BY
            COMPANY_NAME
      </cfquery>
      <cfreturn qCompanies />
   </cffunction>

</cfcomponent>

Finally, the page-view named companyList will render the HTML table of records.

Output: index.cfm?event=showCompanies

Company List

Add a Company

Now we need to drill down to the details of just one Company. The procedural version of this application links to company_detail.cfm for this information. We need to create an event called showCompanyDetail so that Mach-II can retrieve and render this information.

Procedural link:

<a href="company_detail.cfm?COMPANY_ID=2">Narf Ltd.</a>

Mach-II link:

<a href="index.cfm?event=showCompanyDetail&COMPANY_ID=2">Narf Ltd.</a>

Reading a record: showCompanyDetail

New event-handler

config/mach-ii.04.xml
<event-handler event="showCompanyDetail" access="public">

   <event-arg name="pageTitle" value="Company Detail" />
   <!--- [1] --->
   <notify listener="CompanyListener" method="getCompanyDetail" resultArg="companyBean" />

   <view-page name="header" />
   <view-page name="lhsMenu" contentArg="sidebar" />
   <!--- [2] --->
   <view-page name="companyDetail" contentArg="mainContent" />
   <view-page name="template" />
   <view-page name="footer" />

</event-handler>

Other than the pageTitle, there are two differences between showCompanyDetail and showCompanies.

  1. While we're using the same Listener, we're calling a different method and returning a different event arguments.
  2. We're using the page-view companyDetail to render the company detail HTML.

Updated CompanyListener.cfc

model/companies/04/CompanyListener.cfc - added getCompanyDetail()
<cffunction name="getCompanyDetail" access="public" output="false" returntype="Company" hint="returns a populated Company Bean.">

   <cfargument name="event" type="MachII.framework.Event" required="true" />

   <cfset var company = createObject("component", "Company").init( CompanyID = arguments.event.getArg("COMPANY_ID") ) />
   <cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />

   <cfset companyDAO.read(company) />

   <cfreturn company />

</cffunction>

1. Company bean

First, getCompanyDetail creates an instance of the Company bean and populates it with the COMPANY_ID that was passed in through the querystring. Remember that all URL and FORM variables are automatically converted to event arguments by Mach-II.

<cfset var company = createObject("component", "Company").init( CompanyID = arguments.event.getArg("COMPANY_ID") ) />

In the sample code, this line says .init( arguments.event.getArg("COMPANY_ID") ) and should be updated.

model/companies/04/Company.cfc
<cfcomponent name="Company" hint="This is a Company Bean">

   <cfset variables.instance.CompanyID = 0 />
   <cfset variables.instance.Name = "" />
   <cfset variables.instance.AddressOne = "" />
   <cfset variables.instance.AddressTwo = "" />
   <cfset variables.instance.City = "" />
   <cfset variables.instance.State = "" />
   <cfset variables.instance.Zip = "" />
   <cfset variables.instance.PhoneMain = "" />

   <cffunction name="init" displayname="init" hint="Bean for CF_COMPANIES" access="public" output="false" returntype="Company">

      <cfargument name="CompanyID" type="numeric" required="false" default="0" hint="COMPANY_ID" />
      <cfargument name="Name" type="string" required="false" default="" hint="COMPANY_NAME" />
      <cfargument name="AddressOne" type="string" required="false" default="" hint="COMPANY_ADDRESS_ONE" />
      <cfargument name="AddressTwo" type="string" required="false" default="" hint="COMPANY_ADDRESS_TWO" />
      <cfargument name="City" type="string" required="false" default="" hint="COMPANY_CITY" />
      <cfargument name="State" type="string" required="false" default="" hint="COMPANY_STATE" />
      <cfargument name="Zip" type="string" required="false" default="" hint="COMPANY_ZIP" />
      <cfargument name="PhoneMain" type="string" required="false" default="" hint="COMPANY_PHONE_MAIN" />

      <cfset setCompanyID(arguments.CompanyID) />
      <cfset setName(arguments.Name) />
      <cfset setAddressOne(arguments.AddressOne) />
      <cfset setAddressTwo(arguments.AddressTwo) />
      <cfset setCity(arguments.City) />
      <cfset setState(arguments.State) />
      <cfset setZip(arguments.Zip) />
      <cfset setPhoneMain(arguments.PhoneMain) />

      <cfreturn this />

   </cffunction>

   <cffunction name="getCompanyID" access="public" hint="Getter for CompanyID" output="false" returnType="numeric">
      <cfreturn variables.instance.CompanyID />
   </cffunction>
   <cffunction name="setCompanyID" access="private" hint="Setter for CompanyID" output="false" returnType="void">
      <cfargument name="CompanyID" hint="COMPANY_ID" required="yes" type="numeric" />
      <cfset variables.instance.CompanyID = arguments.CompanyID />
   </cffunction>

   <cffunction name="getName" access="public" hint="Getter for Name" output="false" returnType="string">
      <cfreturn variables.instance.Name />
   </cffunction>
   <cffunction name="setName" access="private" hint="Setter for Name" output="false" returnType="void">
      <cfargument name="Name" hint="COMPANY_NAME" required="yes" type="string" />
      <cfset variables.instance.Name = arguments.Name />
   </cffunction>

   <cffunction name="getAddressOne" access="public" hint="Getter for AddressOne" output="false" returnType="string">
      <cfreturn variables.instance.AddressOne />
   </cffunction>
   <cffunction name="setAddressOne" access="private" hint="Setter for AddressOne" output="false" returnType="void">
      <cfargument name="AddressOne" hint="COMPANY_ADDRESS_ONE" required="yes" type="string" />
      <cfset variables.instance.AddressOne = arguments.AddressOne />
   </cffunction>

   <cffunction name="getAddressTwo" access="public" hint="Getter for AddressTwo" output="false" returnType="string">
      <cfreturn variables.instance.AddressTwo />
   </cffunction>
   <cffunction name="setAddressTwo" access="private" hint="Setter for AddressTwo" output="false" returnType="void">
      <cfargument name="AddressTwo" hint="COMPANY_ADDRESS_TWO" required="yes" type="string" />
      <cfset variables.instance.AddressTwo = arguments.AddressTwo />
   </cffunction>

   <cffunction name="getCity" access="public" hint="Getter for City" output="false" returnType="string">
      <cfreturn variables.instance.City />
   </cffunction>
   <cffunction name="setCity" access="private" hint="Setter for City" output="false" returnType="void">
      <cfargument name="City" hint="COMPANY_CITY" required="yes" type="string" />
      <cfset variables.instance.City = arguments.City />
   </cffunction>

   <cffunction name="getState" access="public" hint="Getter for State" output="false" returnType="string">
      <cfreturn variables.instance.State />
   </cffunction>
   <cffunction name="setState" access="private" hint="Setter for State" output="false" returnType="void">
      <cfargument name="State" hint="COMPANY_STATE" required="yes" type="string" />
      <cfset variables.instance.State = arguments.State />
   </cffunction>

   <cffunction name="getZip" access="public" hint="Getter for Zip" output="false" returnType="string">
      <cfreturn variables.instance.Zip />
   </cffunction>
   <cffunction name="setZip" access="private" hint="Setter for Zip" output="false" returnType="void">
      <cfargument name="Zip" hint="COMPANY_ZIP" required="yes" type="string" />
      <cfset variables.instance.Zip = arguments.Zip />
   </cffunction>

   <cffunction name="getPhoneMain" access="public" hint="Getter for PhoneMain" output="false" returnType="string">
      <cfreturn variables.instance.PhoneMain />
   </cffunction>
   <cffunction name="setPhoneMain" access="private" hint="Setter for PhoneMain" output="false" returnType="void">
      <cfargument name="PhoneMain" hint="COMPANY_PHONE_MAIN" required="yes" type="string" />
      <cfset variables.instance.PhoneMain = arguments.PhoneMain />
   </cffunction>

</cfcomponent>

2. The Data Access Object

Second, getCompanyDetail creates an instance of the CompanyDAO, injecting the DSN value through the init() method.

<cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />

model/companies/04/CompanyDAO.cfc
<cfcomponent name="CompanyDAO" output="false" hint="Data Access Object for Companies">

   <cffunction name="init" access="public" output="false" returntype="CompanyDAO" hint="constructor">
      <cfargument name="DSN" type="string" required="true" hint="datasource" />
      <cfset variables.DSN = arguments.DSN />
      <cfreturn this />
   </cffunction>

</cfcomponent>

3. Then the magic happens

Third, the Company bean is passed BY REFERENCE to the read() method of CompanyDAO. Whatever happens inside the read() method affects the same instance of the Company bean that was created on the previous line. The DAO is not creating another instance of the company bean.

<cfset companyDAO.read(company) />

model/companies/04/CompanyDAO.cfc: read()
<cffunction name="read" access="public" output="false" returntype="boolean" hint="returns a populated Company bean.">

   <cfargument name="Company" type="Company" required="true" hint="The arguments accepts a Company bean." />
   <cfset var qCompanies = "" />

   <!--- If COMPANY_ID is blank or Zero, bypass query and return the empty bean--->
   <cfif val( arguments.Company.getCompanyID() ) gt 0>

      <!--- run the query based on the ID loaded from the Listener function --->
      <cfquery name="qCompanies" datasource="#variables.DSN#">
         SELECT
            COMPANY_ID,
            COMPANY_NAME,
            COMPANY_ADDRESS_ONE,
            COMPANY_ADDRESS_TWO,
            COMPANY_CITY,
            COMPANY_STATE,
            COMPANY_ZIP,
            COMPANY_PHONE_MAIN
         FROM
            CF_COMPANIES
         WHERE
            COMPANY_ID = <cfqueryparam value="#arguments.Company.getCompanyID()#" cfsqltype="cf_sql_numeric" />
      </cfquery>

      <!--- re-initialize the bean with data from the query --->
      <cfset arguments.Company.init( CompanyID = qCompanies.COMPANY_ID,
                              Name = qCompanies.COMPANY_NAME,
                              AddressOne = qCompanies.COMPANY_ADDRESS_ONE,
                              AddressTwo = qCompanies.COMPANY_ADDRESS_TWO,
                              City = qCompanies.COMPANY_CITY,
                              State = qCompanies.COMPANY_STATE,
                              Zip = qCompanies.COMPANY_ZIP,
                              PhoneMain = qCompanies.COMPANY_PHONE_MAIN ) />


      <!--- return true means that the bean was populated --->
      <cfreturn true />

   </cfif>

   <!--- return false means that the bean was not populated --->
   <cfreturn false />

</cffunction>

EDIT 2008-01-21: Sean Woods pointed out the difference between this read() and the method from the OO CF Primer part 7.2. This method has been updated to return a boolean value to indicate whether or not the bean was populated.

4. And data is returned

If read() is successful, the instance of the company bean is now populated with data from the datasource.

<cfreturn company />

This returned value is then placed into the event-arg companyBean for the life of the event as defined in the event-handler.

<notify listener="CompanyListener" method="getCompanyDetail" resultArg="companyBean" />

The View

Now that we have a populated Company bean, we can output the company's detail in the page-view companyDetail.

<view-page name="companyDetail" contentArg="mainContent" />

is defined as

<page-view name="companyDetail" page="/views/companies/04/company_detail.cfm" />

/views/companies/04/company_detail.cfm
<cfset company = event.getArg("companyBean") />

<cfoutput>
<p><strong>#company.getName()#</strong></p>

<blockquote>
   #company.getAddressOne()#<br />

   <cfif company.getAddressTwo() neq "">
      #company.getAddressTwo()#<br />
   </cfif>

   #company.getCity()#, #company.getState()# #company.getZip()#<br />

   Phone: #company.getPhoneMain()#
</blockquote>
</cfoutput>
output

Narf Ltd.

234 Anywhere St.
Fort Worth, TX 55555
Phone: 800-555-2345

A calls B calls C calls . . .

It's a lot to absorb when you're new to the game. Hopefully the benefits of creating more files than we had in the Procedural version will make better sense after a few more posts.

You can see these changes in action by using the version 4 files.

  1. Rename the version 3 mach-ii.xml file to mach-ii.03.xml
  2. Rename mach-ii.04.xml to mach-ii.xml
  3. Reload the application and click the Companies link in the left-hand navigation.

Next up, we'll go over how to Create, Update and Delete records using Beans and DAOs.


Comments (Comment Moderation is enabled. Your comment will not appear until approved.)

Copyright © 2001 - 2008 Adrian J. Moreno and i Know Kung Foo Consulting
BlogCFC was created by Raymond Camden. This blog is running version 5.9.001.