Differences between revisions 6 and 7
Revision 6 as of 2008-08-12 21:04:56
Size: 12104
Comment: corrected typos
Revision 7 as of 2009-09-20 23:01:11
Size: 12104
Editor: localhost
Comment: converted to 1.6 markup
No differences found!

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.

Common operations of create update and delete cycles

  1. Display a master mask (often with search functionality) displaying a set of datasets with subsequent operation triggers for our create, update and delete workflows.
  2. Display a create mask which allows the entry of new data and triggers a save of this data
  3. Display an edit mask, which allows the alteration of existing data and the save of the alterations
  4. Display a delete confirmation mask which asks you before you delete a dataset
  5. (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 preinitialization 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 preinitialization 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 persistable 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 initialization, it might be possible to use it, I will investigate further into this, the usage of actions instead of using valueBound for the detail preinitialization is perfectly viable and correct, but to keep a common pattern it might be better to use valueBound instead.

A_simple_Crud_Cycle (last edited 2009-09-20 23:01:11 by localhost)