Saturday, February 16, 2008

GORM insert and update auto-set of createUser and updateUser

Our company has a standard of using the following attributes on all database tables:

String createUser
Date createDate
String updateUser
Date updateDate


But we did not want to put code in all controller's to set these fields on insert or update. Grails has a nice facilities where you specify code in the beforeInsert and beforeUpdate closures to set the dates:

def beforeInsert = {
createDate = new Date()
}
def beforeUpdate = {
updateDate = new Date()
}

Grails will call these closures automatically. But the issue is setting the createUser and updateUser. What we did is create an abstract base class (concrete makes Grails look for a table called base):

abstract class Base {

String createUser = ''
Date createDate = new Date()
String updateUser = ''
Date updateDate = new Date()

def beforeInsert = {
setCreateUsername() // must be injected in login controller
createDate = new Date()
delegate.createUser = this.createUser
delegate.updateUser = this.updateUser
}

def beforeUpdate = {
setUpdateUsername() // must be injected in login controller
updateDate = new Date()
delegate.updateUser = this.updateUser
}
}


Each of our domain classes extended Base (so they did not have to specify the 4 fields.)
Then, when the user login is handled by the Login controller, we inject the setCreateUsername and updateUsername "methods" as closures that set the username with the HttpSession value:


def user = User.findWhere(userId:params.userId, userPwd:params.userPwd)
session.user = user
Base.metaClass.setCreateUsername = {
createUser = session.user.userId
updateUser = session.user.userId
}
Base.metaClass.setUpdateUsername = {
updateUser = session.user.userId
}


And the four columns are automatically set on all insert and updates.

Grails WebFlow and explicit events

Grails WebFlow enables the ability to keep a context specific to a set of pages. Grails webflow is built on top of Spring webflow but, as always, Grails adds ease of use with a DSL.
When do you need webflow? Whenever you find yourself stuffing things in the session context.
One side note, if you are putting something in the session that is used in the very next request, use the flash context, as it retains items in the flash scope for two request cycles (unless the item is refreshed on the second request.

A webflow is contained in one controller. Reading existing webflows is fairly easy but, until you become comfortable with a few terms, writing webflows can be problematic. Things that you need to understand are Flow Scopes, View States, and Action States.

To get started with webflow, read, in its entirety, the documentation at WebFlow and then download and play with the book-flow
sample application. If you do not know how to check out a Subversion, email me and I'll walk you through it.

Grails WebFlow seems to be limited somewhat in GSP tags: g:submitButton and g:link. Remember that in the Grails RESTful architecture, URLs follow the domain/controller/action convention to identify the controller and closure (the action) to handle the request. WebFlows adds the concept of an event. Strangely the g:submitButton's name attribute defines the event but the g:link uses the more obvious event attribute. But what if you want to use an image button or invoke a request from a JavaScript event?

If you look at the HTML generated from the g:submitButton or g:link tags you will see a request parameter with a name the is prefixed with _eventId_. The string that follows the second underscore identifies the WebFlow event.
So for an application that requires an image, a click of which should be handled by the edit event of a webflow:

<input class="edit" name="_eventId_edit"id="_eventId_edit"
value="Edit" type="image"
src="/FAB/images/skin/database_edit.png"
/>

To have a select list that runs the createMop (method of payment) event when a user select a item from the method of payment types list:

<g:select id='mopType' name='mopType'
onchange="location='/FAB/document/documentWork?_eventId_createMop=createMop&'+Form.serialize(\'docLogForm\');"
from='${(BootStrap.mopTypes.entrySet().value)}'
keys='${(BootStrap.mopTypes.entrySet().key)}'
/>

Notice the use of the serialize method of the Form object. The serialize method comes from the prototype JavaScript library.