Tuesday 20 February 2007

JDeveloper and the art of the rollback

It's tempting for beginner ADF programmers to map the Cancel button to the Rollback operation on an edit page within an ADF BC/Faces application, to lose the current changes the user has made within the edit page. However issuing a rollback in a JDeveloper application using ADF BC can have a gotcha for beginner programmers, or those coming from a traditional Forms background.

A side-effect of issuing a rollback is that any open iterators have their current row pointer reset to the first record in the record set. This presents a confusing situation to the user on navigating back to (for example) a page showing a read-only table with a record selector, as the table will be "magically" reset to the first record in the table, which could in fact be a different row set in the table.

The following describes the steps in implementing a method for overriding the side-effects of the rollback, and an alternative method from calling the Rollback at all. Which you use is your choice based around the functionality you require.

Restore current row after rollback

The first method is based on Steve Muench's undocumented example Restore Current Row After Rollback #68. This method changes the manner in which the ADF BC framework handles rollbacks for specified View Objects. It can be applied to all View Objects or a selective set as implemented by the programmer.

The changes for this solution lie in both your ADF BC Model project and your ADF Faces ViewController project. Follow these steps to implement:

ADF BC Model Project

1) Create a new class in your model layer called CustomViewObjectImpl similar to the recommendations in the Advanced Business Components Techniques section of the ADF Guide for Forms/4GL Programmers. Copy the same Java class code from Steve Muench's example into this new class, specifically the beforeRollback and afterRollback methods. Change the package declaration to where ever you've placed the new class.

2) In your existing ApplicationModuleImpl, add this routine:

protected void prepareSession(Session session) {
super.prepareSession(session);
getDBTransaction().setClearCacheOnRollback(false);
}

3) For all View Objects where you want to avoid the rollback side effect, you need to extend their ViewObjectImpl custom classes as follows:

public class myViewObjectImpl extends CustomViewObjectImpl

....and import the CustomViewObjectImpl.

4) In addition in each custom ViewObjectImpl class, and the following routine:

protected void create() {
super.create();
setManageRowsByKey(true);
}

ADF Faces ViewController Project

5) In *each* web page where you have a rollback button, instead of calling the Rollback actionBinding defined within the pageDef, we want the button to call the following code:

public void executeRollbackActionAfterDisablingExecuteOnRollback() {
FacesContext fc = FacesContext.getCurrentInstance();
ValueBinding vb = fc.getApplication().createValueBinding("#{bindings}");
DCBindingContainer bindings = (DCBindingContainer)vb.getValue(fc);
if (bindings != null) {
bindings.setExecuteOnRollback(false);
OperationBinding ob = bindings.getOperationBinding("Rollback");
if (ob != null) {
ob.execute();
} else {
throw new RuntimeException("Binding container has no 'Rollback' action binding");
}
}
}

Note that this solution still requires the pageDef for each page with the rollback button includes the "Rollback" binding. If you deleted the rollback button from the JSF page, JDeveloper will have automatically deleted the pageDef Rollback binding, so you will need to recreate it if need be.

6) Rather than duplicating the above code in each backing bean, it would better be placed and defined once in a utility class. In Steve's example he creates a class RollbackHelperBase, wrapping the above code in the following method spec:

public void executeRollbackActionAfterDisablingExecuteOnRollback() { ... }

7) The button then makes a call to its associated backing bean method onRollback on as follows:

<af:commandButton text="Rollback" immediate="true" action="#{TestPage.onRollback}">

8) The backing bean should extend the RollbackHelperBase:

public class myBackingBean extends RollbackHelperBase { .... }

9) and the onRollback method makes a call to the parent's method executeRollbackActionAfterDisablingExecuteOnRollback as follows:

public String onRollback() {
executeRollbackActionAfterDisablingExecuteOnRollback();
return "navigateToWhereEver";
}

Now when you run your webpage and test the rollback button, for any web page iterator that has been extended with the custom View Object class, the iterator row currency will not be reset.

Steve's solution has the advantage that it a generic enough that it can be applied across all ViewObjects and their associated iterators. The disadvantage of this approach is it requires more code, and to be effective across your app you need to extend each ADF BC ViewObjectImpl to the custom CustomViewObjectImpl class, as well as each rollback button with ADF Faces calling the executeRollbackActionAfterDisablingExecuteOnRollback functionality. The code changes however can be minimised through the use of a customer ADF BC framework.

