javavalidationstruts2crudstruts2-interceptors

Performing CRUD operations in an action class along with the prepare() method in Struts2


Assuming the following action class.

@Namespace("/admin_side")
@ResultPath("/WEB-INF/content")
@ParentPackage(value="struts-default")
public final class TestAction extends ActionSupport implements Serializable, ValidationAware, Preparable
{
    private Long currentPage=1L;

    //This method is called on page load. 
    //Validation is skipped, location is set to a valid action, not "redirectAction", the request is dispatched. 
    //It is mapped to an action of <s:form>.
    public String load() throws Exception
    {
        //Nothing to see here. Leave it empty.
        return ActionSupport.SUCCESS;
    }

    //Assuming necessary validators and action whose result type is set to "redirectAction". 
    //It is mapped to an action of <s:submit>.
    public String insert()
    {
        //Do something to either add or update data in a model based on a conditional check.
        return ActionSupport.SUCCESS;
    }

    //Assuming necessary validators and action whose loction is set to a valid action. not "redirectAction". 
    //The request is dispatched/forwarded. 
    //It is mapped to an action of <s:a>.
    public String edit()
    {
        //Nothing to see here. Leave it empty.
        return ActionSupport.SUCCESS;
    }

    //Assuming necessary validators and action whose result type is set to "redirectAction". 
    //It is mapped to an action of <s:submit>.
    public String delete()
    {
        //Do something to delete data from a model.
        return ActionSupport.SUCCESS;
    }

    @Override
    public void prepare() throws Exception
    {
        list= service.getList((int)(currentPage-1)*pageSize, pageSize);
    }
}

I have excuded annotations and other things to avoid code noise. The actions mapped to these methods use the paramsPrepareParamsStack interceptor.

Here, when an action associated with the insert() method, for example is triggered (it done by <s:submit>), the result will be redirect action. Accordingly, a new instance of the action class will be created which causes the load() method to be executed which in turn causes the prepare() method to be executed once again. The same thing will happen while updating and deleting.

The prepare() method is first executed as soon as an action associated with <s:submit> (or <s:link>) is triggered and then again when the request is redirected (this can be understood because redirection of a request results in creating a new instance of the action class which causes the action associated with the load() method to be executed and prepare() is executed once on every action).

The only line inside the prepare() method has costly operations. To prevent the getList() method from being executed twice, I do some conditional checks like as follows.

@Override
public void prepare() throws Exception
{
    String actionName = ActionContext.getContext().getName();
    if(actionName.equals("load")||actionName.equals("edit"))
    {
        list= service.getList((int)(currentPage-1)*pageSize, pageSize);
    }
}

There could be more conditional checks and complex code in this method.

Doing so is still insufficient. The list will not be initialized, if any validation/conversion error(s) occur(s) because of the condition. None of hasErrors(), hasActionErrors() and hasFieldErrors() will be evaluated to true in the prepare() method after any errors. This requires the list to be loaded inside the validate() method like as follows.

@Override
public void validate()
{
    if(hasErrors())
    {
        list= service.getList((int)(currentPage-1)*pageSize, pageSize);
    }
}

This now fulfills the requirements but looks very ugly to have such conditional checks and cannot be considered to be a good approach.

Is there a better way to have some mechanism that guarantees that the retrieval of the list from the database happens only once after a request to perform operations like insert, update, delete is made?

It should be independent of how many actions are executed behind the scene per request. The list should be retrieved just before the completion of the request only once event though there are some conversion/validation errors.

None of the @Before, @BeforeResult, @After annotations seem to work to get around this situation.


Using such code in the validate() method which is meant to retrieve/initialize a list does not seem to be a good practice.

I expect that there is a way of getting this list after CRUD operations. Since getting this list from the database is costly, this list should be initialized only once after every operation (insert, edit, update, delete) is finished.


Solution

  • Doing in prepare() method which is executed for every action, heavy operation such as populating lists is not a good way. Because not all actions need to perform such operation.

    But when used with validation, no action is executed at all. If validation errors are occurred, the INPUT result is returned. This result is a dispatcher result in your usecase and it requires the lists to be populated before this result is executed.

    You are doing manual checks in the validate method for validation errors, before the validate method ends and INPUT result is returned, then you repopulate the lists.

    This logic is already implemented by the workflow interceptor which is a member of defaultStack. You can configure this interceptor to invoke a method when validation errors occurred. In this method you can repopulate lists.

    public String input() throws Exception {
      list = service.getList((int)(currentPage-1)*pageSize, pageSize);
      return INPUT;
    }
    

    Now you should configure this method to use with workflow interceptor via placing annotation to your crud methods insert(), delete().

    @InputConfig(methodName="input")
    

    In the edit() and load() methods you can just call input() manually if the action is returning a dispatcher result, possibly the same location as INPUT result.