Thursday, 20 August 2009

Seam Master-detail and Pagination - 2nd Try

After further work and testing, I have a Master-Detail editing page, with built in pagination for the Detail instances. The scenario applies where a single Master has a large number of Details. For example, a General Ledger Account, which has many thousands or millions of Transactions.

Key is to avoid referring to the Master's mapped list of Details (or remove the Hibernate mapping entirely?). This will mean that Hibernate will not load the large number of Details. However, transaction management of the Detail instances becomes a problem, as I struggle to find a solution that retains Seam's transaction management, but has it including new Details as well as changed items.

The solution that follows does function. The normal editing of existing Details works as expected, with changes saved when the Save button is pushed. However any new Detail instances are saved immediately on closing the ModalPanel. This problem is because we're not adding the new Details into the Master (which would mean that they are persisted as part of saving the Master).

One solution would be to keep a local List of new Detail instances, and override the EntityHome update() and persist() functions to iterate the List and persist() the Detail entities. However going to this much trouble is making me wonder if there is not a simpler approach.

Anyway, here is the code for the revised example:

1. The EntityHome class:

package org.asr.md.session;

import javax.persistence.EntityManager;

import org.asr.md.entity.*;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.framework.EntityHome;

@Name("masterEditorWithDetailPaging")
@Scope(ScopeType.CONVERSATION)
public class MasterEditorWithDetailPaging extends EntityHome {

private static final long serialVersionUID = -7731494569738456413L;
@In EntityManager entityManager;
@In(create=true) DetailListForPaging detailListForPaging;

private String filterDescription;
private Detail currentlySelectedDetail;

//Getters and setters
public String getFilterDescription() { return this.filterDescription; }
public void setFilterDescription(String filterDescription) { this.filterDescription = filterDescription; }
public Detail getCurrentlySelectedDetail() { return this.currentlySelectedDetail; }
public void selectDetail(Detail detail) { this.currentlySelectedDetail = detail; }

public void createNewDetail() {
Detail detail = new Detail();
detail.setMaster(getInstance());
detailListForPaging.getResultList().add(detail);
currentlySelectedDetail = detail;
}

public void saveNewDetail() {
if (!entityManager.contains(currentlySelectedDetail)) {
entityManager.persist(currentlySelectedDetail);
}
}

public void cancelNewDetail() {
removeDetail(currentlySelectedDetail);
}

public void cancelChanges() {
entityManager.clear();
}

//Delete a detail item
public void removeDetail(Detail detail) {
detailListForPaging.getResultList().remove(detail);
entityManager.remove(detail);
}

public String filterDetails() {
detailListForPaging.setMasterInstance(getInstance());
detailListForPaging.setDetailDescription(filterDescription);
return "success";
}

public void setMasterId(Integer id) {
setId(id);
}

public Integer getMasterId() {
return (Integer) getId();
}

@Override
public String update() {
return super.update();
}

@Override
protected Master createInstance() {
Master master = new Master();
return master;
}

public void wire() {
getInstance();
}

public boolean isWired() {
return true;
}

public Master getDefinedInstance() {
return isIdDefined() ? getInstance() : null;
}
}


2. The DetailQuery class:

package org.asr.md.session;

import org.asr.md.entity.*;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.framework.EntityQuery;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;

@Name("detailListForPaging")
@Scope(ScopeType.CONVERSATION)
public class DetailListForPaging extends EntityQuery {

private static final long serialVersionUID = -427189818462003108L;
private static final String EJBQL = "select detail from Detail detail";
private static final String[] RESTRICTIONS = { "detail.master = #{detailListForPaging.masterInstance}",
"lower(detail.detailDescription) like concat(#{detailListForPaging.detailDescription},'%')", };

private String detailDescription = new String();
private Master master;
private boolean criteriaHaveChanged = true;

public DetailListForPaging() {
setEjbql(EJBQL);
setRestrictionExpressionStrings(Arrays.asList(RESTRICTIONS));
setMaxResults(25);
}

public List getResultList() {
//If the detailDescription is not set to something, then return an empty List.
if ((master == null) || (detailDescription.isEmpty())) {
return new Vector(0);
}

if (criteriaHaveChanged) {
setRestrictionExpressionStrings(Arrays.asList(RESTRICTIONS));
criteriaHaveChanged = false;
}
return super.getResultList();
}

public void setDetailDescription(String detailDescription) {
this.detailDescription = detailDescription;
this.criteriaHaveChanged = true;
}
public String getDetailDescription() { return this.detailDescription; }
public void setMasterInstance(Master master) {
this.master = master;
this.criteriaHaveChanged = true;
}
public Master getMasterInstance() { return this.master; }

}


