Best Practices - A simple Crud Cycle
Introduction
The central core of most applications are masks for data display and editing. The application cycle those set of maskes and operations perform is called the Crud Cycle (Create Update Delete Cycle)
What do we need for such a cycle:
We need a master view and one or several detail views for creation updating and deleting datasets. For the sake of readability we try to focus on the view controller level, in assumption that our business and dao layer does what it should do.
Commpon operations of create update and delete cycles
Display a master maske (often with search functionality) displaying a set of datasets with subsequent operation triggers for our create, update and delete workflows.
Display a create mask which allows the entry of new data and triggers a save of this data
Display an edit mask, which allows the alteration of existing data and the save of the alterations
Display a delete confirmation mask which asks you before you delete a dataset
(optional if not done by one of the other masks, display a view only mask which allows to view datasets as needed.
Simple Crud Mask
Lets start with a simple CRUD mask, lets assume we have a simple room manager for mastering a room database...
What do we need:
A master view controller
A Detail view controller
A master JSF page
A detail JSF page
A Delete confirmation page
the associated DAO and BO objects
The Master View
The master view should be the central entry point for the crud cycle, it should display a set of datasets which then can be further processed, it can contain various search entries etc... for limiting the number of datasets displayed.
A snippet of this view could look like following:
<h:form id="mainarea">
<h:commandLink action="go_create" value="[Create a new Room]">
<t:updateActionListener property="#{roomdetailview.viewmode}"
value="create" />
</h:commandLink>
<t:dataTable id="data" var="room"
value="#{roommasterview.datamodel}" preserveDataModel="false"
rows="10" rowClasses="tableline" styleClass="standardTable"
headerClass="standardTable_Header"
footerClass="standardTable_Header">
<h:column>
<f:facet name="header">
<h:outputFormat value="Id" />
</f:facet>
<h:outputText value="#{room.uid}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputFormat value="Raumname" />
</f:facet>
<h:outputText value="#{room.title}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Bearbeiten" />
</f:facet>
<t:commandLink action="go_detail" immediate="true">
<t:updateActionListener
property="#{roomdetailview.preinitRoomid}" value="#{room.uid}" />
<t:updateActionListener
property="#{roomdetailview.viewmode}" value="edit" />
<t:graphicImage url="../images/edit16.png" border="0"
alt="Edit" />
</t:commandLink>
<t:commandLink action="go_detail" immediate="true">
<t:updateActionListener
property="#{roomdetailview.preinitRoomid}" value="#{room.uid}" />
<t:updateActionListener
property="#{roomdetailview.viewmode}" value="delete" />
<t:graphicImage url="../images/delete16.png" border="0"
alt="Delete" />
</t:commandLink>
</h:column>
</t:dataTable>
</h:form>
The master view itself is a classical page controller which loads a set of datasets and displays them:
public class RoomMasterView implements ConversationBindingListener {
... some additional data
public void refreshRooms() {
clearAll();
datacount = roombo.getDataCount(queryFilter);
List<Room> model = roombo.getPagedData(queryFilter, orderby, pagepos * pagesize, pagesize);
PagedListDataModel loadedData= new PagedListDataModel(model, datacount, pagesize);
this.datamodel = loadedData;
}
public String dosearch() {
refreshRooms();
return "done"
}
public void valueBound(ConversationBindingEvent arg0) {
refreshRooms(); //we first init it with an empty query
}
public void valueUnbound(ConversationBindingEvent arg0) {
}
}
Now we have a search method refreshRooms which updates our current data model which holds our rooms, and we have several callbacks. One being an action probably triggered from a search form.
The other one is more interesting
public void valueBound(ConversationBindingEvent arg0) {
refreshRooms(); //we first init it with an empty query
}
This is called when our conversation is initialized and is defined by the implemented interface
implements ConversationBindingListener {
the ConversationBindingListener provides these callbacks and could be used for our page controllers for preinitialisation of pages.
The valueUnbound is equally interesting, it allows the freeing of resources before the conversation finally is terminated, it is not used in our example however!
From the Master to the Detail View
This is the central core of our master form, so how is this different
Lets have a look at following code:
<t:commandLink action="#{roomdetailview.doDetail}" immediate="true">
<t:updateActionListener property="#{roomdetailview.preinitRoomid}" value="#{room.uid}" />
<t:updateActionListener property="#{roomdetailview.viewmode}" value="edit" />
<t:graphicImage url="../images/edit16.png" border="0" alt="Edit" />
</t:commandLink>
This tag sequence preinitializes some values of our edit mask. Mainly which mode ("Edit instead of new or delete in or case) and the room id of the room we have to process later. After that our init code in doDetail is called
public class RoomDetailView {
RoomMasterView roommasterview;
RoomBO roombo; //business facade
String viewMode;
Integer preinitRoomid;
... setter and getter for the room master
... setter and getter for the view mode and the room it
public String doDetail() {
room = roombo.loadRoomById(preinitRoomid);
... do additional processing
return "go_detail";
}
}
now what has happened? The viewMode is set to Edit, the preinitRoomId is set to our Object, the master view controller and all is associated objects were assigned by spring automatically (note this is the same object instance as before, hence we still have all our master view objects in ram)
No we "misuse" an action method in our detail view controller for preinitialisation and load our working room object from the database into the ram.
So what do we have now
A reference to our master view and all its associated objects (db objecs, bos etc...)
A loaded db object for our detail we can work with
A view mode on our controller set to edit
A reference to our room bo object set by spring
This should be enough to be able to implement a full detail mask
<h:form id="roomeditform">
<h:panelGrid columns="2" styleClass="stdinputlayout">
<h:outputText styleClass="stdlabel" value="Title:" />
<h:panelGroup>
<h:inputText styleClass="stdinput_long"
value="#{roomdetailview.room.title}" required="true"
id="roomtitle" />
<h:message for="roomtitle" />
</h:panelGroup>
<h:outputText styleClass="stdlabel" value="Description" />
<h:inputTextarea styleClass="stdinput_long" cols="30"
value="#{roomdetailview.room.description}" />
</h:panelGrid>
<t:div>
<h:commandButton action="#{roomdetailview.dosubmit}"
value="Save" />
<h:commandButton action="#{roomdetailview.dogomaster}"
value="Overview" immediate="true" />
</t:div>
</h:form>
This is our room edit form with a title, and a description field, a button which should redirect back to our master view is present as well.
Nothing exciting can be found here at the first look, but lets look at following codepart in detail:
<h:inputText styleClass="stdinput_long"
value="#{roomdetailview.room.title}" required="true"
id="roomtitle" />
This is a reference to our preloaded object, which is still alive, despite having crossed the request boundary. All changes done now in this form are directly passed into our peristable object, which still has references to the underlying db orm mapper.
Now to the interesting part:
<h:commandButton action="#{roomdetailview.dosubmit}" value="Save" />
Which should trigger our save operation
public String dosubmit() {
roombo.createUpdateRoom(room);
roommasterview.refreshRooms();
// objects there, different em
return "success";
}
Now what happens here is, that we create or update our rooms and then once being done and no exception is thrown we refresh our still active master room list, so that the updates we are done are available as soon as we go back (always have in mind, everything here is stateful, so we still have the master list available which has to be notified of the changes and has to resync its data)
After that we return and do whatever we want (in most cases jump back to the master list)
The interesting part of this code is the create or update rooms which is nothing more than following code
public class RoomBO {
RoomDAO roomdao;
... setters and getters for the DAO
@Transactional
public Room createUpdateRoom(Room room) {
return roomdao.createUpdateRoom(room);
}
}
The transactional annotation puts our create or update code into a writable transaction context and then calls the associated dao which does the create and update as following:
public class RoomDAO {
@PersistenceContext
EntityManager em;
... setters and getters for the em
public void createUpdateRoom(Room room) {
if(!getEm().contains(room) && room.getUid() == null)
getEm().persist(room);
getEm().flush();
}
}
So what happens here is that we simply check if the object is new or already persisted and issue the subsequent command. If we have to persist anew we have to issue a persist command, for updates a simple em.flush() is sufficient to bring the db into its proper state!
Thats it... If you do not want to use DAOs or BOs, you also could push the entire code into the view contoller:
@Transactional
public String dosubmit() {
if(!getEm().contains(room) && room.getUid() == null)
getEm().persist(room);
getEm().flush();
roommasterview.refreshRooms();
// objects there, different em
return "success";
}
You can see how easy it is with Orchestra to persist objects.
So what is the best way to go to back to our master:
We already have seen that we move back to our master via another action
<h:commandButton action="#{roomdetailview.dogomaster}"
value="Overview" immediate="true" />
So what we do now is just simply to terminate our current detail related conversation and then move back to the master, which by now should have all changes already loaded.
public String dogomaster() {
Conversation.getCurrentInstance().invalidate();
return "go_master";
}
Thats basically it, we are done with it, the master now should display all changes without having to reload everything again.
*Note At the time of writing in our own testcases the valuebound mechanism was not used for detail initialisation, it might be possible to use it, I will investigate further into this, the usage of actions instead of using valueBound for the detail preinitialisation is perfectly viable and correct, but to keep a common pattern it might be better to use valueBound instead.