Primer Part 6: Getting the Details with Beans and DAOs - Mach-II/Mach-II-Framework GitHub Wiki
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).
A bean encapulates related data into a single "record". The data can come from a single table row or from multiple sources.
Generally speaking, a DAO abstracts interactions with a data source. In working with a Bean Object, it handles Create, Read, Update and Delete functions.
A Listener connects the Mach-II framework to the application's Model.
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
- [ Edit ] Narf Ltd.
- [ Edit ] Wibble Inc.
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
.
- While we're using the same Listener, we're calling a different method and returning a different event argument.
- 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>
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>
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>
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.
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" />
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
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.
- Rename the version 3 mach-ii.xml file to mach-ii.03.xml
- Rename mach-ii.04.xml to mach-ii.xml
- Reload the application and click the Companies link in the left-hand navigation. http://localhost/mach-ii-primer/m2/index.cfm?event=showCompanies
Next we'll go over how to Create, Update and Delete records using Beans and DAOs.
- Mach-II Primer Series Navigation
- Part 1 - Files versus Events
- Part 2 - Variables versus Arguments
- Part 3 - Flip This Code
- Part 4 - Using Gateways to manage record sets
- Part 5 - Mediating events with Listeners
- Part 6 - Getting the details with Beans and DAOs
- Part 7 - Processing data with Beans and DAOs using Event-Filters
Special thanks to Adrian J. Moreno of IKnowKungFoo for contributing this series of primers.