3. The XHTML page:

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:a="http://richfaces.org/a4j"
xmlns:rich="http://richfaces.org/rich"
template="layout/template.xhtml">

<ui:define name="body">

<rich:modalPanel id="detailEditWizard"
minHeight="200"
minWidth="500"
autosized="true">

<f:facet name="header"><h:outputText value="Edit Detail..." /></f:facet>
<f:facet name="controls">
<h:graphicImage value="/img/ico_close.gif"
styleClass="linkImage"
onclick="#{rich:component('detailEditWizard')}.hide()" />
</f:facet>

<h:form id="detailEditForm" styleClass="edit">

<h:messages globalOnly="true"/>

<s:decorate id="masterIdField" template="layout/edit.xhtml">
<ui:define name="label">Master id</ui:define>
<h:outputText id="masterId"
required="true"
value="#{masterEditorWithDetailPaging.currentlySelectedDetail.master.id}">
</h:outputText>
</s:decorate>

<s:decorate id="detailDescriptionField" template="layout/edit.xhtml">
<ui:define name="label">Detail description</ui:define>
<h:inputText id="detailDescription"
required="true"
size="100"
maxlength="100"
value="#{masterEditorWithDetailPaging.currentlySelectedDetail.detailDescription}">
<a:support event="onblur" reRender="detailDescriptionField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>

<div class="actionButtons" style="clear:both">

<a:commandButton id="detailEditMPClose"
onclick="#{rich:component('detailEditWizard')}.hide()"
action="#{masterEditorWithDetailPaging.saveNewDetail()}"
reRender="detailListTable"
value="Save" />

<a:commandButton id="detailEditMPCancel"
onclick="#{rich:component('detailEditWizard')}.hide()"
action="#{masterEditorWithDetailPaging.cancelNewDetail()}"
reRender="detailListTable"
value="Cancel" />

</div>
</h:form>

</rich:modalPanel>

<h:form id="master" styleClass="edit">

<rich:panel>
<f:facet name="header">#{masterEditorWithDetailPaging.managed ? 'Edit' : 'Add'} Master</f:facet>

<s:decorate id="masterDescriptionField" template="layout/edit.xhtml">
<ui:define name="label">Master description</ui:define>
<h:inputText id="masterDescription"
size="100"
maxlength="100"
value="#{masterEditorWithDetailPaging.instance.masterDescription}">
<a:support event="onblur" reRender="masterDescriptionField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>

<div style="clear:both">
<span class="required">*</span>
required fields
</div>

</rich:panel>

<rich:panel>
<f:facet name="header">Detail Filter</f:facet>
<s:decorate id="detailDescriptionFilterField" template="layout/edit.xhtml">
<ui:define name="label">Detail Description</ui:define>
<h:inputText id="detailDescriptionFilter"
size="100"
maxlength="100"
value="#{masterEditorWithDetailPaging.filterDescription}">
<a:support event="onblur" reRender="detailDescriptionFilterField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>

<div class="actionButtons">

<h:commandButton id="filter"
value="Search"
action="#{masterEditorWithDetailPaging.filterDetails}"
disabled="#{!masterEditorWithDetailPaging.wired}"
rendered="#{masterEditorWithDetailPaging.managed}">
<s:conversationId />
</h:commandButton>

<a:commandButton id="doFiltering"
action="#{masterEditorWithDetailPaging.filterDetails()}"
reRender="detailListTable"
value="Filter">
<s:conversationId />
</a:commandButton>

</div>

</rich:panel>

<!-- Detail section -->
<rich:panel>
<f:facet name="header">Detail List</f:facet>
<div class="results" id="detailList">

