Skip to Content
Technical Articles

SAP Cloud Application Programming Model: Deep Insert (4) Consume Remote Service

or:

How to implement
Deep Insert
in
SAP Cloud Application Programming Model
with deep insert on remote service

Quicklinks:
Project Files
Part 1: Intro
Part 2: UUID
Part 3: Multi-Level
Part 5: using SDK

I  mean:
We’re creating an OData service with CAP, so we’re provisioning an OData service
Unlike before, our service doesn’t use a database to read and write data
Instead, we want to use data of an existing remote OData service (e.g. S4 Hana)
So we’re consuming an OData service
Clear?
No
Whenever the user of our service calls our service to get data, then our service calls a remote service to consume data, then our service provisions the data to the user.
Clear?
Hmmm…
Maybe a diagram can help

The provisioning-side was already covered in the previous tutorials.
Today I’d like to describe how to execute a deep insert on the consumption-side

Goal

We’ll create an OData V4 service which connects to an existing remote OData v2 service.
Our OData V4 service supports deep insert and the creation happens by using deep insert on the remote OData v2 service

Scenario:
We expose an OData V4 service with very small customized model
The service should allow to create simple Orders along with the corresponding Order Items
We don’t have persistence in database, because there’s an existing remote OData V2 service, which we can reuse
The data model of the remote service is slightly different
When the user of our V4 service executes a CREATE operation, we adapt the data and send it to the remote service

The following description shows how to deal with it in case of deep insert

Note:
About connecting to remote OData service:
In this blog, we’re using the built-in generic library, not the SAP Cloud SDK
The generic library requires some more coding effort, but is a good learning experience
The next tutorial will show how to use the SAP Cloud SDK to achieve the same result

Prerequisites

1) We need a remote OData service which we can call from our implementation code
That remote OData service must be available for public and for free
It must support write operations, otherwise we cannot test the deep insert
Luckily, there’s a solution for our requirements: the OData V2 reference service
In order to access it, you need to sign up to the SAP Gateway Demo System (simple)
Announcement blog: Demo System Available
How to get an account in the demo system: Sign up to Demo System
SAP documentation about the Sample Service

Sample OData V2 service URL: https://sapes5.sapdevcenter.com/sap/opu/odata/iwbep/GWSAMPLE_BASIC

2) Another prerequisite is the first tutorial on deep insert, which can be found here.

Project

We create a CAP project with name DeepInsertRemote and java package com.example and we use OData protocol version V4

Note:
The OData version doesn’t really make a difference for implementation

CDS

Data model
Our little CDS model defines 2 entites, associated to each other

entity OrderEntity {
  . . .
  linkToItems :  Composition of many ItemEntity 
                 on linkToItems.linkToOrder = $self;
}

entity ItemEntity {
  . . .
  linkToOrder : Association to OrderEntity;
}

We have a one-to-many relation from Order to Items
It is a managed “Composition”
Why not association?
We could define it as to-many association as well, there isn’t much difference.
So why “Composition” ?
But by choosing “Composition” we declare that the “Items” belong only to their “Order”.
It wouldn’t make sense to call the “Item” collection as standalone.
Note that an “Item” is not a “Product”, it is just an entry in the list of an order, where the orderer notes down which things are ordered
Therefore, it makes sense to define the association as “Composition”

Service definition

In the service definition CDS file, we need to make sure that our entities are not “autoexposed”.
Note:
Autoexposed means that you can define model and service and then deploy and you have a running OData service with connection to a database.
Means: No coding required, so “autoexposed”

In our case we don’t want a database, because we use an existing remote OData V2 service, to get and store data.
Means: no autoexposure, so coding required
That implies that we have to manually code every operation (see below)

To declare that we don’t want “autoexposure”, that we don’t want database, that we do implement everything ourselves, we add the following annotation to our entities:

@cds.persistence.skip
entity OrderEntity as projection on relation.OrderEntity;

See Appendix for the whole file content

Implementation

Coming to the required java implementation, we know that we would need to code all operations ourselves (QUERY, CREATE, etc).
However, in this blog we want to focus on implementation of deep insert, so we skip all other operations and implement only the hook for CREATE

