Wednesday, 22 April 2009

Seam - Master-Detail Views

Building master-detail views using Seam is a regular topic on the forums, and with good reason. It's not trivial for new developers.

There are three levels that have to work together:
  1. The Hibernate mapping between the master and detail entities
  2. The Seam layer - the EntityHome or other bean
  3. The presentation layer
In terms of requirements for a maintenance screen that deals with both master and detail:
  • you want the user to update both master and detail records on the same page
  • you want to save changes to master and detail at the end (or optionally part-way through)
  • you should be able to add new detail rows or delete existing ones and see the changes reflected immediately on the screen, again without navigating between pages

So to an example. My two entities are Master and Detail, represented in the MySQL database as:



CREATE TABLE master (
id int(11) NOT NULL AUTO_INCREMENT,
master_description varchar(100) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM

CREATE TABLE detail (
id int(11) NOT NULL AUTO_INCREMENT,
master_id int(11) NOT NULL,
detail_description varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY newfk (master_id)
) ENGINE=MyISAM



The Hibernate mappings need to reflect the one-to-many relationship from master to detail.



@Entity
@Table(name = "master", catalog = "test")
public class Master implements java.io.Serializable {

private Integer id;
private String masterDescription;
private List<detail> details = new Vector<detail>(0);

public Master() {
}

public Master(String masterDescription, List<detail>details) {
this.masterDescription = masterDescription;
this.details = details;
}

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
public Integer getId() {
return this.id;
}

public void setId(Integer id) {
this.id = id;
}

@Column(name = "master_description", length = 100)
@Length(max = 100)
public String getMasterDescription() {
return this.masterDescription;
}

public void setMasterDescription(String masterDescription) {
this.masterDescription = masterDescription;
}

@OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY, mappedBy = "master")
public List<detail> getDetails() {
return this.details;
}

public void setDetails(List<detail> details) {
this.details = details;
}
}


Note that this example uses List and Vector instead of the Sets that Seam-gen uses by default. Select lists and some other presentation widgets seem to require Lists rather than Sets, and it's become a habit to use them instead of the default Seam-gen code.


@Entity
@Table(name = "detail", catalog = "test")
public class Detail implements java.io.Serializable {

private Integer id;
private String detailDescription;
private Master master;

public Detail() {
}

public Detail(Master master, String detailDescription) {
this.master = master;
this.detailDescription = detailDescription;
}

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
public Integer getId() {
return this.id;
}

public void setId(Integer id) {
this.id = id;
}

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "master_id", nullable = false)
@NotNull
public Master getMaster() {
return this.master;
}

public void setMaster(Master master) {
this.master = master;
}

@Column(name = "detail_description", nullable = false, length = 100)
@NotNull
@Length(max = 100)
public String getDetailDescription() {
return this.detailDescription;
}

public void setDetailDescription(String detailDescription) {
this.detailDescription = detailDescription;
}
}


MappedBy = "master" identifies the name of the property on the detail entity that links back to the master entity.


Now we need a backing bean for the maintenance page. Seam-gen will give you a set of XXXList, XXX, and XXXEdit pages for each entity. The XXX and XXXEdit pages refer to an EntityHome instance for the entity to be viewed/edited. So after running Seam-gen to reverse-engineer from the MySQL database we will have a set of XHTML pages
  • Master.xhtml
  • MasterEdit.xhtml
  • MasterList.xhtml
  • Detail.xhtml
  • DetailEdit.xhtml
  • DetailList.xhtml
and a set of four backing beans:
  • MasterHome
  • MasterList
  • DetailHome
  • DetailList
Really we want to end up with a single page, and therefore a single backing bean that encompasses the functions of both MasterHome and DetailHome.