Drop the current row changes

An alternative approach to Steve's solution is to not change the rollback functionality, but instead just lose the changes to the current row the user is editing. This can be done on the JSF side by retrieving the current row from the iterator, and instructing the row to drop any changes since the beginning of the transaction, including dropping any new rows.

To implement this solution, follow these steps:

1) In your edit page, create a new commandButton whose action attribute makes a call to the following backing bean code:

public String dropChangesAndReturn() {
FacesContext fc = FacesContext.getCurrentInstance();
ValueBinding vb = fc.getApplication().createValueBinding("#{bindings}");
DCBindingContainer bc = (DCBindingContainer)vb.getValue(fc);

DCIteratorBinding iterator = bc.findIteratorBinding("EmployeesView1Iterator");
ViewObject vo = iterator.getViewObject();
Row row = vo.getCurrentRow();

if (row != null) {
row.refresh(row.REFRESH_UNDO_CHANGES | row.REFRESH_FORGET_NEW_ROWS);
iterator.getDataControl().commitTransaction();
}
return "SomeNavigationRule";
}

2) For the commandButton, set it's immediate attribute to true.

That's it. A smaller solution but dependent on you knowing the iterator name, and adding to all edit pages where you want to drop the row.

Thanks and final comments

Thanks to Steve Muench for giving permission to document his undocumented example # 68, and if I remember correctly to Frank Nimphius and Didier Laurent for their original assistance on the second solution on OTN and via Support way back in 2004 when I was a wee JDeveloper schnapper.

As there is a large amount of text above I've surely made an error at some point. If you find an error please include a comment and any fix and I'll append the above post to the benefit of other readers - thanks!

9 comments:

Anonymous said...

Hi,

Can you post the code of Steve Muench's example? I can't seem to track it down.

Thanks,
Mel

Chris Muir said...

Hi Mel

Sorry, I'm not sure what you mean? The blog post has a link to Steve's web-page that includes the undocumented example # 68. Are you referring to something else?

Regards,

CM.

S. Rothen said...

Hi Chris!
You saved my day (maybe even my week). For weeks I have wondered why my web application generates many unnecessary sessions. Now I know: with each click on a rollback button the current user was lost and then a new session was generated.
Thanks a lot
S. Rothen

Lucas Jellema said...

Hi Chris,

Another very useful post. I sort of completely overlooked this effect of Rollback! It hit me with a vengeance. Thanks to your post I not only understand what is happening but see a way out as well. Thanks.

Lucas

Anonymous said...

Can anyone comment on when the default behavior would ever make sense? It seems to me that by default, rollback doesn't work as expected with the most common use case - i.e. Cancel.

Chris Muir said...

Fair comment Richard and one I've wondered about.

My guess is that the ADF designers originally thought that they should take the vanilla implementation of the rollback in the framework. ie. do the very minimum to rollback then let ADF programmers take care of what additional functionality they wanted. At that time the ADF designers didn't know what strange and wonderful things customers would come up with.

However (and remember my opinion here is based on a guess) it turns out several years later that it was a poor choice especially for beginner ADF programmers as it's not intuitive, and they have a catch 22, because if the framework was changed now to include this feature it would not be backwards compatible with a lot of the implemented solutions out there.

So as a result, something we have to live with.

As I said, just a guess on my part.

CM.

Sean said...

Hi Chris,

Just stumbled upon this page while searching for a solution to the Rollback functionality.

I've a similar problem except I need to undo the changes caused by a removeRowWithKey. Is it possible to recover those rows without using a Rollback?

Thanks for your help!

Sean

Chris Muir said...

Hi Azzur

Sorry for the delay. Is this what you're looking for?:

http://jobinesh.blogspot.com/2011/05/soft-deletion-of-rows.html

CM.

Unknown said...

Hi Chris

nice article, thanks.

First I implemented workaround 2 and it worked ok. However I had also delete+commit button and if operation failed due to some constrains in db I had to do either rollback or manual restoration of the row. Because I couldn't find any way to do the latter (I'm not experienced) I switched to workaround 1. And it worked OK except for 1 tiny problem with delete+commit
operation - when it fails the rollback sets current row in the table to the next one.

Regards,
t