@Create(entity = "OrderEntity", serviceName = "RelationService")
public CreateResponse createSalesOrder(CreateRequest createRequest) 

Note:
In our @Create implementation, we even don’t support normal CREATE, without inline data

So what do we need to do?
We implement the CREATE operation and we need to support deep insert

Usually, a CREATE operation works as follows:

  1. obtain the payload which is sent by the user of our service (v4)
  2. perform the CREATE of data (database or whatever)
  3. compose the response of our service (response contains the created data)

In our case, the second point means to do a CREATE on the remote service
So, the user of our service (V4) calls us and sends a payload which contains nested (inline) entity (deep insert)
We take that payload and send it to the remote OData V2 service, again as a deep insert.

Sounds  like easy task
Yes, it is, but there’s some tedious manual work to do:

Our data model doesn’t match the remote data model:
It has similar structure, but different property names
So we cannot simply forward the payload.


We have to adapt it.
As such, our concrete steps look as follows:

  1. obtain the payload which is sent by the user of our service (v4)
  2. convert the payload such that it matches the remote service
    Means to map the incoming properties to the target model
  3. execute the CREATE request (deep insert) on the remote service and send the converted payload
  4. receive the response of the remote service and convert it to our V4 data model
  5. compose the response of our (v4) service and send the converted data
// 1) access the payload from request body, which is OData V4
Map<String, Object> v4RequestMap = createRequest.getData().asMap();

// 2) compose the payload to be sent to remote OData V2 service
Map<String, Object> v2RequestMap = convertV4RequestPayloadToV2(v4RequestMap); 
        
// 3) send request to v2 service
Map<String, Object> v2ResponseMap = ODataCreateRequestBuilder												 
                     .withEntity("/sap/opu/odata/IWBEP/GWSAMPLE_BASIC", "SalesOrderSet")												            
                     .withBodyAsMap(v2RequestMap)												      
                     .build()												      
                     .execute("ES5Destination")												       
                     .asMap();

// 4) convert v2 response back to v4
Map<String, Object> v4ResponseMap = convertV2ResponsePayloadToV4(v2ResponseMap);
			
// 5) compose the V4 response of our service
return CreateResponse.setSuccess().setData(v4ResponseMap).response();

Doesn’t look too bad
Yes, but….
The tedious manual work is in the helper methods
What exactly do we have to do?

1. Obtain request payload
We receive the incoming (v4) request payload as map

Map<String, Object> v4RequestMap = createRequest.getData().asMap();

2. convert
We create a new map which will be used to send to the remote v2 service

Map<String, Object> v2RequestMap = new HashMap<String, Object>();

We take each property out of the v4-map

v4RequestMap.get("commentOnOrder")

and put into the v2-map

v2RequestMap.put("Note", v4RequestMap.get("commentOnOrder"));

This line shows how the different property names are mapped

One more task to do while converting:
If the remote service has additional requirements, that our service doesn’t support, we need to fulfill it manually.
In our simple example, we’re just hard-coding some values:

v2RequestMap.put("CurrencyCode", "USD");

Coming to the actual deep insert, the nested inline data (associated entitiy):
We get the nested data as nested map, it is accessed via the navigation property, afterwards processed as usual

List<Map<String, Object>> v4InlineItemsList = (List<Map<String, Object>>)v4RequestMap.get("linkToItems");

The line shows that the nested map is not a map, it is a list.
The reason is that we modelled a one-to-many association, so we get potentially many nested maps

To do the conversion, we have 3 helper methods,
one for the top-level entity (parent, the OrderEntity)
one to look over the list
third, to do the conversion for each nested map (children, ItemEntity)

3. call the remote service
In this blog, we’re using the generic library, so we compose a create request using the builder and passing the map which contains entries matching the remote data model
The request needs some more info: the URL of the remote service
The remote service host is extracted into a destination, the destination name is passed to the request and the concrete service name and entity name is passed to the request as well
The return value of the request is the response of the remote V2 service

Map<String, Object> v2ResponseMap = ODataCreateRequestBuilder												 
   .withEntity("/sap/opu/odata/IWBEP/GWSAMPLE_BASIC", "SalesOrderSet")												 
   .withBodyAsMap(v2RequestMap)												 
   .build()										 
   .execute("ES5Destination")
    .asMap();