@Name("masterEditor")
public class MasterEditor extends EntityHome<Master> {

@In EntityManager entityManager;

@DataModel private List<Detail> detailListFromMaster = new Vector<Detail>(0);
@DataModelSelection @Out(required=false) private Detail currentlySelectedDetail;

//For the selection on the detail list
public void select() {}

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

public void cancelNewDetail() {
removeDetail();
}

//Delete a detail item
public void removeDetail() {
detailListFromMaster.remove(currentlySelectedDetail);
entityManager.remove(currentlySelectedDetail);
}

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() {
//Both triggers getInstance() and sets the local variable
//to the list of Detail items from the Master entity
detailListFromMaster = getInstance().getDetails();
}

public boolean isWired() {
return true;
}

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


This is an edited EntityHome, which started life as MasterHome. A few things to note:
  • we want to use a DataModel and DataModelSelection to link with the presentation layer
  • the wire() function now gets the list of Detail objects from the Master object and pushes them into the DataModel list
  • there are a few simple functions added to be triggered from the presentation layer to do simple CRUD stuff (add a new Detail, remove, etc)


So on to the presentation layer. This is in two parts - the main page, plus a Modal Panel, all using RichFaces.

[UPDATE: Note that if you want a presentation technique that does not use Modal Panels, try this more recent post.]


<!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:inputText id="masterId"
required="true"
value="#{currentlySelectedDetail.master.id}">
<a:support event="onblur" reRender="masterIdField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</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="#{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()"
reRender="detailListTable"
value="Close" />

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

</div>
</h:form>

</rich:modalPanel>



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

<rich:panel>
<f:facet name="header">#{masterEditor.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="#{masterEditor.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>


<!-- 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 masterEditor.instance.details}"/>

<rich:dataTable id="detailListTable"
var="_detail"
value="#{detailListFromMaster}"
rendered="#{not empty detailListFromMaster}">
<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="#{masterEditor.select()}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="Edit">
<s:conversationId />
</a:commandLink>
#{' '}
<a:commandLink id="removeDetail"
action="#{masterEditor.removeDetail()}"
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="#{masterEditor.createNewDetail()}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="New Detail...">
<s:conversationId />
</a:commandLink>
</s:div>
</rich:panel>

<div class="actionButtons">

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

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

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

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

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

</div>

</h:form>

</ui:define>

</ui:composition>

