Adding Todo Lists (Handlers and the Service Layer) - ColdBox Series Part 8d

Now we are really getting into this ColdBox stuff. We've got a view set up, we've got some handlers, we've auto-wired our handlers, and we've added ColdSpring into our application. Now we can start to do stuff.

So where did we leave off last time? Thinking.... thinking... thinking... oh yeah.

Accepting user input

Before we went off on a tangent of setting up ColdSpring and Autowiring Handlers, we had two simple handler methods inside of the list.cfc handler. We had addList() and doAddList().

addList():


    <cffunction name="addList" access="public" returntype="void" output="false">
        <cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
        
        <cfset event.setValue('xehDoit', 'list.doAddList') />
        <cfset event.setValue('btn', 'Add')>
        <cfset event.setView('listForm') />
    </cffunction>

doAddList():


    <cffunction name="doAddList" access="public" returntype="void" output="false">
        <cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
        
        
    </cffunction>

addList() just added some values to the event object and then set the view to be displayed. doAddList() accepted the event arguments, which includes the form fields that were submitted from the form inside of the view listForm.cfm and is now ready to do something with that data.

NOTE: Don't forget that in the last post, we set up this handler to be autowired with the todoService, so right now it is available to us in the variables scope.

But FIRST, let's make some changes

So just like with any project, change happens. In this case, it was due to my design. Originally, I thought we would auto-increment our primary key in the lists and items tables, but it occurs to me that I don't like that, and I don't want to have to deal with getting the key back after it is set. So we are going to modify our DB design, and we'll need to make some model changes too. Nothing major.

First, since we have not yet done anything with our database, we can just drop the tables and recreate them. Here's the script.


