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
view plain print about
1<event-handler event="showCompanies" access="public">
2
3    <event-arg name="pageTitle" value="Company List" />
4
5    <notify listener="CompanyListener" method="getCompanyList" resultArg="qCompanies" />
6
7    <view-page name="header" />
8    <view-page name="lhsMenu" contentArg="sidebar" />
9    <view-page name="companyList" contentArg="mainContent" />
10    <view-page name="template" />
11    <view-page name="footer" />
12
13</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
view plain print about
1<cfcomponent name="CompanyListener" displayname="CompanyListener" output="false" extends="MachII.framework.Listener" hint="CompanyListener for CF Contact Manager Demo">
2
3    <cffunction name="configure" access="public" output="false" returntype="void" hint="Configures this listener as part of the Mach-II framework">
4        <!--- do nothing for now --->
5    </cffunction>
6
7    <cffunction name="getCompanyList" access="public" output="false" returntype="query" hint="returns a query recordset from the CompanyGateway">
8        <cfset var companyGateway = createObject("component", "CompanyGateway").init( DSN = request.DSN ) />
9        <cfreturn companyGateway.getAllCompanies() />
10    </cffunction>
11
12</cfcomponent>

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

model/companies/03/CompanyGateway.cfc
view plain print about
1<cfcomponent name="CompanyGateway" output="false" hint="Defines Gateway functions for Companies">
2
3    <cffunction name="init" access="public" output="false" returntype="CompanyGateway" hint="constructor">
4        <cfargument name="DSN" type="string" required="true" hint="datasource" />
5        <cfset variables.DSN = arguments.DSN />
6        <cfreturn this />
7    </cffunction>
8
9    <cffunction name="getAllCompanies" access="public" output="false" returntype="query" hint="returns a query recordset of companies">
10        <cfset var qCompanies = "" />
11        <cfquery name="qCompanies" datasource="#variables.DSN#">
12            SELECT
13                COMPANY_ID,
14                COMPANY_NAME,
15                COMPANY_ADDRESS_ONE,
16                COMPANY_ADDRESS_TWO,
17                COMPANY_CITY,
18                COMPANY_STATE,
19                COMPANY_ZIP,
20                COMPANY_PHONE_MAIN
21            FROM
22                CF_COMPANIES
23            ORDER BY
24                COMPANY_NAME
25        </cfquery>
26        <cfreturn qCompanies />
27    </cffunction>
28
29</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:

view plain print about
1<a href="company_detail.cfm?COMPANY_ID=2">Narf Ltd.</a>

Mach-II link:

view plain print about
1<a href="index.cfm?event=showCompanyDetail&COMPANY_ID=2">Narf Ltd.</a>

Reading a record: showCompanyDetail

New event-handler

config/mach-ii.04.xml
view plain print about
1<event-handler event="showCompanyDetail" access="public">
2
3    <event-arg name="pageTitle" value="Company Detail" />
4    <!--- [1] --->
5    <notify listener="CompanyListener" method="getCompanyDetail" resultArg="companyBean" />
6
7    <view-page name="header" />
8    <view-page name="lhsMenu" contentArg="sidebar" />
9    <!--- [2] --->
10    <view-page name="companyDetail" contentArg="mainContent" />
11    <view-page name="template" />
12    <view-page name="footer" />
13
14</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()
view plain print about
1<cffunction name="getCompanyDetail" access="public" output="false" returntype="Company" hint="returns a populated Company Bean.">
2
3    <cfargument name="event" type="MachII.framework.Event" required="true" />
4
5    <cfset var company = createObject("component", "Company").init( CompanyID = arguments.event.getArg("COMPANY_ID") ) />
6    <cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />
7
8    <cfset companyDAO.read(company) />
9
10    <cfreturn company />
11
12</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.