<h:outputText value="The detail search returned no results."
rendered="#{empty detailListForPaging.resultList}"/>

<rich:dataTable id="detailListTable"
var="_detail"
value="#{detailListForPaging.resultList}">
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Id"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.id}"/>
</h:column>
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Master id"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.master.id}"/>
</h:column>
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Detail description"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.detailDescription}"/>
</h:column>
<rich:column styleClass="action">
<f:facet name="header">Action</f:facet>
<a:commandLink id="editDetailMP"
action="#{masterEditorWithDetailPaging.selectDetail(_detail)}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="Edit">
<s:conversationId />
</a:commandLink>
#{' '}
<a:commandLink id="removeDetail"
action="#{masterEditorWithDetailPaging.removeDetail(_detail)}"
reRender="detailListTable"
value="Delete">
<s:conversationId />
</a:commandLink>

</rich:column>
</rich:dataTable>

</div>
<s:div styleClass="actionButtons" rendered="#{empty from}">
<a:commandLink id="editDetailMP"
action="#{masterEditorWithDetailPaging.createNewDetail()}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="New Detail...">
<s:conversationId />
</a:commandLink>
</s:div>
<div class="tableControl">

<s:link id="firstPage"
action="#{detailListForPaging.first()}"
reRender="detailListTable"
value="First Page">
</s:link>

<s:link id="previousPage"
action="#{detailListForPaging.previous()}"
reRender="detailListTable"
value="Previous Page">
</s:link>

<s:link id="nextPage"
action="#{detailListForPaging.next()}"
reRender="detailListTable"
value="Next Page">
</s:link>

<s:link id="lastPage"
action="#{detailListForPaging.last()}"
reRender="detailListTable"
value="Last Page">
</s:link>

</div>
</rich:panel>

<div class="actionButtons">

<h:commandButton id="save"
value="Save"
action="#{masterEditorWithDetailPaging.persist}"
disabled="#{!masterEditorWithDetailPaging.wired}"
rendered="#{!masterEditorWithDetailPaging.managed}"/>

<h:commandButton id="update"
value="Save"
action="#{masterEditorWithDetailPaging.update}"
rendered="#{masterEditorWithDetailPaging.managed}"/>

<h:commandButton id="delete"
value="Delete"
action="#{masterEditorWithDetailPaging.remove}"
immediate="true"
rendered="#{masterEditorWithDetailPaging.managed}"/>

<s:button id="done"
value="Done"
propagation="end"
view="/MasterList.xhtml"
rendered="#{masterEditorWithDetailPaging.managed}"/>

<s:button id="cancel"
value="Cancel"
action="#{masterEditorWithDetailPaging.cancelChanges}"
propagation="end"
view="/#{empty masterFrom ? 'MasterList' : masterFrom}.xhtml"
rendered="#{masterEditorWithDetailPaging.managed}"/>

</div>

</h:form>

</ui:define>

</ui:composition>


4. The page.xml:

<?xml version="1.0" encoding="UTF-8"?>
<page xmlns="http://jboss.com/products/seam/pages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.1.xsd"
no-conversation-view-id="/MasterList.xhtml"
login-required="true">

<begin-conversation join="true" flush-mode="MANUAL"/>

<action execute="#{masterEditorWithDetailPaging.wire}" on-postback="false"/>

<param name="masterFrom"/>
<param name="masterId" value="#{masterEditorWithDetailPaging.masterId}"/>


<navigation from-action="#{masterEditorWithDetailPaging.persist}">
<rule>
<end-conversation/>
<redirect view-id="/MasterEditorWithPaging.xhtml"/>
</rule>
</navigation>

<navigation from-action="#{masterEditorWithDetailPaging.update}">
<rule>
<end-conversation/>
<redirect view-id="/MasterEditorWithPaging.xhtml"/>
</rule>
</navigation>

<navigation from-action="#{masterEditorWithDetailPaging.remove}">
<rule>
<end-conversation/>
<redirect view-id="/MasterEditorWithPaging.xhtml"/>
</rule>
</navigation>

</page>

