By Peter Bell

Interfaces: Starting with the Service

So, what methods does a base service need to support? Lets start with taking care of CRUD (Create Read Update and Delete).

Getting Data I want my service layer to be able to return an iterating business object loaded with 0..n records depending on my requirements. I’ve spent a LOT of time thinking about this and I like the idea of getbyUniqueAttribute () and getbyFilter() as being the two main methods for requesting a loaded IBO. I may add An additional getReport() method to handle reporting where there needs to be a much richer set of grouping and aggregation, and (if possible) a getStoredProcedure() to handle n-parameter calls to stored procedures (might require a little bit of runtime generation), but these two methods have a lot of flexibility for most use cases.

With getbyUniqueAttribute(AttributeName required,AttributeValueList required,AttributeNameList optional), all we need to provide is the Attribute Name and the comma delimited list of 0..n Attribute Values. By definition, the filter is the objects which match the attribute values passed in and the order is the order in which the attribute values were provided (I get that there could be a use case for getting a list of products based on an ID list and then ordering by price but I’d handle that edge case using my IBO order method which I’ll describe later tonight). I also allow for an attribute name list where you want to load only a subset of the properties for a given object.

With getbyFilter(Filter optional, Order optional, AttributeNameList optional, Page optional, RecordsPerPage, optional), you can specify the filter, order, attribute name list, and optionally the page and number of records per page.

If anyone is wondering why there isn’t a get(), getAll() or getList() . . .

In my opinion, get() isn’t sufficiently meaningful for a service method query as I have no idea what kind of get it supports. getAll() can be implemented in this API using getByFilter() with no value for the filter clause and I don’t use getAll() very often. I may add it if this becomes an issue.

As for getList(), I’m personally (and it is mainly a personal preference) not a big fan of this naming as while it is extremely common (which makes it good because it is expected and understood). I could never justify using getList() for filters when I might well get back a list using a comma delimited list of attribute values as well. This was probably driven by the fact that I do a lot of admin systems where you can pick 7 products to edit or 4 users to approve so I often have a comma delimited list of email addresses, user ID’s, product skus or product ID’s that I have to return using an IN clause. I always saw that as closer in nature to a getbyAttribute() than a getByFilter(), and if my getbyAttribute() could return a list, I couldn’t very well call my getbyFilter() “getList()”!

Just to further clarify, the reason I don’t have a separate getbyAttribute() and getbyAttributeList() (where the first expects a single value and the second supports 1..n values) is that if someone does select one of n options on an admin screen I want to encapsulate the process of determining whether to use an EQUALS or an IN clause. I don’t want to have to keep writing “if ListLen(AtrributeValueList) GT 1 call x else call y” all over the place.

The fact that my API is non-standard is a distinct weakness, but I feel the benefits derived from the clarify of the naming are worth the trouble of having to explain them (at least to me :->).

Of course, we haven’t talked about ORM gets. That is because I’m going to add some very simple relationship methods in tomorrow.

Save I like the idea of a single save() method handling both inserts and updates. Just pass in the appropriate iterating business object and the save method runs any transformations and validations before saving the data if it is valid. This needs to return error information and/or status, but that is a whole other post!

Delete I see deletes as extremely analogous to gets. You could want to delete product 17, users 3, 4 and 6 or all shopping baskets more than 2 weeks old. So I use deletebyUniqueAttribute(AttributeName required,AttributeValueList required) and deletebyFilter(Filter).

Comments
Pete,

I am working on a an application right now that uses an XML database. Since i have to give the XML data files an API I have two methods: DeleteRecordByKey() and DeleteRecordsByKeyList()... the second uses the IN SQL clause. The first merely calles the second passing in the KEY as the list. This stops me from having to check wether or not my list length is greater than one.

I know that this is a delete case, not a get case (which group similarly) and I was just wondering if you thought this is an OK way to go?

Also, excellent posting. I am on the edge of my seat waiting for more :)
# Posted By Ben Nadel | 10/1/06 7:56 PM
Hi Ben,

I'm not sure I understand 100%. Is your second method public or just private? The way I'd solve this would be to just have the first method as public and have it internally use an "if listlen(KEY) GT 1 call DeleteRecordsbyHeyList(KEY) ELSE <go get the 1 record>". I think that is exactly what your saying in which case you don't really need to make the second method as part of your API at all. You'd just define the first method as your API with a property called KEYLIST with a hint of "I am the comma delimited list of one or more keys to delete". You would then make the DeleteRecordsByKeyList() a private method so it could only be called by the DeleteRecordByKey() method, simplifying your API (unless there was a compelling reason to make them both part of the published API).

As I say, I THINK that's what you're doing already but am not 100% sure. Also, I'm pretty pedantic. Truth is it wouldn't do any harm to have both methods in the API, I just like the idea of only having one as my API and letting it figure out which method to do the work based on the listlen(KEY).

Plenty more coming - including code!!!
.
# Posted By Peter Bell | 10/1/06 8:11 PM
Pete,

Both methods are public. The first one ONLY takes the arguments and then calls the second on:

<cffunction>
<cfargument name="Name" />
<cfargument name="Key" />

<cfset DeleteRecordsByKeyList(
Name = ARGUMENTS.Name,
KeyList = ARGUMENTS.Key
) />
</cffunction>

While I am still buildling the app out, I can envision a use for both. Since the database is XML file based and I have many in-memory ColdFusion queries, and I can't do nice SQL stuff as I would be able to do in a traditional DB. So, to over come some of this, I picture getting all values from one table and then passing a ValueList() of a particular column to the Delete method:

