This document continues the series A Programming Model for Business Applications. If you havn´t read the first parts in the series, I recommend that you read the first three parts:
In this part, I want to discuss the implementation of the service adaptation read services on the server side (service adaptation view and OData definition, xsodata file) and we will have a look at the OData implementation in the SAP UI5 UI.
As introduced in part 1 of this series, we would like to discuss the services for two UIs:
- A very simple special-purpose UI that allows the creation of a new private prospect (for example as a mobile UI as a part of a fair application) with access restriction to sales territories (Figure 3 in part 1).
- A UI for a sales representative for managing sales orders of “his/her” accounts and territories (SAP UI5 application, screenshot see Figure 4 in part 1)
Service Adaptation View
Let us move to the read service in the service adaptation layer now. Let us start with the prospect UI service (see Figure 4 in part 1). The requirements to the backend service are:
- For read operations: Join of BusinessPartner, Customer, AddressInformation, and Address into a flat structure for the UI, using the following filter criteria:
- BusinessPartner.Type = Person (the UI supports private accounts only)
- Customer.IsProspect = true (the UI supports prospect only)
- BusinessPartner.AddressInformation.IsDefault = true (for the main UI. For the “EnterAdditionalAddress” also a non-default ship-to address can be managed)
- BusinessPartner.AddressInformation.Validity: for read operation, select the address that is valid today
- For write operations (will be discussed in the next document): “decoding” of the flat UI structure into CUD operations of the BusinessPartner and Address BO, including the defaulting of the attributes that are used as filter for the UI (BusinessPartner.Type = Person, and so on).
Again CDS limitations in SPS8 prevent us implementing the view with CDS. So, we define the view with the following select statement as Prospect.hdbview.
ST.“DescriptionText” as “Status.DescriptionText”,
ADR.“DefaultEmailURI” as “EmailURI”,
ADR.“DefaultPostalAddress.CityName” as “CityName”
from <schema>.<path>::bo.businesspartner.BusinessPartner” as BP
inner join <schema>.<path>::bo.businesspartner.BusinessPartner$C” as BPC
on BP.ID_ = BPC.ID_
inner join <schema>.<path>::bo.businesspartner.Customer” as CUS
on BP.ID_ = CUS.“Parent_ID_”
left outer join <schema>.<path>::bo.businesspartner.LifeCycleStatus” as ST
on BP.“Status.Code” = ST.“Code”
left outer join <schema>.<path>::bo.businesspartner.AddressInformation” as AI
on BP.ID_ = AI.“Parent_ID_” and AI.“DefaultIndicator” = 1
left outer join <schema>.<path>::bo.address.Address” as ADR
on AI.“Address.ID_” = ADR.“ID_”
where CUS.“ProspectIndicator” = 1
and BP.ID_ in (select “Customer.ID_” from <schema>.<path>::bo.salesterritorymgmt.Customer” )
- To increase the readability, not all elements are listed in the select clause
- Calculated elements (here Formatted Name) are taken from the $C view that was introduced before
- The description of the codes (here Status) is read via join to the code list tables
- The join on the Address Information filters to the default address
- The where clause filters to prospects
- The where clause implements the access restriction to prospect that belong to the sales territory the user belongs to.
- I implemented the view in a separate UI repository folder.
Based on this view, we can now define the OData definition (Prospect.sxodata) as:
In the browser, you can now consume the OData service using the following URL:
Here is an example
FormattedName: “Frieda Friday”,
On the UI (Figure 3 in part 1) you can see a link, Enter Ship-To Address. The idea behind this link is that you can optionally maintain a dedicated ship-to address. As
the next step, I will implement the backend service for this secondary address.
As first step, I create a second view called ShipToAddress.hdbview, which is basically a copy of the Prospect view, with one exception: instead of the
filter AI.“DefaultIndicator” = 1 , we define the filter as AI.“AddressUsage” = ‘SHIP_TO’.
As a second step, I extend the OData definition by the ShipToAddress and an association from Prospect to ShipToAddress:
navigates ( “ToShipToAddress” as “ShipToAddress” );
<schema>.<path>::ShipToAddress” as “ShipToAddress”
principal “Prospect”(“ID_”) multiplicity “1”
dependent “ShipToAddress”(“ID_”) multiplicity “1”;
As a result, I can read the secondary address (if available) by the following OData call:
Based on the same principle, the implementation of the Sales Order OData service is straight forward: It consists of the Sales Order view, an Item view and the OData definition:
navigates ( “ToItem” as “Item”,
“ToSalesOrder$P” as “SalesOrder$P” );
<schema>.<UIpath>.Item” as “Item”;
<schema>.<path>.SalesOrder$P” as “SalesOrder$P”;
principal “SalesOrder”(“ID_”) multiplicity “1”
dependent “Item”(“Parent_ID_”) multiplicity “*”;
association “ToSalesOrder$P ”
principal “SalesOrder”(“ID_”) multiplicity “1”
dependent “SalesOrder$P”(“ID_”) multiplicity “1”;
- I implemented the views in a separate UI repository folder (<UIpath>), so they can have the same short name (SalesOrder, Item) as the entities.
- I have defined the association and navigation property to the properties view (SalesOrder$P). So In the UI I can read the properties directly with the date to avoid unnecessary roundtrips.
Finally, let us have a look at the OData implementation in the SAP UI5 UI. It consists of the following code lines in the view and controller implementation.
1) Set the model (e.g. in the view implementation, (createContent method of the main view)
var oModel = new sap.ui.model.odata.ODataModel(“<path>/SalesOrderView.xsodata”, true);
Discussion: Setting the count mode to false is helpful to avoid unnecessary roundtrips (unless you really need it).
2) Attach the model to a list (in the respective view implementation):
var oMain = sap.ui.getCore().byId(“<id of the main view>”);
var oModel = oMain.getModel();
3) Implement the controller for filling the list of orders (e.g. in the onBeforeShow method):
oMasterList.bindItems(“/SalesOrder”, oList._oItemTemplate, oMasterList._oItemSorter);
To read the properties with the data, replace “/SalesOrder” by “/SalesOrder?$expand=SalesOrder$P”. After this change, the OData call will expand the association to the properties view and read them into the OData model.
4) Implement the controller for filling the list of items (e.g. in the onSelectionChange method):
var selectedItem = oMasterList.getSelectedItem();
var bindingContext = selectedItem.getBindingContext();
var itemContext = bindingContext.sPath;
oItemList.bindItems(itemContext + “/Item”, oList._oItemTemplate, oItemList._oItemSorter);
Discussion: itemContext contains “/SalesOrder(<key>)”., itemContext + “/Item” is the path to load the Items following the Item association in the
The OData services are not yet ready for write services, because the views behind the OData services span multiple entities, and so the OData service cannot write to the database tables. If you try to write data with a POST or PUT request, the service will answer with an error message: “Service exception: data manipulation operation not legal on this view.“ In the next blog, I will explain how to implement the write services: A Programming Model for Business Applications (5) Write Service: Business Object, Service Adaptation, and UI.