13 comments:

  1. Hello again Andrew :D

    Why detailListTable return an empty List when i tried to edit one row in a master?

    i see in the table detail value is detailListForPaging.resultList

    and this is the resultList method:

    public List getResultList() {
    // If the detailDescription is not set to something, then return an
    // empty List.
    if ((master == null || (detailDescription == null))) {
    return new Vector(0);
    }

    if (criteriaHaveChanged) {
    setRestrictionExpressionStrings(Arrays.asList(RESTRICTIONS));
    criteriaHaveChanged = false;
    }
    return super.getResultList();
    }


    i am using java 1.5 so, i replace detailDescription.isEmpty() with detailDescription == null


    Thanks

    ReplyDelete
  2. Hi Rab,

    I put that logic into the function so that it would not return the complete list of Details when you do a search without having specified a filter. I figured that if you're trying to filter the Details, you are doing so because there are too many to display all of them at once. So you don't want anything triggering the population of the list by mistake. I guess you would also want to put a "required" attribute on the search detailDescription field.

    ReplyDelete
  3. Thanks for your explaination.

    I want to return complete list by default, because i dont have many data in my Details.
    Please help how to do that?
    Thanks.

    ReplyDelete
  4. Hi Rab,

    If you want to return the complete list by default (Hibernate will load all Detail rows, so if you're dealing with large volumes, this might take a while), you will need to change the getResultList() function.

    Instead of returning a new Vector if the detailDescription field is empty/blank, just execute the Query as normal. Because the detailDescription will be empty, the RESTRICTIONS will end up looking like "%", which should match to all rows. The query should then return all the Detail rows.


    So the function should look something like this (I cannot test this out right now as I am traveling):

    public List getResultList() {
    //If the Master is null for any reason, then return an empty List.
    if (master == null) {
    return new Vector(0);
    }

    if (criteriaHaveChanged) {
    setRestrictionExpressionStrings(Arrays.asList(RESTRICTIONS));
    criteriaHaveChanged = false;
    }
    return super.getResultList();
    }

    Also important to note - the criteriaHaveChanged boolean is initialised to True, so when you first use the component, the second part of the function will be triggered (causing the retrieval of all rows matching the restriction criteria).

    ReplyDelete
  5. Hi Andrew,

    The problem is master always return null. so return new Vector always called, and show an empty list. I tested with System.out.println(master).

    Then, i removed the conditional

    if (master == null) {
    return new Vector(0);
    }

    it return all the Detail rows, not just Detail from the Master instance.

    Thanks.

    ReplyDelete
  6. Hi Rab,

    Sorry, I missed that bit when thinking about your question earlier. There is one more change you'll need to make (again, I'm unable to test this out right now as I don't have access to my normal machine).

    The Master instance variable should be set by the MasterEditor component. Seam will inject the detailListForPaging, creating automatically if needed.

    @In(create=true) DetailListForPaging detailListForPaging;

    When the user clicks the "Filter" button, the filterDetails() function will be triggered. This sets the Master on the detailListForPaging, and then triggers a re-display of the detail datatable list. This causes a retrieval from the Query component.

    However, if you want all Detail rows displayed at the beginning (when you first display the page for a Master instance), you'll need to trigger the filterDetails() some other way. I'd suggest putting a call in the wire() function on the Master - that is called when the page is displayed.

    ReplyDelete
  7. Thanks andrew, works like a charm :)
    Like your suggested, i called filterFuelDetails() in wire() function.

    if(getDefinedInstance() != null) {
    filterFuelDetails();
    }

    My new problem is with update the detail item.
    As you said before
    "new Detail instances are saved immediately on closing the ModalPanel"

    Now, i want to update edit detail, click Save not work.
    I think entityManager.persist(currentlySelectedFuelDetail);
    doesnt work with update edit detail.

    I add update button in modal panel.

    a:commandButton id="fuelDetailEditMPUpdate"
    onclick="#{rich:component('fuelDetailEditWizard')}.hide()"
    action="#{masterEditorWithDetailPaging.update}"
    reRender="fuelDetailListTable"
    value="Update"

    it works! but i am not sure this is ok or not.

    Now, i had 2 button.. Save and Update in modal panel, but how to managed rendered button?
    If New Detail is clicked, then show the Save button, if Edit is clicked, then show the Update button.

    Thanks.

    ReplyDelete
  8. Hi,

    The best thing would be to use the same approach that a seam-gen XHTML file does for this situation. There are usually two Save buttons, one which calls persist(), and one which calls update(). The key part if the "rendered={#XXXXX}" attribute on the button.

    You want to call persist() when dealing with a new row, so just check if the entity is "managed" by Seam. If it's not, then it's a new entity which needs to be persist()ed, so show that version of the Save button. If it is already "managed" by Seam, then you need to update() instead, so show that version of the Save button.

    The "Save" buttons on the XHTML above use this approach - look for the lines
    rendered="#{!masterEditorWithDetailPaging.managed}"

    Try using that approach and see if it does what you need?

    ReplyDelete
  9. Hi Andrew,

    using managed not work to rendered the Save and Update button in Modal panel.

    So, i added a field with boolean type in masterEditorWithDetailPaging component class.

    private boolean createdNewDetail;

    then, when createNewDetail() method triggered set createdNewDetail to true.

    public void createNewDetail() {
    Detail detail = new Detail();
    detail.setMaster(getInstance());
    detailListForPaging.getResultList().add(detail);
    currentlySelectedDetail = detail;
    createdNewDetail = true;
    }

    other, if selectDetail / Edit triggered, createdNewDetail set to false;

    public void selectDetail(Detail detail) {
    this.currentlySelectedDetail = detail;
    createdNewDetail = false;
    }


    public boolean isCreatedNewDetail() {
    return createdNewDetail;
    }

    Then, in modal panel xhtml:

    a:commandButton id="detailSaveMPClose"
    onclick="#{rich:component('detailEditWizard')}.hide()"
    action="#{masterEditorWithDetailPaging.saveNewDetail()}"
    reRender="detailListTable"
    rendered="#{masterEditorWithDetailPaging.createdNewDetail}"
    value="Save"


    a:commandButton id="detailEditMPClose"
    onclick="#{rich:component('detailEditWizard')}.hide()"
    action="#{masterEditorWithDetailPaging.saveNewDetail()}"
    reRender="detailListTable"
    rendered="#{!masterEditorWithDetailPaging.createdNewDetail}"
    value="Update"


    Thanks.

    ReplyDelete
  10. Hi Andrew,

    Can you please help me, how to display a random quote from database? i want to display a wise word. The example in this website:

    http://www.shultz-project.com/web-development/homepage.jsf

    There is a random quote using richfaces effect appear and fade, but i can't figure it out, where is the message came from, i guess from the database and display randomize using servlet or what...

    Can u help me please?

    ReplyDelete
  11. Hi Rab,

    I'd suggest setting up a table in the database - two columns: "id", and "quotation". Map this to a simple entity in Seam.

    Then create a component that has one function that returns a String. In the function, generate a random number, and use that is a parameter in a Query. The Query should return the quotation for whatever row has an ID that matches the random number.

    Then call the component's function from within an EL expression on the XHTML page.

    ReplyDelete
  12. Hi Andrew,

    I had done what you suggested.

    I mapped table to a simple entity in seam.
    Then i created a function that return a String.

    I am using rand() mysql function to random select row.

    So, my component like this:

    public String getQuotes() {
    Query q = getEntityManager().createQuery("select o.quote from WiseWord o order by rand()");
    q.setMaxResults(1);
    return q.getSingleResult().toString();
    }


    And then i called component with EL expression in XHTML page :

    h:outputText value="#{wiseWordList.quotes}"

    Success! Quote appear...

    But, now i had problem.
    When the quote appear, fade and appear again, display the same quote, not randomize.

    I have to Refresh the page to display a random quote.

    Btw, i am using rich:effect to display show and hide effect quote.


    Many Thanks.

    ReplyDelete
  13. Rab,

    Sorry, I've never used the rich:effect tag, so I am not going to be much help. Essentially what is missing is that the getQuotes() function is only being called when the page is initially rendered, but you want it to be called each time the effect completes.

    I did take a quick look through some of the docs, and wonder whether you might be able to do something with ajax:support to trigger a "reRender" of the outputText component when the effect is completed - or something like that.

    ReplyDelete