Primer Part 6: Getting the Details with Beans and DAOs - Mach-II/Mach-II-Framework GitHub Wiki

Table of Contents

  1. Object Overview
  2. The Devil is in the Details
  3. Other Chapters

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.

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

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

  1. While we're using the same Listener, we're calling a different method and returning a different event argument.
  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>

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>

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>

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.

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. http://localhost/mach-ii-primer/m2/index.cfm?event=showCompanies

Other Chapters

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

Special thanks to Adrian J. Moreno of IKnowKungFoo for contributing this series of primers.

⚠️ **GitHub.com Fallback** ⚠️