4. convert the v2 response to our v4 model
In this step we have to do the same work like in step 2, but the other way around

5. compose the response of our v4 service
We’ve already converted to the right format, so we can just put it into the response object

return CreateResponse.setSuccess().setData(v4ResponseMap).response();

That’s it

See Appendix for the whole file content

MTA

Before deployment, we do the 2 small modifications to the mta.yaml file, as described in previous blog

In addition, we need 2 more changes:
Add  the destination information, which will end up in the environment of deployed application
First, declare a new “resource”

  - name: ES5Destination
    properties:
       url: https://sapes5.sapdevcenter.com
       username: your_ES5_username
       password: your_ES5_password   

Second, use it in the service-module:

      - name: ES5Destination
        group: destinations
        properties:
         name: ES5Destination
         url: '~{url}'
         username: '~{username}'
         password: '~{password}'

See Appendix for the whole file content

Test

AFter deploy, to test our implementation of deep insert, we fire the following request in our favorite REST client

HTTP Verb
POST
URL
https://p123trial-acc-space-deepinsertremote…..odata/v2/RelationService/OrderEntity
Headers
Content-Type: application/json
Request body
{
“commentOnOrder”: “Want my laptops”,
“linkToItems”:
[
{
“productForOrder”: “HT-1001”,
“numberOfProductsInOrder”: 11,
}
]
}

Result:

{
“@odata.context”: “$metadata#OrderEntity(linkToItems())/$entity”,
“orderId”: “0500000521”,
“commentOnOrder”: “Want my laptops”,
“linkToItems”:
[
{
“itemId”: “2ec15423-8e74-4511-a7d9-75876ed58318”,
“productForOrder”: “HT-1001”,
“numberOfProductsInOrder”: 11,
“linkToOrder_orderId”: “0500000521”,
}
]
}

In the response of our CREATE request, we can see the data that we’ve sent
How boring…
But more interesting is that we can see the SalesOrderID and SalesOrderLineItemID which were generated in the backend system
I don’t see..
Remember that they were mapped to properties orderId and foreignOrderId_orderId
OK, I see
We can use them to verify the result

Verify

To test the successful execution I suggest to invoke directly the remote OData V2 service.
We can compose a single READ URL with the ID which we received above (‘0500000509’) and with expanded LineItems:

https://sapes5.sapdevcenter.com/sap/opu/odata/iwbep/GWSAMPLE_BASIC/SalesOrderSet(‘0500000521’)?$expand=ToLineItems

And the result:

Why can’t we use our own v4 service?
We can not
We don’t have autoexposure in our service, because we marked it as cds.persistence.skip
As such, we cannot execute any GET request

Summary

In this tutorial, we’ve learned how to execute a deep insert on a remote OData V2 service.
We’ve also showcased the way of transforming the payload a OData model to a different OData model
We’ve seen, implementing the usage of remote service is easy, but all the transformation code in 2 directions is tedious
It is tedious because we have to deal with Maps and property names
It becomes more readable and less tedious if we use the SAP Cloud SDK and the “Virtual Data Model” (VDM)

To make it clear again:
The steps are always the same (only if models are identical, no conversion is necessary)
In this tutorial we’ve used the generic connectivity library, which requires that we build and execute requests etc
BTW, this library also supports usage of proxy classes instead of maps. And such pojos can also be generated on command line (or maven)
In the next blog, we’ll do the same deep insert implementation, but it will be based on the SAP Cloud SDK

Next Steps

Which are our next steps?
Wait
Wait? For what?
For the next tutorial to appear

 

Appendix: Sample Project Files

data-model.cds

namespace com.relation;

entity OrderEntity {
  key orderId: String;
  commentOnOrder: String;
  linkToItems :  Composition of many ItemEntity on linkToItems.linkToOrder = $self;
}

entity ItemEntity {
  key itemId: UUID; 
  productForOrder: String;
  numberOfProductsInOrder: Decimal(5, 2); 
  linkToOrder : Association to OrderEntity;
}

cat-service.cds

using com.relation from '../db/data-model';

