Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
ThomasSchneider
Product and Topic Expert
Product and Topic Expert
0 Kudos

This post continues the series A Programming Model for Business Applications. The first four parts are:

A Programming Model for Business Applications (1): Assumptions, Building Blocks, and Example App

A Programming Model for Business Applications (2): Implementing the Entity Model with CDS

A Programming Model for Business Applications (3): Calculated Elements and Properties

A Programming Model for Business Applications (4) Service Adaptation and UI (Read Service)

In this part, I want to discuss the implementation of the write channel. It consists of (see also figure 1 in part 1):

  • OData definition (xsodata)
  • JavaScript service adaptation implementation (xsjslib)
  • JavaScript business object write service implementation (xsjslib)

Finally, we will have a look at the implementation in the SAP UI5 UI.

As mentioned 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)

In this document, I will discuss the implementation of the Create services for the Prospect UI in detail.

OData Service

As a first step, refer to the OData definition of the Prospect OData service in part 4 of this series. Ignoring the association and navigation to the ship-to address it reads:

service {

  <schema>.<UIpath>::Prospect"

   as "Prospect"

   key ("ID_");

}

As mentioned in part 4, this service is not ready for writing, because the underlying view (“Prospect”) is a join of more than one table. To enable the service for writing, we have to define a service implementation, in the following example, we add the implementation for create.

service {

"D022899"."<UI path>::Prospect"

   as "Prospect"

   key ("ID_")

   create using "<UI path>:ProspectWriteExit.xsjslib::create"

   ;

}

Discussion:

  1. We are omitting the ship-to-address association (see part 4)
  2. Before activating the xsodata-File, we have to create the create service, this is a xsjslib file <UI path>:ProspectWriteExit.xsjslib that contains a function called create. Otherwise HANA studio will raise an activation error.

Now, you can call the following POST service with URL http://<host>:<port>/<path>/Prospect.xsodata/Prospect and the following JSON body:

{

"ID_": "-1",

"ID": "C001",

"CategoryCode": "2",

"Person.GivenName": "Thomas",

"EmailURI": "thomas.schneider@Sap.com",

"CityName": "Heidelberg"

}

Service Adaptation Implementation

Note: I have not put any effort in streamlining the JavaScript code in the following examples. The code is not meant as "nice code" but it sould be as simple as possible and make the basic tasks transparent. In a real project there is much room for refactoring.

As a next step, we have to implement the create service. I have implemented the following skeleton, which shows the basic tasks of the service adaptation layer in the write case:

var BO = $.import("<path>", "BO");

