Tuesday 22 January 2013

Spring Roo Updating two entities from one View

Following on from the last post about Spring Roo, this post looks at how to customize the views and controller to update multiple entities from a single view.

Using the web mvc add-on Spring Roo generates views with crud functionality for each entity. For a given entity, Roo will typically generate views for create, show, list and update that map to methods on the generated Spring controller. By specifying attributes to the RooWebScaffold annotation, it is possible to control which views get generated.

However, there are some situations where it may be desirable to allow updating multiple entities (parent-child) from within the same view, perhaps to provide a better user experience. For example, consider a domain model with Business and User entities having a one-to-one relationship to an Address entity (managed from the Business/User). When creating a new Business or User entity, it would provide a better user experience to allow entering all the Business or User details and Address details from a single view, rather than having to create the Address from a separate view and then select the address identifier from a drop-down list within the create Business/User view (as would be the default behaviour with Roo generation).

To achieve this behaviour, the create.jspx view can be augmented with additional fields to enter the address details. Note the use of the dot-notation to navigate to the nested property of the address object.


    <form:create id="fc_com_changesoft_samples_domain_Business" modelAttribute="business" path="/businesses" render="${empty dependencies}" z="0qN2h7loptVj50JEP4Htm9WcQ2U=">
        <field:select field="businessType" id="c_com_changesoft_samples_domain_Business_businessType" items="${businesstypes}" path="businesstypes" z="lbHfbOekwe5M/bFdLTQsGY43NTs="/>
        <field:input field="businessName" id="c_com_changesoft_samples_domain_Business_businessName" z="H/Qzpi7gDgY45Ag3tLqLZiYL9wY="/>
        <field:input field="registrationNumber" id="c_com_changesoft_samples_domain_Business_registrationNumber" z="v0EWCe/zpM89BvaLz1O6+1p1ZEs="/>
        <field:input field="businessDescription" id="c_com_changesoft_samples_domain_Business_description" z="oly643Jm218C3sMXDHNnxbCaKC8="/>
        <field:input field="address.addressLine" id="c_com_changesoft_samples_domain_Address_addressLine" z="nDT/wm1FdsgfzEXLv9zJkmZybmg="/>
        <field:select field="address.addressType" id="c_com_changesoft_samples_domain_Address_addressType" items="${addresstypes}" path="addresstypes" z="aYI9UiQIlRB+F0Bwu93/eo4ZFAc="/>
        <field:input field="address.city" id="c_com_changesoft_samples_domain_Address_city" z="FVwmWn28G7JtqIfUIvAdZORMVkM="/>
        <field:input field="address.postcode" id="c_com_changesoft_samples_domain_Address_postcode" z="eezB9GBw9FiIb+ISiJosM7zXbi4="/>
    </form:create>


This results in the following rendered jsp view.

When the form is submitted, it correctly persists a new Business with a new Address entity (due to the address attribute of Business having cascading behaviour).

The update.jspx view requires slightly more work. When the update form of an entity is submitted, the id and version attributes are also present as hidden form fields and sent as request parameters to identify and retrieve the detached entity and update it. Therefore when updating 2 entities from a single view, it is also necessary to encapsulate the id and version attributes of the Address entity as hidden form fields; and subsequently submit them along with the Business attributes. The jsp tags that are provided by the web mvc add-on do not cater for hidden form fields. Therefore to fulfil this requirement it was necessary to create a custom tag that would generate an html hidden form field that could then be used within the update view jsp.


  <c:set value="hidden" var="type" />


  <c:choose>
    <c:when test="${disableFormBinding}">
      <input id="_${sec_field}_id" name="${sec_field}" type="${fn:escapeXml(type)}" />
    </c:when>
    <c:otherwise>
      <form:hidden id="_${sec_field}_id" path="${sec_field}" />
      <br />
      <form:errors cssClass="errors" id="_${sec_field}_error_id" path="${sec_field}" />
    </c:otherwise>
  </c:choose>



With the new hidden.tagx custom tag file, the update view for the Business entity is adjusted to mark any required fields as hidden:


    <field:hidden field="address.id" id="c_com_changesoft_samples_domain_Address_Id" z="user-managed"/>
    <field:hidden field="address.version" id="c_com_changesoft_samples_domain_Address_Version" z="user-managed"/>


Using the new hidden field tags, the update view renders as follows:

This view allows updating both the Business and Address entities by correctly sending the id and version properties for each entity as hidden form fields, shown below in the HTML source.

<div style="display: none;" id="_c_com_changesoft_samples_domain_Address_Id_id">
<input id="_address.id_id" name="address.id" type="hidden" value="1"/>
<br />
</div>
<div style="display: none;" id="_c_com_changesoft_samples_domain_Address_Version_id">
<input id="_address.version_id" name="address.version" type="hidden" value="2"/>
<br />
</div>
<div id="_c_com_changesoft_samples_domain_Address_addressLine_id">
<label for="_address.addressLine_id">
Address Line
:
</label>
<input id="_address.addressLine_id" name="address.addressLine" type="text" value="119 Middlesex Street"/>

This works quite well without requiring any custom code within the controller.

8 comments:

  1. nice blog..its helpful for me
    Did you create two controller for business and address

    ReplyDelete
  2. If I remember correctly, I only had one controller that handled the request. The Business bean submitted will contain the Address bean populated from the request.

    ReplyDelete
  3. if I have two entities that have relation to each other, how the relation is saved if the information is sent in the same form?

    ReplyDelete
    Replies
    1. Hi Paul, the information sent in the same form will be used to persist two entities due to the dot notation used to navigate to the related entity.

      Delete
    2. and, is there no way to establish relation between these two entities? ie if I add a product type and product, to which I wish to assign the type of product that is created in the same form, how could I do?

      Delete
    3. Hi, I'm not sure what you're trying to do but if its a matter of associating a ProductType to a Product, then I would expect the ProductType to be an enum or another Entity. In that case you can have the ProductType appear as a select element in the UI and the id of the ProductType will be set as an attribute of the Product bean when the form is submitted to create the entity.

      Delete
  4. Hi!
    It's very useful your example but i can't understand how are using the "address" field in create form with modelAttribute="business" ?

    It's possibme to post a full example with *_Roo_JavaBean.aj and *_Roo_Controller.aj ?

    In my case i have one-to-many relationship between Person and Contact.
    When i try to use contacts properties on Person create form i get the following error :
    ERROR org.springframework.web.servlet.tags.form.InputTag - Invalid property 'contact' of bean class [gr.manousos.domain.Person]: Bean property 'contact' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
    org.springframework.beans.NotReadablePropertyException: Invalid property 'contact' of bean class [gr.manousos.domain.Person]: Bean propert

    ReplyDelete
  5. Hi, javaguru. Have you done any modification in the Business controller to make this work? Because I have implemented something very similar but I get a nasty exception:

    org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing: es.cesga.cloudpyme2.openinnovation.Solicitud.solicitudEs -> es.cesga.cloudpyme2.openinnovation.SolicitudEs; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing: es.cesga.cloudpyme2.openinnovation.Solicitud.solicitudEs -> es.cesga.cloudpyme2.openinnovation.SolicitudEs

    Thank you for your help.

    ReplyDelete