service RelationService {

    @cds.persistence.skip
    entity OrderEntity as projection on relation.OrderEntity;
    
    @cds.persistence.skip
    entity ItemEntity as projection on relation.ItemEntity;
}

ServiceImplementation.java

package com.example;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cloud.sdk.odatav2.connectivity.ODataCreateRequestBuilder;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.service.prov.api.exception.DatasourceException;
import com.sap.cloud.sdk.service.prov.api.operations.Create;
import com.sap.cloud.sdk.service.prov.api.request.CreateRequest;
import com.sap.cloud.sdk.service.prov.api.response.CreateResponse;
import com.sap.cloud.sdk.service.prov.api.response.ErrorResponse;

public class ServiceImplementation {
	
	@Create(entity = "OrderEntity", serviceName = "RelationService")
	public CreateResponse createSalesOrder(CreateRequest createRequest) throws DatasourceException{
		// 1) access the payload from request body, which is OData V4
        Map<String, Object> v4RequestMap = createRequest.getData().asMap();
        // 2) compose the payload to be sent to remote OData V2 service
        Map<String, Object> v2RequestMap = convertV4RequestPayloadToV2(v4RequestMap); 
        
        try {
        	// 3) send request to v2 service
        	Map<String, Object> v2ResponseMap = ODataCreateRequestBuilder												 
                     .withEntity("/sap/opu/odata/IWBEP/GWSAMPLE_BASIC", "SalesOrderSet")												   
                     .withBodyAsMap(v2RequestMap)												   
                     .build()												 
                     .execute("ES5Destination")												 
                     .asMap();

        	// 4) convert v2 response back to v4
			Map<String, Object> v4ResponseMap = convertV2ResponsePayloadToV4(v2ResponseMap);
			
			// 5) compose the V4 response of our service
			return CreateResponse.setSuccess().setData(v4ResponseMap).response();
		} catch (ODataException e) {
			return CreateResponse.setError(ErrorResponse.getBuilder()
					.setMessage(e.getMessage())
					.setStatusCode(501)
					.setCause(e)
					.response());
		}
	}	
	
	/* REQUEST HELPERS */
	
	// handle the full payload, the parent entity (SalesOrder) including nested inline list of children (LineItems)
	private Map<String, Object> convertV4RequestPayloadToV2(Map<String, Object> v4RequestMap){
		// map the v4 property names to v2 and, in addition, fill some non-nullable fields
		Map<String, Object> v2RequestMap = new HashMap<String, Object>();
		v2RequestMap.put("Note", v4RequestMap.get("commentOnOrder"));
		v2RequestMap.put("CustomerID", "0100000000");
		v2RequestMap.put("CurrencyCode", "USD");

		// now handle the deep insert, the nested inline list of maps
		List<Map<String, Object>> v4InlineItemsList = (List<Map<String, Object>>)v4RequestMap.get("linkToItems"); // to-many association
		List<Map<String, Object>> v2RequestInlineList = convertV4InlineListToV2(v4InlineItemsList);
		v2RequestMap.put("ToLineItems", v2RequestInlineList);
		
		return v2RequestMap;
	}
	 // inline list: SalesOrderLineItem collection
	private List<Map<String, Object>> convertV4InlineListToV2(List<Map<String, Object>> v4InlineItemsList){
		// the nested inline data is a list, because of to-many association
		List<Map<String, Object>> v2InlineItemsList = new ArrayList<Map<String, Object>>();
		// loop through all v4 line items in order to map them to v2
		for (Map<String, Object>  v4InlineMap : v4InlineItemsList) {
			Map<String, Object> v2InlineMap = convertV4InlineMapToV2(v4InlineMap);
			v2InlineItemsList.add(v2InlineMap);
		} 

		return v2InlineItemsList;
	}
	
	// inline Map: single SalesOrderLineItem entry
	private Map<String, Object> convertV4InlineMapToV2(Map<String, Object> v4InlineMap){
		Map<String, Object> v2InlineMap = new HashMap<String, Object>();
		v2InlineMap.put("DeliveryDate", new Date(System.currentTimeMillis())); // immediate delivery
		v2InlineMap.put("ProductID", (String)v4InlineMap.get("productForOrder"));
		v2InlineMap.put("Quantity", (BigDecimal)v4InlineMap.get("numberOfProductsInOrder"));
		v2InlineMap.put("CurrencyCode", "USD");
		v2InlineMap.put("ItemPosition", "0000000010");
		
		return v2InlineMap;
	}
	
