Skip to Content

Introduction

How to handle transactional operations in a “stateless world” like SAPUI5? This is a central question which should be addressed in every architecture design phase. One possible scenario I want to cover in this blog is the optimistic locking approach with ETag handling:

Within an update operation, the backend will not actual lock the data. Instead, the data will be checked before updating and it ensures, that the operation (CREATE, UPDATE) will be done on the latest version of the entity.

ETags in a request sequence

After a single read of an entity, the backend service transforms the so called ETag via HTTP response to the client. Once the UI requests an UPDATE, the ETag value must be send back to the server via the IF-Match Header.

Before executing any operation, the backend service compares the transmitted value with the current value of the database. If they match, the operation will be executed. Otherwise an HTTP Error (412 – Precondition failed) will be thrown. Due to this mechanism, the client gets a notification, that a “newer” version of the entity exists.

If it is acceptable for a given client to overwrite any version of the Entry in the server, then the value “*” may be used instead. In that case, there won’t be a precondition check in the backend.

Scenario

SAP Gateway and UI5 supports ETag handling. There are some great blog postings about how to configure your SAP Gateway to implement the precondition check.

In this tutorial, I want to focus on the UI handling for optimistic locking.

Our example is based on a demo Service (ZSEPM_C_SALESORD_UPDATE_ENTITY). The backend configuration is covered in this tutorial from Andre Fischer.

The good news is: If the backend configuration is done, there is no additional effort within the UI in terms of keeping the ETag or sending it back to the backend. The oData model takes care of this!

However… we should consider an appropriate user guidance: We want to notify the User if an update fails. Then there should be a notification, that a more recent version would be available. The user gets the opportunity to refresh to the most recent version or overwrite the backend entry with his changes.

UI5 Application

The first screen of our demo app displays a table of sales orders.

By selecting an item, there will be a navigation to the detail page, where the property “SalesOrderText” can be updated.

Our view is straight forward: A Simple form with some fields and a button to trigger the update event:

<f:SimpleForm id="SalesOrder" title="Sales Order {SalesOrder}" editable="true" Layout="ResponsiveGridLayout" singleContainerFullSize="false">
 <f:toolbar>
 ....
 </f:toolbar>
 <f:content>
    <Label text="Text"/>
    <Input value="{SalesOrder_Text}" editable="{detailView>/editMode}"/>
    <Label text="Net Amount"/>
    <Input value="{NetAmountInTransactionCurrency}" editable="false"/>
    <Label text="Gross Amount"/>
    <Input value="{GrossAmountInTransacCurrency}" editable="false"/>
 </f:content>
</f:SimpleForm>

I want to focus on the update operation within the detail page. The onSave function looks like this:

onSave: function(oEvent) {
	this._update();
},

_update: function() {
       var sPath = that.getView().getBindingContext().getPath();
       var oSalesOrder = that.getView().getBindingContext().getObject();
	
       this._updateSalesOrder(sPath, oSalesOrder)
		.then(function() {
		MessageToast.show("Success...");
		})
		.catch(function(oError) {
                  //Error handling...
       });
},

The function _updateSalesOrder returns a promise to retrieve the result of the oData update:

_updateSalesOrder: function(sPath, oSalesOrder, bForceUpdate) {
	var oDataModel = this.getView().getModel();
        return new Promise(function(resolve, reject) {
	        oDataModel.update(sPath, oPlan, {
			success: resolve,
			error: reject
		});
	});
 },

So far, nothing special…

But If the update fails due to the precondition error, we want a dialog (sap.m.dialog) appearing on the screen

.catch(function(oError) {
     // Error handling        
     //open Dialog if Precondition failed
	if (oError === "412") {
		that._openDialog();
	}
});

The appropriate coding for the dialog looks like this:

_openDialog: function() {
	var that = this;
	var dialog = new Dialog({
		title: 'Confirm',
		type: 'Message',
		content: new Text({
				text: 'There is a more recent version available. Do you want to refresh or overwrite the backend entry?'
			}),
		beginButton: new Button({
				text: 'Overwrite',
				press: function() {
					that._update(true);
					dialog.close();
					}
				}),
				endButton: new Button({
					text: 'Refresh',
					press: function() {
						//Refresh entity....
						dialog.close();
					}
				}),
				afterClose: function() {
					dialog.destroy();
				}
			});
			dialog.open();
		},

By submitting the change, our update function will be called again.

Here, we want to ignore the precondition check. Therefore we must add an additional parameter to our update function (bForceUpdate).

Once this value is true, we will set the parameter “eTag” of the oData model manually:

mParameters.eTag = “*”;

 

For this we also have to adjust _updateSalesOrder.  At the end these two functions look like this.

_update: function(bForceUpdate) {
	var that = this;
	var sPath = that.getView().getBindingContext().getPath();
	var oModel = that.getView().getModel();
	var oSalesOrder = that.getView().getBindingContext().getObject();

	this._updateSalesOrder(sPath, oSalesOrder, bForceUpdate)
			.then(function() {
				MessageToast.show("Success...");
			})
			.catch(function(oError) {
                // Error handling
                //open Dialog if Precondition failed
				if (oError === "412") {
					that._openDialog();
				}
			});
},

_updateSalesOrder: function(sPath, oSalesOrder, bForceUpdate) {
	var oDataModel = this.getView().getModel();
	var oPromise = new Promise(function(resolve, reject) {
	var mParameters = {
		success: resolve,
		error: function(oError) {
		        reject(oError.statusCode);
			}
		};
		if (bForceUpdate) {
			mParameters.eTag = "*";
		}

	oDataModel.update(sPath, oSalesOrder, mParameters);
	});
return oPromise;
},

Conclusion

Etag handling is very well supported in SAP Gateway and UI5. The ETag value is cached within the entity of the oData model. It will be handed over to the backend with every request. The Gateway framework takes care of the whole precondition checks.

With this “out of the box” support, there are just some small UI adoptions necessary in order to setup a complete optimistic locking approach for transactional apps.

To report this post you need to login first.

3 Comments

You must be Logged on to comment or reply to a post.

  1. Matt Harding

    Nice post for an important topic (though risky in many scenarios to let a user overwrite a recent update, even if they made the update themselves)

    But I have a quick question that I still don’t know the official answer to about one of your comments in the code…e.g.

    //Refresh entity….

    Is there a nice way to implement this without refreshing everything in your odata model (including drop downs)?

    All ways I’ve discovered are a little hacky in my opinion because of the smarts behind odata caching entities. e.g. a read will not necessarily retrieve from the backend if there is a local cached version already. I had to hack it a little to ensure in my app, that when switching objects, the eTag was getting updated if they had not navigated to it for a long time.

    Cheers,

    Matt

    (0) 
    1. Maximilian Rupp Post author

      Hi Matt!
      Did you check the refresh method of the ODataModel?
      This will check all bindings and updates the controls if data has been changed. There is also a ForceUpdate parameter provided.

      (0) 
      1. Matt Harding

        Hi Max,

        I did try these and this is what I was referring to above as refresh everything. In other words, these are the brute force refresh everything methods which even remove your existing drop down entity sets that may have already loaded…e.g. Use this and be careful as it breaks existing bindings against comboboxes as an example.

        Maybe Odata v4 will fix everything 😉

        Cheers,

        Matt

        (0) 

Leave a Reply