DROP TABLE IF EXISTS `todo`.`lists`;
CREATE TABLE `todo`.`lists` (
`listid` varchar(36) NOT NULL,
`name` varchar(50) NOT NULL,
`priority` int(11) default NULL,
PRIMARY KEY (`listid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

DROP TABLE IF EXISTS `todo`.`items`;
CREATE TABLE `todo`.`items` (
`itemid` varchar(36) NOT NULL,
`title` varchar(45) NOT NULL,
`dueDate` datetime NOT NULL,
`listid_fk` varchar(36) NOT NULL,
PRIMARY KEY (`itemid`,`listid_fk`),
KEY `fk_items_lists` (`listid_fk`),
CONSTRAINT `FK_list_items` FOREIGN KEY (`listid_fk`) REFERENCES `lists` (`listid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Next, we need to make some changes to the model objects to accommodate this change. In the todoGateway's createList() method we need to add the listID to the INSERT statement.


        <cfquery name="qCreate" datasource="#variables.dsn#">
            INSERT INTO lists (
                listid,
                name,
                priority
                ) VALUES (
                <cfqueryparam value="#arguments.list.getListId()#" cfsqltype="cf_sql_varchar" />,
                <cfqueryparam value="#arguments.list.getname()#" CFSQLType="cf_sql_varchar" />,
                <cfqueryparam value="#arguments.list.getpriority()#" CFSQLType="cf_sql_integer" null="#not len(arguments.list.getpriority())#" />
                )
        </cfquery>

We'll need to do the same for createitem():


        <cfquery name="qCreate" datasource="#variables.dsn#">
            INSERT INTO items (
                itemID,
                title,
                dueDate,
                listid_fk
                ) VALUES (
                <cfqueryparam value="#arguments.item.getItemId()#" CFSQLType="cf_sql_varchar" />,
                <cfqueryparam value="#arguments.item.gettitle()#" CFSQLType="cf_sql_varchar" />,
                <cfqueryparam value="#arguments.item.getdueDate()#" CFSQLType="cf_sql_timestamp" />,
                <cfqueryparam value="#arguments.item.getlistid_fk()#" CFSQLType="cf_sql_varchar" />
                )
        </cfquery>

Ok, one more change. In the WHERE statements for updateList(), updateItem() and createItem() inside of todoGateway, we need to change the CFSQLType attributes to cf_sql_varchar. Change it in both where clause statements in updateItem() and in the last value statement for createItem(). A total of 4 changes are needed.

Working with the service layer

We have this fancy new service layer with one method in it, and that is just for dependency injection, what else are we going to use it for? Well, what I use the service layer for is to communicate between the controller (ColdBox) and the model(Beans and Gateway). Some might argue that the service layer is part of the model. I'm not sure if I care.

We want our doListAdd() method to so something now, don't we? But what? Well we want it to take the user input, validate it, and persist it to the database. We're going to do all but the validation today. Let's start with this.


    <cffunction name="doAddList" access="public" returntype="void" output="false">
        <cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
        
        <cfset var rc = event.getCollection()>
        <cfset var listBean = variables.todoService.newList() />
        
        <cfset getPlugin('beanFactory').populateBean(listBean) />
    
        <cfset variables.todoService.saveList(listBean) />
        <cfset setNextEvent('list.allLists') />
        
    </cffunction>

There is actually quite a lot going on in these 5 new lines of code. Let's examine each.

1. Here we are putting the request collection into a "virtual scope" (a struct) called rc. This rc scope contains all of the FORM and URL scoped variables and the name of the event that was called. This way we can get the data out of the event without needing the very verbose getValue() method. We will not actually be using this struct today.

2. Here we are calling the todoService (the one that was autowired in) method newList(). I supposed we'll need to create that method now ;). This method will create a new bean, persist it to the database as an empty record with only and id and return the bean with the id in it.

3. This line is pretty cool. ColdBox has a beanFactory plugin that does several things. One of those things is to populate a bean from the event object data. It will look at the object's properties and call setter methods for each of the properties on the bean that is passed in. So if there is a firstname property, it will call setFirstname() on the bean and pass in the value from the event object. So as long as our formfields were named the same as the bean properties, we can use this.

4. The listBean is then passed to the todoService.saveList() method to be persisted to the database.

5. The last line tells ColdBox which event to fire next. We are telling it to fire the list.allLists event so that we can view all of the lists that we have made so far. We'll need to create that too.

Our first Service Method

In our doAddList() handler we are making a call to a todoService method called newList(). Let's make that method.


    <cffunction name="newList" access="public" returntype="model.todos.List" output="false">
        <cfset var bean = createObject('component','model.todos.List').init(createUUID()) />
        <cfset variables.todoGateway.createList(bean) />
        <cfreturn bean />
    </cffunction>

Here we are using the createObject() method to create an instance of the List object. To do this we are calling the init() method and passing in a new UUID using the createUUID() method. NOTE: In the future this would be a good place for an Object Factory that would be injected using ColdSpring, but we are trying to keep this simple.

The method is then going to take that newly created bean and pass it into the todoGateway's createList() method. NOTE: remember, the todoGateway was injected with ColdSpring. The createList method takes the bean as-is and persists it to the database. At this point we have not yet called populateBean(), so, other than the UUID ListID, this bean is empty.

Finally, it returns the bean to the handler.

Now our handler populates the bean:


    <cfset getPlugin('beanFactory').populateBean(listBean) />

This will take the form field values and populate the bean with them.

The bean is then passed to the todoService.saveList() method. So we'll need to create that.


    <cffunction name="saveList" access="public" returntype="void" output="false">
        <cfargument name="bean" type="model.todos.List" required="true" />

        <cfset variables.todoGateway.updateList(arguments.bean) />
    </cffunction>

This is simple enough. It takes in the bean as an argument, then passes it to the todoGateway's updateList() method, which will actually save it to the database.

Finishing Up

Finally, the handler uses setNextEvent() to fire the next ColdBox event. Which is list.allLists. So let's create that event handler method.


    <cffunction name="allLists" access="public" returntype="void" output="false">
        <cfargument name="Event" required="true" type="coldbox.system.beans.requestcontext" />
        
        <cfset var qAllLists = variables.todoService.getAllLists() />
        <cfset event.setValue('qAllLists', qAllLists) />
        
        <cfset event.setView('allLists') />
    </cffunction>

Like all event handler methods, we are first taking in the event object as an argument. Then we are creating a query object by calling getAllLists() from the todoService. Next we are injecting that query into the event object using the setValue() method. I am giving it the same name, "qAllLists". Lastly, the method sets a new view to display, allLists, which is actually allLists.cfm inside of the /views directory.

We have a few things here to create. todoService does not have a getAllLists() method, and there is not a allLists.cfm file.

todoService.getAllLists():


    <cffunction name="getAllLists" access="public" returntype="query" output="false">
        <cfreturn variables.todoGateway.getAllLists() />
    </cffunction>

allLists.cfm:


    <cfset qAllLists = event.getValue('qAllLists')>
    <cfdump var="#qAllLists#">

getAllLists(), like many of our service methods, makes a call to the gateway object. The method todoGateway.getAllLists() is for requesting all of the lists. Now, there was an oversight in my design, because we never added this method to the todoGateway, so let's.


    <cffunction name="getAllLists" access="public" output="false" returntype="query">
        <cfset var qLists = "" />
        <cftry>
            <cfquery name="qLists" datasource="#variables.dsn#">
                SELECT     listid, name, priority
                FROM    lists
            </cfquery>
            
            <cfcatch type="database">
                <cfset qLists = QueryNew("id") />
            </cfcatch>
        </cftry>
        
        <cfreturn qLists />
    </cffunction>

Then our allLists.cfm view is getting the query object our of the event object and dumping the values. This will show that the lists we create are actually being persisted.

Conclusion

Well, that was fun. We got a lot done today. We handled our user input, created a bean, populated it with the user input and persisted it to the database. We also set up some methods and a view for displaying all of the lists. On top of all that we made some changes to the model layer to make things a little better. I am attaching the entire application to the post.

Next time we will look at adding data validation, I think. Who knows, maybe we will talk about astro-photography instead.

Comments
Francois Levesque's Gravatar Another nice post, Jason. I had no clue about CB's beanFactory plugin, that populateBean method is pretty neat! Looks like I'll have to look into it ;).
# Posted By Francois Levesque | 11/17/08 4:29 AM
Doug's Gravatar Jason, I think there is a problem with the itemID. Is it supposed to be varchar or and integer? The db shows it as int, but the INSERT query shows it as varchar.

Great reading and learning a lot. Head still hurts, though ;o)

Doug
# Posted By Doug | 11/18/08 6:19 PM
Jason Dean's Gravatar @Doug. Thanks! Good catch. I have fixed it. Anyone that has used that SQL script will want to rerun the part for the items table to get the corrections.
# Posted By Jason Dean | 11/18/08 6:52 PM
Jason Dean's Gravatar @Francois Thanks for the comment. Yeah, the populateBean() method is pretty cool. However, I ended up building my own populate() method, with the help of Bob Silverberg, to be a little more dynamic and to work with my error validation. One of these days I will need to blog about it. Let me know and I will email you the code. It is an abstract transfer decorator I use for all of my TrOs
# Posted By Jason Dean | 11/18/08 6:56 PM
Doug's Gravatar One more change to the INSERT item query listing. The getlistid_fk should be type cf_sql_varchar instead of cf_sql_integer.

Doug
# Posted By Doug | 11/19/08 5:47 PM
Jason Dean's Gravatar @Doug, Thanks. I have fixed the entry and updated the ZIP file attached to this entry.
# Posted By Jason Dean | 11/19/08 6:58 PM
Nicholas's Gravatar So how do you create an empty record (other than the id) if the list name column is set up to be NOT NULL?
# Posted By Nicholas | 4/24/09 3:55 PM
Jason Dean's Gravatar @NIcholas,

You wouldn't. Why would I want a todo list without a name? In my application all Lists must have a name. The fact that I enforce this in the database is really just a personally choice. There is no reason that you couldn't make the field nullable in the DB if you wanted too. Then you could make a list without a name.

I will point out though, that it might make your UI a little hard to work with if you or your users are allowed to make unnamed lists, especially if the id is a UUID. Good luck telling those apart in table :)
# Posted By Jason Dean | 4/24/09 7:35 PM
Nicholas's Gravatar @Jason