	/* RESPONSE HELPERS */
	
	// response of V2 deep insert contains a SalesOrder with expanded list of LineItems
	private Map<String, Object> convertV2ResponsePayloadToV4(Map<String, Object> v2ResponseMap){
		Map<String, Object> v4ResponseMap = new HashMap<String, Object>();
		v4ResponseMap.put("orderId", (String) v2ResponseMap.get("SalesOrderID"));
		v4ResponseMap.put("commentOnOrder", (String) v2ResponseMap.get("Note"));

		// the response of deep insert is a $expand
		List<Map<String, Object>> v2ResponseInlineList =  (List<Map<String, Object>>) ((List<Map<String, Object>>)v2ResponseMap.get("ToLineItems"));
		
		// convert it to v4
		List<Map<String, Object>> v4ResponseInlineList = convertV2InlineListToV4(v2ResponseInlineList);
		
		// response of CREATE operation: the response body is composed by response of call to v2 service
		v4ResponseMap.put("linkToItems", v4ResponseInlineList);
		
		return v4ResponseMap;
	}
	
	private List<Map<String, Object>> convertV2InlineListToV4(List<Map<String, Object>>  v2ResponseInlineList){
		List<Map<String, Object>> v4ResponseInlineList = new ArrayList<Map<String, Object>>();
		for(Map<String, Object> v2ResponseInlineMap : v2ResponseInlineList){
			Map<String, Object> v4ResponseInlineMap = convertV2InlineMapToV4(v2ResponseInlineMap);
			v4ResponseInlineList.add(v4ResponseInlineMap);
		}
		
		return v4ResponseInlineList;
	}
	
	// convert inline entry of V2 SalesOrderLineItem to V4 Map
	private Map<String, Object> convertV2InlineMapToV4 (Map<String, Object> v2ResponseInlineMap){
		Map<String, Object> v4ResponseInlineMap = new HashMap<String, Object>();
		v4ResponseInlineMap.put("linkToOrder_orderId", v2ResponseInlineMap.get("SalesOrderID"));
		v4ResponseInlineMap.put("itemId", UUID.randomUUID() ); 
		v4ResponseInlineMap.put("productForOrder", v2ResponseInlineMap.get("ProductID"));
		v4ResponseInlineMap.put("numberOfProductsInOrder", v2ResponseInlineMap.get("Quantity"));
		
		return v4ResponseInlineMap;
	}		
}

 

mta.yaml

_schema-version: 2.0.0
ID: DeepInsertRemote
version: 1.0.0
modules:
  - name: DeepInsertRemote-db
    type: hdb
    path: db
    parameters:
      memory: 256M
      disk-quota: 256M
    requires:
      - name: DeepInsertRemote-db-hdi-container
  - name: DeepInsertRemote-srv
    type: java
    path: srv
    parameters:
      memory: 990M
    provides:
      - name: srv_api
        properties:
          url: ${default-url}
    requires:
      - name: DeepInsertRemote-db-hdi-container
        properties:
          JBP_CONFIG_RESOURCE_CONFIGURATION: '[tomcat/webapps/ROOT/META-INF/context.xml:
            {"service_name_for_DefaultDB" : "~{hdi-container-name}"}]'
      - name: DeepInsertRemote-uaa
      - name: ES5Destination
        group: destinations
        properties:
         name: ES5Destination
         url: '~{url}'
         username: '~{username}'
         password: '~{password}'              
resources:
  - name: DeepInsertRemote-db-hdi-container
    type: com.sap.xs.hdi-container
    properties:
      hdi-container-name: ${service-name}
  - name: DeepInsertRemote-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      service-plan: application
      service: xsuaa
      config:
        xsappname: DeepInsertRemote-${space}
        tenant-mode: dedicated
  - name: ES5Destination
    properties:
       url: https://sapes5.sapdevcenter.com
       username: your_ES5_username
       password: your_ES5_password
Be the first to leave a comment
You must be Logged on to comment or reply to a post.