Skip to Content
Technical Articles

SAP Cloud Application Programming Model: Deep Insert (2) with UUID

This tutorial describes

How to implement
Deep Insert
in
SAP Cloud Application Programming Model
(Part 2: using UUID)

Quicklinks:
Part 1: Intro
Sample Project files

This blog is required to cover a case which I guess is relevant for most CAP users:
In CDS models, the data type frequently used for key fields is UUID:

key companyId : UUID;

The benefit is that the value can be generated by the framework.
It affects the CREATE operation, where the user doesn’t send a value for key property
As such, it also affects the DEEP INSERT

Let’s view it in detail

Note that this blog builds upon the previous blog where we created a project to learn how to implement deep insert

New Requirements

In the previous blog we implemented deep insert for a model without generated key values
Let’s have a look at our request payload:

{
“companyId”: 3,
“companyName”: “BeerShop”,
“linkToContact_contactId”: “ContactForBeerShop”,
“linkToContact”: {
“contactId”: “ContactForBeerShop”,
“contactName”: “Peter”,
“contactPhone”: 9876543
}

We can see that we’ve passed each and every property
But in reality, we don’t want to pass ID values, we want it to be automatically generated
So we would remove the key fields from payload:

{
“companyName”: “BeerShop”,
“linkToContact_contactId”: “ContactForBeerShop”,
“linkToContact”: {
“contactName”: “Peter”,
“contactPhone”: 9876543
}

But in above payload, there’s a problem: we don’t know the foreign key value, because it should be generated:
“linkToContact_contactId”: “Which contactId?”,
No, it must be filled by the server
So we remove that field from payload:

{
“companyName”: “BeerShop”,
“linkToContact”: {
“contactName”: “Peter”,
“contactPhone”: 9876543
}

This is how it should look like:
User doesn’t care about IDs.
Generating and assigning is done by the service

But how to realize it?
Well, remember: the service – that’s us

We will need some code.
And a little change in CDS model

Handling UUID

You may create a new project or just apply 2 little changes to your existing project:
Change the data type assigned to the key properties to UUID, for both entities:

namespace com.relation;

entity CompanyEntity {
   key companyId : UUID;
   companyName  : String;
   linkToContact  : Association to ContactEntity;
}

entity ContactEntity {
   key contactId : UUID;
   contactName : String;
   contactPhone : Integer;	
}

Apart from these 2 little changes, all the rest of the project of previous blog remains the same.
And of course, a couple of lines which we have to add to the java code

Note:
After re-deploying the mtar, containing the modeified CDS model, your existing data will disappear, due to change of table meatadata

Background

We have to recognize that the requirements mentioned above require additional effort by us.
Changing the data type to UUID is not enough.

Key values for UUID are usually generated by the FWK, but not for nested entities in a deep insert

Second requirement:The value of foreign-key element.
If the FWK doesn’t generate a UUID, then obviously it cannot be automatically assigned to the foreign-key field

Both tasks have to be taken care in our custom code

Implementation

As pointed out, in addition to the implementation we did above, we need to manually implement 2 little tasks:

1. Generate key value for inline entity

As we know, if we specify UUID as data type for a key field in CDS, then the value will be generated by the FWK
For the user of the OData service, this has the advantage:
When creating an entry with POST request, he doesn’t need to send the key property in the request body

BUT: this works only for normal CREATE requests.
In case of deep insert, only the key value for the parent entity (CompanyEntity) will be generated on the fly.
But not for the associated entities (ContactEntity).
Because the FWK doesn’t know which is the key property.
Because it is done before our implementation code is reached

Solution:
We have to manually generate a UUID for the associated entities in the deep insert

UUID contactGuid = UUID.randomUUID();  
Map<String, Object> inlineContactMap = (Map<String, Object>)mapForCreation
                                       .get("linkToContact");
inlineContactMap.put("contactId", contactGuid); 

In this snippet, we can see:
We generate a UUID (in OData, the type is Edm.Guid) manually
To set the UUID as key value for the nested ContactEntity, we have to fetch the corresponding map
We have a map which represents the payload of the POST request. It contains the parent entity plus nested child entities
The map contains a key which is a navigation property (“linkToContact”)
The value is a map, it is the data for the nested entity (ContactEntity). This is what will be created inline, the deep insert.
In this nested map, we have to set the value (the UUID) for the key field (contactId)
If the end-user has passed a value, it will be overridden, which is the desired behavior.

BTW, the above code has to be surrounded by a little check such that it runs only in case of deep insert.

2. foreign-key value

We’ve generated a GUID and set it as key value for the nested entity.
This is the target of the association.
So at this point in time, the CREATE operation on the database will create 2 new rows in the 2 tables
But the new company entry needs to know the ID of the contact, otherwise the navigation wont work.
As such, now we have to set the same value also for the foreign-key field of the parent entity

Remember:
In our CDS model, we have a managed Association from CompanyEntity to ContactEntity
“Managed” means, that CDS generates a property into the CompanyEntiy which carries the foreign-key for the associated ContactEntity
Means, a Company knows which Contact of the list of Contacts belongs to the Company
This generated property is visible in the edmx.
As such, when generating a guid for the Contact, we have to set this GUID as value for the foreign-key property of the CompanyEntity
This is done in the following line:

mapForCreation.put("linkToContact_contactId", contactGuid);

We have a map which contains all the data which will be created on the database.
In this map we set the value for the foreign-key property (“linkToContact_contactId”)

The name of the foreign-key property is generated by CDS, so it has to be searched in the (generated) edmx or CSN file (in folder src/main/resources/edmx)

See appendix for the full code

Test it

After deployment, try the deep insert with this payload:

HTTP Verb
POST
URL
https://…deepinsertdemo-srv.cfapps…./odata/v2/RelationService/CompanyEntity
Headers
Content-Type: application/json
Request body
{
“companyName”:”CarShop”,
“linkToContact”:{
“contactName”:”Bill”,
“contactPhone”:12345678
}
}

It should work without error and both entities should be created and the navigation (or $expand) should work fine

Summary

In addition to what we summarized in the previous blog, we can summarize the following:
If the deep insert includes child entities which have UUID as datatype for the key element, then it is necessary to write additional code in the custom implementation of CREATE.
1. the value has to be created and assigned to key of nested child
2. the same value has to be assigned to the foreign-key element of the parent

 

Appendix: Sample Project files

data-model.cds

namespace com.relation;

entity CompanyEntity {
   key companyId : UUID;
   companyName  : String;
   linkToContact  : Association to ContactEntity;
}

entity ContactEntity {
   key contactId : UUID;
   contactName : String;
   contactPhone : Integer;	
}

cat-service.cds

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

service RelationService {
   entity CompanyEntity as projection on relation.CompanyEntity;
   entity ContactEntity as projection on relation.ContactEntity;    
}

ServiceImplementation.java

package com.example.deep;

import java.util.ArrayList;
import java.util.Collections;
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.service.prov.api.DataSourceHandler;
import com.sap.cloud.sdk.service.prov.api.EntityData;
import com.sap.cloud.sdk.service.prov.api.EntityMetadata;
import com.sap.cloud.sdk.service.prov.api.ExtensionHelper;
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;

public class ServiceImplementation {
	private static Logger logger = LoggerFactory.getLogger(ServiceImplementation.class);
	
	@Create(entity = "CompanyEntity", serviceName = "RelationService")
	public CreateResponse createCompany(CreateRequest createRequest, ExtensionHelper extensionHelper) throws DatasourceException{
		// 1) retrieve the request payload, the data to create in backend
	    Map<String, Object> mapForCreation = createRequest.getData().asMap();
	    
	    // special handling required in case of UUID: FWK cannot generate it for inline entity
        Map<String, Object> inlineContactMap = (Map<String, Object>)mapForCreation.get("linkToContact");
        // check if request is deep insert
        if(inlineContactMap != null) {
    		// manually generate Guid for inline-entity-key-field (Contacts) and foreign-key-field (Companies)
    		UUID contactGuid = UUID.randomUUID();  
        	inlineContactMap.put("contactId", contactGuid); // fill the key field of inline entity (Contacts)
        	mapForCreation.put("linkToContact_contactId", contactGuid);// fill the forein-key field of "Companies" entity                	
        }
        		
        // 2) our actual task is: specify key field for navigation entity
        //Compose the map of key list for all entities of the deep insert
        Map<String, List<String>> keyMap = new HashMap<String, List<String>>();
        // the key map for the parent entity: Companies. Here, the key field is "companyId"       
        keyMap.put("CompanyEntity", Collections.singletonList("companyId"));        
        // here we assign the key field (contactId) of navigation target entity (Contacts) to the navigationProperty name (contact) 
        keyMap.put("linkToContact", Collections.singletonList("contactId"));                
        
        // 3) send data to database, including the info about keys 
        EntityData entityDataToCreate = EntityData.createFromDeepMap(mapForCreation, keyMap, "RelationService.CompanyEntity");              
         // execute it in database
        EntityData result = extensionHelper.getHandler().executeInsertWithAssociations(entityDataToCreate, true);// true to return created entity
        return CreateResponse.setSuccess().setData(result).response();
	}	
}

mta.yaml

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