DeleteRecordsByKey(
Name= [table name],
KeyList = ValueList( qData.id )
)

Sorry if I can't be more specific, I am just shooting from the hip at the moment.

So basically, I can see both versions of the method being used. But if I was only going to have one, I would get rid of the first and have only the one that deletes by list as this one will have the most flexbility. But really, what would I gain by not allowing one.

... But now thinking about it, the first one (the single ID) is really only for human reasons... meaning, it just makes more sense to write from a readability stand point, but really dont' add anything to the application other than another method declaration...

Anyway, you don't need to respond this, I am just using your posts as a way to think out loud.
# Posted By Ben Nadel | 10/1/06 8:28 PM
Sounds like you're doing what I do - using my posts to think things through - works for us both!!!

You are right. If you only had one method public it would be the list one and you don't NEED to make the first method public, but it doesn't do any harm and it does seem strange to call a "list" method in the cases when you know for sure you're only passing a single ID, so I'd probably do the same as you for that use case!
# Posted By Peter Bell | 10/1/06 8:44 PM
Peter,

Do you think the service layer should have a standard API for certain operations or a more flexible approach. I say this, because potentially all services (business objects) can have different methods you would want to encapsulate. So a standard, does not quite apply in all cases. However, the DAO's are another case, where they should follow a standard. Do you agree?

I ask this, because I have in some cases found that my service layer objects are more in the likes of a session facade (java) + business delegates that permit me to have a certain degree of transaction demarcation and control and make the DAO calls. Although in some cases, I feel retracted or just confused when a simple getbyID() method in my service layer basically just does the call to the DAO and returns. There is no modifications to the query or IBO or no extra validation. I get perplexed, because I feel I am just redirecting access. That is when you realize you are delegating the database access and that is exactly what it should do. The flexibility is there and the extensibility is there. Am I heading in the right direction??

Like in a Materials Service Object.

function getByID( arguments.id ){
var qResults = getDAO("materials").getByID(arguments.id);
return qResults;
}

ps: getDAO() is an inherited method from a baseService.cfc which basically creates DAO objects for you.
# Posted By Luis Majano | 1/9/07 9:13 PM
Hi Luis,

because I am in the business of application generation (as opposed to hand crafting optimized project specific code), I am a big believer in standard, flexible APIs for ALL of my cfcs (DAOs, Service methods, IBOs, etc.). The benefit is that if you use a flexible enough set of APIs, you can solve MOST problems declaratively, making it easy to generate the code.

IMO, there is nothing wrong with service class just calling a DAO and not "adding" anything to it. That is just the idea of a delegate that calls a composed object to do the work for it and is perfectly OK (even though it seems a little strange at first). Definitely the right direction!

Only thing I'd consider is using ColdSpring (or LightWire when it is ready for prime time) to replace your getDAO() call. You could use constructor or setter injection and then would be able to just call the MaterialsDAO directly within the MaterialsService.cfc
# Posted By Peter Bell | 1/10/07 6:28 AM
Thanks Peter,

This is reassuring.

I use the getDAO() method as a DAO factory in the base Service. I did it mainly because I did not want to place another object composition into the service. Since the service lives in the application scope. Actually there is one main service called modelService, which has my other services as object compositions. I guess, I am very tedious when placing a lot of cfc's in memory. I guess because I was used to working on massive traffic sites. Where placing cfc's in memory was to be done very carefully. Should I consider just creating an instance of the specific DAO on the init() of the service or construct it via the factory?

Ohh, by the way, I am also not a big believer of gateways. :)
# Posted By Luis Majano | 1/10/07 12:30 PM
Hi Luis,

Only thing I would consider is using ColdSpring (or if you're worried about all of the extra objects then LightWire which is a single 300 line cfc) to act as your factory - directly injecting your dependencies. I don't know if you've played with ColdSpring or Dependency Injection, but once you have you won't want to go back to writing your own factories!
# Posted By Peter Bell | 1/10/07 12:51 PM
Glad to hear you also don't love gateways! They have a place, but are a little overused . . .
# Posted By Peter Bell | 1/10/07 12:51 PM
Thanks Peter,

If you have a query like so:

<cffunction name="get" access="public" output="false" returntype="query">
      <!--- ***************************************************************** --->
      <cfargument name="roots_only"       type="boolean" default="true" required="false">
      <cfargument name="parent_id"       type="numeric" default="0" required="false">
      <cfargument name="product_id"       type="numeric" default="0" required="false">
      <!--- ***************************************************************** --->
      <cfset var qProducts = "">
      <cfquery name="qProducts" datasource="#instance.dsn#">
         SELECT a.*, (select count(id) from products where parent_id = a.id ) as ChildrenCount
         FROM products a
         WHERE 1 = 1
         <cfif arguments.roots_only>
            AND parent_id is NULL
         </cfif>
         <cfif arguments.product_id neq 0>
            AND id = <cfqueryparam cfsqltype="cf_sql_numeric" value="#arguments.product_id#">
         </cfif>
         <cfif arguments.parent_id neq 0>
            AND parent_id = <cfqueryparam cfsqltype="cf_sql_numeric" value="#arguments.parent_id#">
         </cfif>
         ORDER BY NAME
      </cfquery>
      <cfreturn qProducts>
   </cffunction>

would you leave it as is, or in another format? I want to start getting into formats.
# Posted By Luis Majano | 1/10/07 4:25 PM
No quick and easy answer. The limitation is that you're going to have to either write or generate that query because of the queryparams as they can't be concatenated dynamically - it is a real limitation in ColdFusion.
# Posted By Peter Bell | 1/11/07 8:14 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.