function create(p) {

var conn = p.connection, pstmt, sql, rs;

var address = {};

var addressInformation = {};

var businessPartner = {};

var customer = {};

var addressKey = 0, bpKey = 0, dummyKey = 0;

var failed = false;

//

// get the input

//

sql = 'select "Person.GivenName", "Person.FamilyName", "EmailURI", "CityName" from "' +  p.afterTableName +'"';

pstmt = conn.prepareStatement(sql);

rs = pstmt.executeQuery();

  if (!rs.next()) {

   failed = true;

} else {

    if (rs.getString(1) !== null) {

     businessPartner["Person.GivenName"] = rs.getString(1);

   }

    if (rs.getString(4) !== null) {

      businessPartner["Person.FamilyName"] = rs.getString(2);

     }

    if (rs.getString(2) !== null) {

     address.DefaultEmailURI = rs.getString(3);

   }

    if (rs.getString(3) !== null) {

     address["DefaultPostalAddress.CityName"] = rs.getString(4);

    }

}

  rs.close();

pstmt.close();

  //

  // create the BO entities

  //

  if (failed === false ) {

    // address

    //if (address.length > 0) { // TODO: this is wrong because of the "technical" properties

     addressKey = BO.create(conn, 'bo.address.Address', address); 

    //}

    // businessPartner

   businessPartner.CategoryCode = '1'; // Person

   bpKey = BO.create(conn, 'bo.businesspartner.BusinessPartner', businessPartner);

    if (bpKey > 0) {

     customer.ProspectIndicator = 1;

     customer["Parent_ID_"] = bpKey;

     dummyKey = BO.create(conn, 'bo.businesspartner.Customer', customer);

      if (addressKey > 0) {

       addressInformation["Parent_ID_"] = bpKey;

       addressInformation["Address.ID_"] = addressKey;

       addressInformation.DefaultIndicator = 1;

       dummyKey = BO.create(conn, 'bo.businesspartner.AddressInformation', addressInformation);

     }  

   }

}

   //

  // update of OData table for response

  //

  if (failed === false && bpKey > 0) {

   sql = 'update "' + p.afterTableName + '" set ID_ = ' + bpKey;

   pstmt = conn.prepareStatement(sql);

   pstmt.executeUpdate();

       pstmt.close();

}

Discussion:

  1. The coding consists of three parts.
  2. The first part (“get the input”) reads the input data from the temporary table that is provided by the OData handle (afterTableName) and copies the data into maps for the involved entities: BusinessPartner, Customer, Address. Only a small fields are implemented to keep the coding readable.
    The second part (“create the BO entities”) creates the involved entities. Please note that the service adaptation implements the create sequence of the entities and sets the filters. In other words, it “inverts” the logic that is implemented in the service adaptation view (see part 4 of the series).
  3. The third part (“update of OData table for response”) updates the table of the OData framework. Please note that we only update the key here. This is not sufficient. At this point, we should select the complete data from the service adaptation view (see part 4 of the series) to read the calculated elements, and update their values in the OData framework table. This step is omitted here.
  4. Error handling is not implemented.

Business Object Implementation
The creation of the entity instance is delegated to the create method of the business object service, that is implemented in <path>.bos.xsjslib. To make the example simple, I have implemented only one create service for all entities in my example.

function create(conn, bo, elements) {

var pstmt, sql, rs;

var failed = false;

var IDkey = 0;

var elementIndex = 0;

var element;

  //

// BO specific code goes here

//

if (bo === 'bo.businesspartner.BusinessPartner') {

   // Validation "CategoryCode is mandatory"

   if (elements.CategoryCode === undefined) {

     failed = true;

     // TODO message handling, handling of "failed" through the stack

   }

    // Determination "Set Status to default"

    if (elements["Status.Code"] === undefined) {

     elements["Status.Code"] = '1'// In Preparation

   }

}

//

// get the IDkey

//

sql = 'select top 1 "<schema>"."<path>::Sequence".nextval from dummy';

pstmt = conn.prepareStatement(sql);

rs = pstmt.executeQuery();

if (!rs.next()) {

   failed = true;

} else {

   if (rs.getString(1) !== null) {

     IDkey = rs.getString(1);

   } else {

     failed = true;

   }

}

rs.close();

pstmt.close();

  if (bo === 'bo.businesspartner.BusinessPartner') {

elements.ID = IDkey.toString();

}

//

// insert statement

//

if (failed === false ) {

  sql = 'insertinto "<schema>"."<path>::';

   sql += bo + '"';       // Ex.:'bo.address.Address"';

   sql += '(ID_';

    for (element in elements) {

      if (elements.hasOwnProperty(element))

       sql += ',"' + element + '"';

     }

   }

   sql += ') values(';

   sql += '?'; // IDKey

   for (element in elements) {

     if (elements.hasOwnProperty(element))

       sql += ',?';

     }

   }

   sql += ')';

   pstmt = conn.prepareStatement(sql);

   pstmt.setBigInt(1, IDkey);

   elementIndex = 2;

    for (element in elements) {

      if (elements.hasOwnProperty(element))

       if (element === 'Parent_ID_' || element === "Address.ID_"

          || element === 'DefaultIndicator' || element === 'ProspectIndicator') {  // TODO: workaround for Parent ID -> solution for non-string values
needed!

         pstmt.setInt(elementIndex,parseInt(elements[element], 10));

       } else {

         pstmt.setString(elementIndex, elements[element]);

       }

       elementIndex++;

     }

   }

   pstmt.executeUpdate();

   pstmt.close();

}

  if (failed === true) {IDkey = -1;}

    return IDkey;

}

