Photo from Chile

How I Use Transfer - Part VII - A Concrete Gateway Object

In the previous post, I described my Abstract Gateway Object. As I did with the Service Objects, I'm going to take a moment to describe a Concrete Gateway Object as an illustration of how I use the Abstract Gateway. I'll start with a recap of the differences between the Abstract Gateway and Concrete Gateways, followed by a look at the code of a specific Concrete Gateway.

    The Abstract Gateway Object
  • Is never instantiated as an object.
  • Cannot be used as is.
  • Is only ever used as a base object for Concrete Gateway Objects.
  • There is only one Abstract Gateway Object, called AbstractGateway.cfc.
  • Does not have any Transfer classes associated with it.
    Concrete Gateway Objects
  • Are instantiated as objects.
  • Methods on them are called by Service Objects.
  • All extend AbstractGateway.cfc.
  • There are many Concrete Gateway Objects, e.g., UserGateway.cfc, ProductGateway.cfc, ReviewGateway.cfc, etc.
  • Have one "main" Transfer class associated with them, but can interact with others via code specific to the Concrete Gateway.

One thing to note here is that my Gateway Objects are all injected into Service Objects via Coldspring, and are only called by Service Objects. So the Service acts as an API to the entire model. If a Business Object needs to call a method on a gateway, it calls it via a Service Object that is injected into the Business Object.

Let's take a look at an example of a Concrete Gateway Object, ProductGateway.cfc. To start, here's the Bean definition of this gateway from my Coldspring config file:

view plain print about
1<bean id="ProductGateway" class="model.Gateway.ProductGateway">
2    <property name="TransferClassName"><value>product.product</value></property>
3    <property name="DescColumn"><value>ProductCode</value></property>

In here I indicate that the main Transfer class with which this service interacts is product.product. That means that calls to GetList(), GetActiveList() and ReInitActiveList() will be directed at the table defined to Transfer as product.product. I can write additional methods in my gateway that will interact with other Transfer classes, but the default methods, inherited from the AbstractGateway, will be directed at product.product.

I also indicate that the property that represents the description of the main class is ProductCode. That is used as a default sort sequence for the default methods.

And here's what's inside ProductGateway.cfc:

view plain print about
1<cffunction name="GetList" access="public" output="false" returntype="any" hint="Interface to the getByAttributesQuery method">
2    <cfargument name="Level1CategoryId" type="any" required="false" />
3    <cfargument name="Level2CategoryId" type="any" required="false" />
4    <cfargument name="Level3CategoryId" type="any" required="false" />
5    <cfargument name="ProductName" type="any" required="false" />
6    <cfargument name="SKU" type="any" required="false" />
7    <cfargument name="ProductCode" type="any" required="false" />
9    <cfset var TQL = "" />
10    <cfset var TQuery = "" />    
11    <cfsavecontent variable="TQL">
12    <cfoutput>
13        SELECT    Product.ProductId, Product.ProductCode, Product.SKU, Product.ActiveFlag,
14                Product.ProductName, Product.Level1CategoryName, Product.Level2CategoryName,
15                Product.Level3CategoryName
16        FROM    product.catalog_admin AS Product
17        WHERE    Product.ProductId IS NOT NULL
18    <cfif structKeyExists(arguments,"Level1CategoryId") and Val(arguments.Level1CategoryId)>
19        AND        Product.Level1CategoryId = :Level1CategoryId
20    </cfif>
21    <cfif structKeyExists(arguments,"Level2CategoryId") and Val(arguments.Level2CategoryId)>
22        AND        Product.Level2CategoryId = :Level2CategoryId
23    </cfif>
24    <cfif structKeyExists(arguments,"Level3CategoryId") and Val(arguments.Level3CategoryId)>
25        AND        Product.Level3CategoryId = :Level3CategoryId
26    </cfif>
27    <cfif structKeyExists(arguments,"ProductName") and Len(arguments.ProductName)>
28        AND        Product.ProductName like :ProductName
29    </cfif>
30    <cfif structKeyExists(arguments,"SKU") and Len(arguments.SKU)>
31        AND        Product.SKU like :SKU
32    </cfif>
33    <cfif structKeyExists(arguments,"ProductCode") and Len(arguments.ProductCode)>
34        AND        Product.ProductCode like :ProductCode
35    </cfif>
36        ORDER BY Product.ActiveFlag DESC, Product.ProductName
37    </cfoutput>
38    </cfsavecontent>
39    <cfset TQuery = getTransfer().createQuery(TQL) />
40    <cfif structKeyExists(arguments,"Level1CategoryId") and Val(arguments.Level1CategoryId)>
41        <cfset TQuery.setParam("Level1CategoryId",arguments.Level1CategoryId) />
42    </cfif>
43    <cfif structKeyExists(arguments,"Level2CategoryId") and Val(arguments.Level2CategoryId)>
44        <cfset TQuery.setParam("Level2CategoryId",arguments.Level2CategoryId) />
45    </cfif>
46    <cfif structKeyExists(arguments,"Level3CategoryId") and Val(arguments.Level3CategoryId)>
47        <cfset TQuery.setParam("Level3CategoryId",arguments.Level3CategoryId) />
48    </cfif>
49    <cfif structKeyExists(arguments,"ProductName") and Len(arguments.ProductName)>
50        <cfset TQuery.setParam("ProductName","%" & arguments.ProductName & "%") />
51    </cfif>
52    <cfif structKeyExists(arguments,"SKU") and Len(arguments.SKU)>
53        <cfset TQuery.setParam("SKU","%" & arguments.SKU & "%") />
54    </cfif>
55    <cfif structKeyExists(arguments,"ProductCode") and Len(arguments.ProductCode)>
56        <cfset TQuery.setParam("ProductCode","%" & arguments.ProductCode & "%") />
57    </cfif>
59    <cfset TQuery.setDistinctMode(true) />
60    <cfreturn getTransfer().listByQuery(TQuery) />