view plain print about
1<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
view plain print about
1<cfcomponent name="Company" hint="This is a Company Bean">
2
3    <cfset variables.instance.CompanyID = 0 />
4    <cfset variables.instance.Name = "" />
5    <cfset variables.instance.AddressOne = "" />
6    <cfset variables.instance.AddressTwo = "" />
7    <cfset variables.instance.City = "" />
8    <cfset variables.instance.State = "" />
9    <cfset variables.instance.Zip = "" />
10    <cfset variables.instance.PhoneMain = "" />
11
12    <cffunction name="init" displayname="init" hint="Bean for CF_COMPANIES" access="public" output="false" returntype="Company">
13
14        <cfargument name="CompanyID" type="numeric" required="false" default="0" hint="COMPANY_ID" />
15        <cfargument name="Name" type="string" required="false" default="" hint="COMPANY_NAME" />
16        <cfargument name="AddressOne" type="string" required="false" default="" hint="COMPANY_ADDRESS_ONE" />
17        <cfargument name="AddressTwo" type="string" required="false" default="" hint="COMPANY_ADDRESS_TWO" />
18        <cfargument name="City" type="string" required="false" default="" hint="COMPANY_CITY" />
19        <cfargument name="State" type="string" required="false" default="" hint="COMPANY_STATE" />
20        <cfargument name="Zip" type="string" required="false" default="" hint="COMPANY_ZIP" />
21        <cfargument name="PhoneMain" type="string" required="false" default="" hint="COMPANY_PHONE_MAIN" />
22
23        <cfset setCompanyID(arguments.CompanyID) />
24        <cfset setName(arguments.Name) />
25        <cfset setAddressOne(arguments.AddressOne) />
26        <cfset setAddressTwo(arguments.AddressTwo) />
27        <cfset setCity(arguments.City) />
28        <cfset setState(arguments.State) />
29        <cfset setZip(arguments.Zip) />
30        <cfset setPhoneMain(arguments.PhoneMain) />
31
32        <cfreturn this />
33
34    </cffunction>
35
36    <cffunction name="getCompanyID" access="public" hint="Getter for CompanyID" output="false" returnType="numeric">
37        <cfreturn variables.instance.CompanyID />
38    </cffunction>
39    <cffunction name="setCompanyID" access="private" hint="Setter for CompanyID" output="false" returnType="void">
40        <cfargument name="CompanyID" hint="COMPANY_ID" required="yes" type="numeric" />
41        <cfset variables.instance.CompanyID = arguments.CompanyID />
42    </cffunction>
43
44    <cffunction name="getName" access="public" hint="Getter for Name" output="false" returnType="string">
45        <cfreturn variables.instance.Name />
46    </cffunction>
47    <cffunction name="setName" access="private" hint="Setter for Name" output="false" returnType="void">
48        <cfargument name="Name" hint="COMPANY_NAME" required="yes" type="string" />
49        <cfset variables.instance.Name = arguments.Name />
50    </cffunction>
51
52    <cffunction name="getAddressOne" access="public" hint="Getter for AddressOne" output="false" returnType="string">
53        <cfreturn variables.instance.AddressOne />
54    </cffunction>
55    <cffunction name="setAddressOne" access="private" hint="Setter for AddressOne" output="false" returnType="void">
56        <cfargument name="AddressOne" hint="COMPANY_ADDRESS_ONE" required="yes" type="string" />
57        <cfset variables.instance.AddressOne = arguments.AddressOne />
58    </cffunction>
59
60    <cffunction name="getAddressTwo" access="public" hint="Getter for AddressTwo" output="false" returnType="string">
61        <cfreturn variables.instance.AddressTwo />
62    </cffunction>
63    <cffunction name="setAddressTwo" access="private" hint="Setter for AddressTwo" output="false" returnType="void">
64        <cfargument name="AddressTwo" hint="COMPANY_ADDRESS_TWO" required="yes" type="string" />
65        <cfset variables.instance.AddressTwo = arguments.AddressTwo />
66    </cffunction>
67
68    <cffunction name="getCity" access="public" hint="Getter for City" output="false" returnType="string">
69        <cfreturn variables.instance.City />
70    </cffunction>
71    <cffunction name="setCity" access="private" hint="Setter for City" output="false" returnType="void">
72        <cfargument name="City" hint="COMPANY_CITY" required="yes" type="string" />
73        <cfset variables.instance.City = arguments.City />
74    </cffunction>
75
76    <cffunction name="getState" access="public" hint="Getter for State" output="false" returnType="string">
77        <cfreturn variables.instance.State />
78    </cffunction>
79    <cffunction name="setState" access="private" hint="Setter for State" output="false" returnType="void">
80        <cfargument name="State" hint="COMPANY_STATE" required="yes" type="string" />
81        <cfset variables.instance.State = arguments.State />
82    </cffunction>
83
84    <cffunction name="getZip" access="public" hint="Getter for Zip" output="false" returnType="string">
85        <cfreturn variables.instance.Zip />
86    </cffunction>
87    <cffunction name="setZip" access="private" hint="Setter for Zip" output="false" returnType="void">
88        <cfargument name="Zip" hint="COMPANY_ZIP" required="yes" type="string" />
89        <cfset variables.instance.Zip = arguments.Zip />
90    </cffunction>
91
92    <cffunction name="getPhoneMain" access="public" hint="Getter for PhoneMain" output="false" returnType="string">
93        <cfreturn variables.instance.PhoneMain />
94    </cffunction>
95    <cffunction name="setPhoneMain" access="private" hint="Setter for PhoneMain" output="false" returnType="void">
96        <cfargument name="PhoneMain" hint="COMPANY_PHONE_MAIN" required="yes" type="string" />
97        <cfset variables.instance.PhoneMain = arguments.PhoneMain />
98    </cffunction>
99
100</cfcomponent>