Discussion:

  1. The coding consists of three parts.
  2. The first part (“BO specific code goes here”) contains the business logic. Based on the input data, you can implement validations and determinations here. I have added two simple examples here.
  3. The second part (“get the IDkey”) implements the handling of the key (technical and semantic ID). I have implemented this using a database sequence (in my example: one sequence for all entities).
  4. The third part (“insert statement”) implements the insert statement using a prepared statement. Unfortunately, the implementation works for fields of type sting only. As you can see I have hard-coded some exceptions for Integer values. This part of the coding has to be generalized, for example by implementing a map that contains the type of the elements of an entity and using the correct method for setting the values.
  5. Error handling is not implemented.

To call the OData service from your SAP UI5 UI, you can use the create method of the SAP UI5 OData model implementation:

oModel.create(

  contextString,

  oData,

  null,              // context

  function(oData, response) {

       alert("successfully changed");

  },

  function(oError) {

      alert("Error");

  },

  false,                    // merge

  "*"                        // eTag

);

Discussion:

  1. The context of the OData service (contextString) is “Prospect”.
  2. The body of the request (oData) is the JSON object that contains the data.

Update Implementation

I hope that my example implementation illustrates the basic tasks of the service adaptation and the business object logic. Here are some comments that you have to consider when implementing the update service:

  1. In the OData service, you have to include an update clause (similar to the create clause).
  2. In the service adaptation update implementation, you have to implement the sequence of updates / creates /deletes. For sub-entities (for example the address / address information entity), you have to check whether the associated entity already exists. If yes: update it, if no, create it.
  3. You also have to consider the case that a user clears fields. This may result in an update of the entity, or in the deletion of the entity instance. Clearing fields, however, is a non-trivial situation, because you cannot decide in the implementation what the reason for a null value in the OData framework table (afterTableName). This may mean that the UI sent delta update, and expects that the data on the database remain as before, or the UI expects that the data should be set to null on the database. I personally prefer the first option, but this is definitely a starting point for discussion.
  4. The business object update implementation is similar to the create implementation. It consists of business logic (validation and determination logic) and the creation of the update statement.
  5. Properties: In part 3 of this series, I introduced the concept of properties. In the update implementation of the business object, you can read the properties from the properties view of the business object ($P) and check if the incoming elements are disabled or read-only. If yes, you would reject the update and send an error message back.
  6. Value help screen on the UI: When your UI contains input fields with value help (for example the Select Dialog control), you have to expose the respective entity in the OData service (for example the FulfilmentBlockingReason entity in the Sales Order xsodata service). Please make sure to use the with clause to restrict the elements that are exposed for the value help.

The UI implementation uses the update method instead of the create method. The context of the OData service (contextString) contains now the reference to the instance (for example “Prospect(9017)”).

Note: Normally, you will send only the changed fields to the server and not the complete data set. This works fine in case of a JS-implemented update service, because your update statement will update changed fields only. But be careful when using the XS OData-Service without JS implementation (directly on an entity or on an updatable projection view). When you send partial data using the update method from an SAP UI5 UI, the standard OData implementation will set all data that are not contained in the OData call to null. If you want to avoid this, you can use the create method and set the http header element x-http-method to PATCH:

oModel.setHeaders({"x-http-method" : "PATCH"} );

oModel.create(<...> );

We have no reached almost the end of the journey. I will close the series with a final summary and outlook looking again at the advantages of the discussed programming model, but naming also the gaps and disadvantages: A Programming Model for Business Applications - (6) Summary and Outlook