How I Use Transfer - Part VII - A Concrete Gateway Object
Posted At : July 15, 2008 1:27 PM | Posted By : Bob Silverberg
Related Categories: OO Design, How I Use Transfer, ColdFusion, Transfer
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:
2 <property name="TransferClassName"><value>product.product</value></property>
3 <property name="DescColumn"><value>ProductCode</value></property>
4</bean>
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:
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" />
8
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>
58
59 <cfset TQuery.setDistinctMode(true) />
60 <cfreturn getTransfer().listByQuery(TQuery) />
61
62</cffunction>
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:
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 product.review 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) />
21</cffunction>
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.
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.
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.
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....