2. The Data Access Object

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

view plain print about
1<cfset var companyDAO = createObject("component", "CompanyDAO").init( DSN = request.DSN ) />

model/companies/04/CompanyDAO.cfc
view plain print about
1<cfcomponent name="CompanyDAO" output="false" hint="Data Access Object for Companies">
2
3    <cffunction name="init" access="public" output="false" returntype="CompanyDAO" hint="constructor">
4        <cfargument name="DSN" type="string" required="true" hint="datasource" />
5        <cfset variables.DSN = arguments.DSN />
6        <cfreturn this />
7    </cffunction>
8
9</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.

view plain print about
1<cfset companyDAO.read(company) />

model/companies/04/CompanyDAO.cfc: read()
view plain print about
1<cffunction name="read" access="public" output="false" returntype="boolean" hint="returns a populated Company bean.">
2
3    <cfargument name="Company" type="Company" required="true" hint="The arguments accepts a Company bean." />
4    <cfset var qCompanies = "" />
5
6    <!--- If COMPANY_ID is blank or Zero, bypass query and return the empty bean--->
7    <cfif val( arguments.Company.getCompanyID() ) gt 0>
8
9        <!--- run the query based on the ID loaded from the Listener function --->
10        <cfquery name="qCompanies" datasource="#variables.DSN#">
11            SELECT
12                COMPANY_ID,
13                COMPANY_NAME,
14                COMPANY_ADDRESS_ONE,
15                COMPANY_ADDRESS_TWO,
16                COMPANY_CITY,
17                COMPANY_STATE,
18                COMPANY_ZIP,
19                COMPANY_PHONE_MAIN
20            FROM
21                CF_COMPANIES
22            WHERE
23                COMPANY_ID = <cfqueryparam value="#arguments.Company.getCompanyID()#" cfsqltype="cf_sql_numeric" />
24        </cfquery>
25
26        <!--- re-initialize the bean with data from the query --->
27        <cfset arguments.Company.init( CompanyID = qCompanies.COMPANY_ID,
28                                        Name = qCompanies.COMPANY_NAME,
29                                        AddressOne = qCompanies.COMPANY_ADDRESS_ONE,
30                                        AddressTwo = qCompanies.COMPANY_ADDRESS_TWO,
31                                        City = qCompanies.COMPANY_CITY,
32                                        State = qCompanies.COMPANY_STATE,
33                                        Zip = qCompanies.COMPANY_ZIP,
34                                        PhoneMain = qCompanies.COMPANY_PHONE_MAIN ) /
>

35
36        <!--- return true means that the bean was populated --->
37        <cfreturn true />
38
39    </cfif>
40
41    <!--- return false means that the bean was not populated --->
42    <cfreturn false />
43
44</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.

view plain print about
1<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.

view plain print about
1<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 plain print about
1<view-page name="companyDetail" contentArg="mainContent" />

is defined as

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

/views/companies/04/company_detail.cfm
view plain print about
1<cfset company = event.getArg("companyBean") />
2
3<cfoutput>
4<p><strong>#company.getName()#</strong></p>
5
6<blockquote>
7    #company.getAddressOne()#<br />
8
9    <cfif company.getAddressTwo() neq "">
10        #company.getAddressTwo()#<br />
11    </cfif>
12
13    #company.getCity()#, #company.getState()# #company.getZip()#<br />
14
15    Phone: #company.getPhoneMain()#
16</blockquote>
17</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.
    view plain print about
    1http://localhost/mach-ii-primer/m2/index.cfm?event=showCompanies

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