I've been following your series, trying to learn how to use ColdBox, with the only change, really, being that I'm using Oracle as a database. No biggie.

The problem I have is that when I try to add a new list, I get an error (ORA-01400: cannot insert NULL into ("COLDBOXTEST"."TODO_LISTS"."LIST_NAME")).

Looking at the code, the only thing I can figure is that we call TodoService.newList() from list.doAddList() and we don't send any arguments. We create a UUID in newList(), but I don't see how it ever gets access to the list name. I'm using ColdSpring.

Could you at least tell me how the createList() call in newList() knows the name of the list so I can backtrack and figure out what I screwed up?
# Posted By Nicholas | 4/24/09 7:59 PM
Nicholas's Gravatar @Jason

And before you ask, I know the table name is different, and I changed the form elements to reflect that. :)
# Posted By Nicholas | 4/24/09 8:00 PM
Jason Dean's Gravatar @nicholas, I just went back and reread the whole post, and you are right. There is an error in my logic there. I am persisting the bean to the database before I set a listname.

So the easiest way to fix it is to remove the NOT NULL constraint from the listname field in the database. Otherwise your next option would be to modify the newList() method to only create the bean and return it, then use the saveList() methods to determine if the bean needs to be INSERTed or UPDATEd.

Thanks for pointing out the mistake. I am not sure how I did not notice it. I am guessing that I did not have the NOT NULL constraint on the column.
# Posted By Jason Dean | 4/24/09 10:29 PM
Steve's Gravatar Jason-
I have been going through this tutorial, and It has been great. Thanks!!! I just wanted to point out that under the "But FIRST, let's make some changes" section the code is scrambled. Just wanted to give you a heads up.
# Posted By Steve | 5/16/09 4:45 PM
Steve's Gravatar Never mind, that was strange. My browser seemed to render that section funny. After I refreshed, it seems to be okay.
# Posted By Steve | 5/16/09 4:47 PM
Steve's Gravatar Hey Jason-
Sorry to bother you again, but I am getting some strange error when I try and run the event handler doAddList.