22 comments:

  1. Hi, I have a couple of questions :

    1. Suppose that every master have a lot of details, let say, about a million. Normally, we would use dataScroller for dataTable, and pagination.
    a)How to determine total number of details for particular master ? Can we simply use master.getDetails().size() /and what Hibernate will do in this case - fetch all details into memory, or exists a better/cheaper way to do this ?
    b) regarding pagination - how, starting from n-th detail, to obtain the next m detail record /and achieve the same effect as with setFirstResult(), setMaxRsult()/ ? Manipulate with List object in java, or execute another Query ?

    2. Suppose we want to further refine search troughout master.getDetails()- let user to propose search criteria, and find all details (for particular master) conforming that search criteria. How to do that ? Make another Query with that criterias, or somehow different ?

    ReplyDelete
  2. In response to the two points raised:

    1. Masters with large data sets for Details:
    I'm not an expert on the details of what Hibernate would do, so I cannot answer the specific question. However, if you are dealing with such a large dataset, I suspect the presentation style I used in the example (one page with a popup modal panel to edit a detail row) would not be appropriate. I would be more likely to start with a MasterHome with an embedded Query, allowing for search criteria and pagination on the Detail entity. If you're dealing with that much data, I would imagine the focus is less on the Master and more about finding the specific Detail instances that you're interested in? (I could be mistaken!).

    However, I have seen examples where people have built pagination on the Detail entity List, so you could take that approach. It just seems to me that you're probably starting from a different place.

    2. Yes, I would start with a Query (embedded within the Master), where search criteria are Master + whatever the user enters as filters.

    ReplyDelete
  3. - thanks for response, and for advice about embedded Query! Yes, this approach is what I need, but I do not understand why presentation style with modal popup (for editing detail row) is not appropriate in this scenario ? Simply, I want to show details in the dataTable, and allow user to edit detail record in that modal popup...

    Further - the select() method is empty in the MasterEditor class, why ? How to select current detail in order to edit him ?

    "I have seem examples where poeple have built pagination on the Detail entity List.."

    - can you please point me to those examples, if some available, posibly along with the source code ?

    ReplyDelete
  4. Sorry, now I understand what you're doing with the dataset, I can see the presentation style will still work. In my past experience (life insurance systems), large datasets are usually never shown in a list. Instead you start with a search page, then show just the one or two rows that match. But that's really what you're saying anyway?

    The select() method is something I found I needed to add to get the @Datamodel and @DatamodelSelection to work. According to my reading of the documentation it should not be needed. However I found in some instances it would not work correctly without it. And I still have not narrowed things down to the specific instance.

    I don't have the references to hand (for writing pagination) but I will see if I can find them again, and will post here.

    ReplyDelete
  5. - Just to check, when you say "Query embedded within the Master", you probably mean to define @NamedQuery in the Master entity class, and invoke him with entityManager.createNamedQuery() ?

    ReplyDelete
  6. I was actually thinking of using a Query object as a private variable within the Master instance. However on thinking further it might get complicated with managing the changes? I'll see if I can put together a simple example.

    ReplyDelete
  7. Ok, I tested out the approach, and there is a post here with updates to the original example.

    ReplyDelete
  8. - Ok, I saw you example, we will come to this later. But for now, just to ensure that you understand point of my first question, take a look at :

    http://wiki.apache.org/myfaces/WorkingWithLargeTables

    - So even with filtering capability for details, there still can be problem with large dataset, if we simply say :

    rich:dataTable value="#{detailListFromMaster}"

    -So I think, we need another way to specify value for such one datatable - avoid master.getDetails() completely, and provide solution with custom DataModel.
    Just want to know what do you think...

    ReplyDelete
  9. Sure, I think I understand what you're getting at: calling the detailsListFromMaster object will mean retrieving all of the Detail instances as soon as the page is opened.

    Without having worked through it in detail, your suggestion makes a lot of sense. So you would start the MasterEditor without ever retrieving the Detail list from the Master instance. Then when the user enters their search criteria, execute the Query and populate the list for display.

    You could work with an injected QueryHome (seam class) instead of the Query object I used in my example. So something like
    @In(create=true) QueryHome detailQueryList;
    Then use it in much the same way I was using the Query class. For the front end, using the same style of logic that the seam-gen XXXXList.xhtml pages use for dataset paging.
    That would give the paging functionality as needed, and the rest of the example should work as described? Or yes, you could write a custom DataModel wrapper.

    Working with a native List (returned from a Query or QueryHome) instead of a Datamodel, we'd need to add some extra stuff to notify the MasterEditor when we are editing a row in the table. For example, an a:support to the datatable, calling a MasterEditor.selectDetail(Detail detailSelected) method. Or this could be done in the method called from the "Edit" link - tell MasterEditor which Detail we are about to edit, then open the ModalPanel.

    I would need to test it out to confirm what Hibernate will do with the embedded Details list when we start editing the Master. Might need to remove the mapping to Detail from the Master entity? Or if the lazy loading works properly, the List should never be loaded because we never touch Master.getDetails()...

    I'll have another go at putting together a complete example like you've described.

    ReplyDelete
  10. Oops - fat fingers. I meant EntityQuery, not QueryHome...

    ReplyDelete
  11. I have just tested your master-detail example. However, got the following error when try to add the detail record:

    Caused by javax.servlet.ServletException with message: "javax.el.PropertyNotFoundException: /master.xhtml @34,60 value="#{currentlySelectedDetail.detailDescription}": Target Unreachable, identifier 'currentlySelectedDetail' resolved to null"

    Seam and JBoss version -> Seam 2.1 and JBoss 4.2 server.

    ReplyDelete
  12. Hi Zairi, sorry to hear it failed. The way it should work:

    Click the "New Detail" link, which looks something like:
    <s:div styleClass="actionButtons" rendered="#{empty from}">
    <a:commandLink id="editDetailMP"
    action="#{masterEditor.createNewDetail()}"
    oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
    reRender="detailEditForm"
    value="New Detail...">
    <s:conversationId />
    </a:commandLink>
    </s:div>

    This calls masterEditor.createNewDetail(), which tells masterEditor to create a new Detail instance, and to add it to the details List. It also assigns it to currentlySelectedDetail, which is what is used in the ModalPanel.

    If you're getting that error, I would start by adding a System.out.println into the createNewDetail() function - just to confirm that it's being called. If it is being called, then print out what is in currentlySelectedDetail at the end of the createNewDetail() function - just to check that the new Detail instance is being created and assigned correctly.

    Lastly, I'd use debug.seam to take a look at what is happening to the masterEditor component throughout the process.

    FYI, I am using Seam 2.1.2, on JBoss 4.2.3.

    ReplyDelete
  13. I've upgraded to Seam 2.1.2 from 2.1.1. The 'New Detail' function is now working. However, calling 'masterEditor.persist' does not inserting the detail.

    ReplyDelete
  14. That suggests that the Detail instance is not getting added into the Master's list of attached Detail instances. Persisting the Master will automatically save any Details attached to it. Try debugging the createNewDetail() to make sure that your Master's list is growing in size by 1 when you create and attach the new Detail.

    ReplyDelete
  15. so strange ... the details were there but only master was updated, no insert stmt was generated ...

    17:47:42,343 INFO [STDOUT] Update All!
    17:47:42,343 INFO [STDOUT] Master:11
    17:47:42,343 INFO [STDOUT] Detail Size:2
    17:47:42,343 INFO [STDOUT] Detl Rec:I am the detl no 1
    17:47:42,343 INFO [STDOUT] Detl Rec:I am the detl no 2
    17:47:42,343 INFO [STDOUT] Hibernate:
    update
    mydb2.master
    set
    master_description=?
    where
    id=?

    ReplyDelete
  16. Zairi, if you send me your source code to arickard -at- amberleyseven -dot- com I can try testing it out and see if I can figure out what the problem is?

    ReplyDelete
  17. Finally, got it working. I recreated the project using seam command line instead of using Seam Tool. Then somehow it just worked.

    Thanks for the excellent master-detail example.
    Thanks also for your help :)

    ReplyDelete
  18. Hello Andrew,

    Thanks for your the tip. But i had a problem get Master Id.

    I had prinln the createNewDetail() method System.out.println(currentlySelectedDetail.getMaster().getId());

    Output:
    16:35:29,312 INFO [STDOUT] null

    why the master Id not set?

    thanks.

    ReplyDelete
  19. Hi Rab,

    As long as you added the println() statement after the detail.setMaster(getInstance()) call, then the Detail object should be able to return the master Id. Of course that assumes that the Master object has already been saved (persisted). If the Master has never been saved (so you created the Master, and immediately created a Detail without first saving the Master), then the Id will be null - persisting saves the new Master, and then Hibernate will populate the Id field. Is this the scenario you experienced?

    ReplyDelete
  20. Thanks, i understood now, i had to create master first and then create the detail. Before, i tried to save master and detail simultaneously, not saved the master first.

    My new problem :
    1. When i Click Edit in Detail table, and then click Cancel in the modal panel, row in detail table is deleted, i checked it was because action in command button call cancelNewDetail() method

    public void cancelNewDetail() {
    removeDetail();
    }

    public void removeDetail() {
    detailListFromMaster.remove(currentlySelectedDetail);
    entityManager.remove(currentlySelectedDetail);
    }

    it seems the method try to remote currenty selected detail.


    2. When i clicked New Detail.. i do not fill anything and then i clicked cancel or OK it add new blank row in the detail table.


    Please help me how to solve the problem.

    Thank you very much.

    ReplyDelete
  21. Hi Rab,

    Oops!

    The Cancel button should only be doing that if you are editing a new Detail instance. My XHTML originally had two different versions of the Cancel button. This has been lost somewhere along the way to posting on here.

    One version of the button should only be rendered when editing a new row, and it should call removeDetail() - because the user is canceling creation of the new Detail.

    A second version of the button should be rendered when you are editing an existing Detail row. This version should refresh the Detail instance from the database - which has the effect of canceling any changes the user has made.

    I am traveling now, but will be back in a couple of weeks. Then I'll fix/test the code and re-post an updated and correct version! Thanks for pointing this out.

    ReplyDelete
  22. When I just press tab from Master Id field in the Model panel, it goes to an error page.

    Caused by javax.faces.FacesException with message: "javax.el.PropertyNotFoundException: /MasterDet.xhtml @14,94 value="#{currentlySelectedDetail.master.id}": Target Unreachable, identifier 'currentlySelectedDetail' resolved to null"

    ReplyDelete