Here I'm overriding the default GetList() method to allow for criteria to be passed in by a user. This function is based on one automatically generated for me by Brian Rinaldi's most excellent Illudium PU-36 Code Generator. One thing to note in here is the Transfer Class that this TQL is referring to. It's called product.catalog_admin, and is actually pointing at a view in my database. The product information in this application is spread across many tables, and rather than having to write a complicated TQL statement with multiple inner and outer joins, I just write my TQL against a view that already joins everything together.

Let's look at another method on the Concrete Gateway:

view plain print about
1<cffunction name="getApprovedReviews" access="public" output="false" returntype="any" hint="Returns a query of Approved Reviews for a given Product">
2    <cfargument name="ProductId" type="any" required="true" />
3    <cfset var TQuery = 0 />
4    <cfset var TQL = "" />
5    <cfsavecontent variable="TQL">
6        <cfoutput>
7            SELECT    Review.ReviewId, TUser.Nickname, Review.Rating, Review.Comments, Review.LastUpdateTimestamp
8            FROM    product.product AS Product
9            JOIN AS Review
10            JOIN    product.reviewstatus AS ReviewStatus
11            JOIN    user.user AS TUser
12            WHERE    Product.ProductId = :ProductId
13            AND        ReviewStatus.ReviewStatusDesc = :ReviewStatus
14            ORDER BY Review.LastUpdateTimestamp
15        </cfoutput>
16    </cfsavecontent>
17    <cfset TQuery = getTransfer().createQuery(TQL) />
18    <cfset TQuery.setParam("ProductId",Val(arguments.ProductId)) />
19    <cfset TQuery.setParam("ReviewStatus","Approved") />
20    <cfreturn getTransfer().listByQuery(TQuery) />

This is basically just another query that I need within the application. My Concrete Gateways are quite simple in that they only include methods that return query objects.

As I alluded to in a previous post, I have one "special" Concrete Gateway Object, called ValueListGateway, which I use to interact with all of my "code" or "lookup" objects. It is used for objects like UserGroup, OrderStatus, ProductCategory, Colour, etc. It too extends AbstractGateway, but is built itself in an abstract way so that I can use it to interact with all of those "code" objects, without having to write a Concrete Gateway Object for each one. I plan on discussing that in a future post.

In the next installment I'm going to start looking at my AbstractTransferDecorator, which can be thought of as an Abstract Business Object.

This is very cool, thanks for posting. All I need to know know is how to replicate the group attribute of cfquery with Transfer!
# Posted By John Whish | 7/21/08 2:51 AM
Thanks for the feedback, John. Do you mean the group attribute of <cfoutput>? Transfer has no bearing on that. If you mean using the GROUP BY keyword of SQL from within TQL, then unfortunately Transfer doesn't support that...yet.
# Posted By Bob Silverberg | 7/21/08 5:32 AM
@Bob, oops - I didn't make myself very clear did I :/

I think that not being able to do <cfoutput query="q" group="mycolumn"></cfoutput> is a problem with the ORM approach. I don't need to do it very often, but when I do it is so useful. So much easier than having to check for an id on each loop through the iterator and then executing conditional code.

I didn't know that you couldn't use a SQL GROUP BY clause in TQL. How would you return a query that returned a Count() as well as column values in the select statement? Which is something I do a lot when building reports.

I'm quite new to transfer, but interested! Sorry about the n00b questions.
# Posted By John Whish | 7/21/08 5:50 AM
Oh, I see what you mean. You mean when dealing with a composition. Yeah, you don't have much choice other than a manual loop in that case. If I need to do that I'll often include a method in my decorator that returns a query and then use that for output, rather than an Iterator or Collection. To be more precise, the method in the decorator calls a method in a gateway, which is injected into the decorator. You can use one of Transfer's methods that returns queries for that, or just write your own SQL in a <cfquery>. Does that make sense?

Regarding Count(), TQL does not currently support any aggregate functions, so you'd have to write all of that stuff in SQL. I believe this may be an enhancement down the road. For the record, there's nothing wrong with including a <cfquery> in a decorator, or better yet, a gateway. You can still pick up the datasource from Transfer so you don't need to maintain that info in more than one place.
# Posted By Bob Silverberg | 7/21/08 6:25 AM

How are you dealing with Service/Gateway layer "pagination"?

I am returning some very large recordsets (or TO arrays).. I have developed a custom pageBean + paginatorService approach with model glue in my beta apps.. but I am trying to move more to a nicly abstracted model layer and My view render times are killing me. surly you have though about pagination in your service/gateway objects, yet i havn't seend any thoughts in this series yet.... I would really appreciate any input on the subject.. if anybody had any....
# Posted By adam | 7/22/08 5:29 PM
Honestly Adam, I developed all of this "OO" stuff on an as needed basis for a recent large project. I haven't needed a solution for pagination yet, so I haven't tried to crack that nut. If I ever do need to do that, and come up with a solution that I'm happy with, I'll be sure to blog about it.
# Posted By Bob Silverberg | 7/22/08 6:10 PM