Element LIST is undefined in a Java object of type class [Ljava.lang.String;.

I went back through my code several times, and I can't seem to figure it out. Any help would be greatly appreciated.
# Posted By Steve | 5/16/09 8:25 PM
Jason Dean's Gravatar @Steve

Thanks for all of your comments. I'm glad to see that you are enjoying the posts.

I think that may be a hard one to deduce without more info. I'm sending you a chat request in gmail, or if you can use something like http://pastebin.com to link me to your code, I can see if I can reproduce it.
# Posted By Jason Dean | 5/16/09 8:51 PM
Steve's Gravatar Jason-
Thanks again for all of your help. I have been pulling my hair out all day. I guess I should have taken a walk or something to clear my head. I just needed another pair of eyes to look at it. Strange error though, for one misspelled word?
# Posted By Steve | 5/16/09 9:30 PM
Rich Kroll's Gravatar @Jason
Firstly, thanks for putting together a great series! Is there a reason why you create a new list bean in newList()? If there is any problem downstream, you could end up with empty rows of lists in your DB. You could put the UUID in as a placeholder so you would have the id handy before and after the save operation. This would allow you to keep the update as well - just skip the extra DB hit and potential for bugs.
# Posted By Rich Kroll | 6/4/09 11:01 AM
Jason Dean's Gravatar @Rich, I see what you are saying and I agree.

Just like with so much code that we write, looking back on this several months later, I see MANY things that I would do differently. Just as has been repeated so many time in this OO learning experience, there are many ways to skin the proverbial cat.

Definitely, I think that if I was rewriting this that I would not persist the list at that point, I would wait. Part of the problem was that I had never created an app that used a model built using the PU36 code generator, so I was, sometimes, confused about how to use the model in certain places. Perhaps, one of these days, i should revisit and rewrite this. Maybe.
# Posted By Jason Dean | 6/4/09 12:20 PM
Gary's Gravatar So what was the solution to Steve's problem?

I get the same error message except mine is Element TODOSERVICE ...
# Posted By Gary | 6/11/09 1:17 PM
Jason Dean's Gravatar @Gary,

As I recall, it was a typo. It was something really simple like todoGateway was spelled todoGatway or something.
# Posted By Jason Dean | 6/11/09 1:45 PM
Josh's Gravatar @Gary
It is because you have to setup the autowire=true and add a <cfproperty> tag in the pseudo-constructor area of the component. The pseudo-constructor area is in between the <cfrcomponent> tag and the first function.
# Posted By Josh | 6/18/09 1:33 AM
Arun Thomas's Gravatar I am also getting an error Element TODOSERVICE is undefined in a Java object of type class [Ljava.lang.String;.
var listBean = variables.todoService.newList()
in do add list is it a proper place to call..?? I can not find out todoService object created before this listbean is creating.
Could you please help me..
# Posted By Arun Thomas | 6/25/09 1:18 AM
Jason Dean's Gravatar @Arun,

The todoService object should be created through autowiring. Which we took care of in Part 8c when we turned on the autowire interceptor, added autowire = true to the event handler, and added a cfproperty for the todoService. If that stuff is not working, then that may be your issue. At the top of your event handler you should be able to dump the todoService and abort and it should show you that the service exists.

<cfdump var="#variables.todoService#">
<cfabort>
# Posted By Jason Dean | 6/25/09 7:24 AM
Terrence Tyson's Gravatar Jason, great series.

I'm a frameworks newbie, and this series is increasing my understanding of dependencies and conventions. But I think I hit a roadblock.

I'm getting the error message: "The method settodoGateway was not found in component /Applications/ColdFusion8/wwwroot/todoslist/model/todos/todoService.cfc." I'm not sure why the app is looking for a setTodoGateway method. Because in the coldspring.xml.cfm the dsn property is included in the todoGateway bean, should it be looking for a setter?
# Posted By Terrence Tyson | 8/21/09 4:05 AM
Jason Dean's Gravatar @Terrence,

Thanks. I am glad you are enjoying the series.

In your coldspring.xml you have this definition:

<bean id="todoService" class="model.todos.todoService">
<property name="todoGateway">
<ref bean="todoGateway" />
</property>
</bean>

Right?

This is where we are telling ColdSpring that it needs to inject the todoGateway into the todoService. It does this through a setter method. So our todoService needs a setTodoGateway() method in it. Like this.

<cffunction name="setTodoGateway" access="public" returntype="void" output="false">
<cfargument name="todoGateway" required="true" type="model.todos.todoGateway" />

<cfset variables.todoGateway = arguments.todoGateway />
</cffunction>

We covered it in part8b be of the series, but you may have missed it.
http://www.12robots.com/index.cfm/2008/11/3/Adding...
Let me know if this doesn't help. Thanks for the comment.
# Posted By Jason Dean | 8/21/09 6:44 AM
Paul Baylis's Gravatar Rich Kroll made a comment "You could put the UUID in as a placeholder so you would have the id handy before and after the save operation. This would allow you to keep the update as well - just skip the extra DB hit and potential for bugs. "

How would this be done in code? I've noticed the post-error empty rows and would like to avoid that. I'm not used to working with UUIDs.

Many thanks. I made it to the end of the tutorial. So, thanks for taking the time Jason. Much appreciated.
# Posted By Paul Baylis | 3/20/10 2:55 AM
Brad's Gravatar Hi Jason,

This is my second time around on this series (love it!), I was able to resolved my error but now I got an error that I just can't figure out. I was hoping you can give me some pointer. I'm getting this error when I try to submit the form. It seem appears that the "@REWRITE@" is not rewriting. Thank you in advance!

HTTP Error 404.0 - Not Found
The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.


Module   IIS Web Core
Notification   MapRequestHandler
Handler   StaticFile
Error Code   0x80070002
Requested URL   http://127.0.0.1:80/dev/mycoldbox@REWRITE@/list/al...
Physical Path   M:\wwwroot\mycoldbox@REWRITE@\list\allLists
Logon Method   Anonymous
Logon User   Anonymous
# Posted By Brad | 6/19/10 1:52 AM
Brad's Gravatar Here’s a little more info as I’m attempt to trouble shot this. I have followed the event and found the problem to be in the list.cfc handler doAddList() method. I added a <cfabort> after the <cfset variables.todoService.saveList(listBean)> and dump the rc variable without any error. I check the database and the data is there. If I remove the <cfabort> I get the error. It seem that this <cfset setNextEvent("list.allLists")> is cause the error because I can go to the url and enter http://127.0.0.1/dev/mycoldbox/index.cfm?event=lis... and it work just fine. I have to be missing something. Any help would be extremely appreciated.
# Posted By Brad | 6/19/10 2:28 AM
Brad's Gravatar I found the solution, in case anyone new to CB is getting the same error here is what i did. I updated the setBaseURL() in config/routes.cfm file from <cfset setBaseURL("http://#cgi.HTTP_HOST#/#getSetting('AppMapping')#@REWRITE@")> to <cfset setBaseURL("http://#cgi.HTTP_HOST#/#getSetting('AppMapping')#/index.cfm")>.
I'm new to CB and not sure if this is the correct way but it worked for me.
# Posted By Brad | 6/19/10 3:07 AM
Paul's Gravatar You mentioned not wanting to use auto-increment primary key on insert because of the hassle with bringing the id back again. How would you go about it if you had no choice but to insert with an auto-increment primary key and get the new id back again.
# Posted By Paul | 2/11/12 3:27 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner