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:
- obtain the payload which is sent by the user of our service (v4)
- perform the CREATE of data (database or whatever)
- 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:
- obtain the payload which is sent by the user of our service (v4)
- convert the payload such that it matches the remote service
Means to map the incoming properties to the target model - execute the CREATE request (deep insert) on the remote service and send the converted payload
- receive the response of the remote service and convert it to our V4 data model
- 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:
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