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:
Sample Project files
Part 1: Intro
Part 3: Multi-Level
Part 4: Consume Remote Service
Part 5: Remote Service with SDK
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
Sorry,
for peace of code below
i don't understand where you store on db the information about inlineContactMap.
Regards
Francesco
Hello Francesco Pisano ,
thanks for the question and sorry if I wasn't clear enough
The "normal" way of creating an entity in database would be this one:
In case of deep insert, the method to call is:
In the first case, the entityData doesn't contain nested map.
In the second case, the entityData does contain nested map
I assume that the FWK ignores associations in the first case and in the second case, the FWK will follow the associations and will find 2 (or multiple) entities to create.
With the help of the provided key-map, the FWK can find out how the INSERT statement for the associated entites has to be built.
Then, it can fire multiple INSERTs to DB
That's at least my assumption
Kind Regards,
Carlos
Hello Carlos,
I'm trying to build an app with many to many associations model composed by UUID identifier.
I'd like to know how is possible to get the ID of the parent entity to make the POST possible.
You explained that is the FWK that generate the UUID for the parent entity, but if I'd like to know this UUID before execution of "executeInsertWithAssociations", how should I do it ?
Thank's for your work and your tutorial.
Best Regards,
Vincent.
Hello PoC Sopra Steria ,
Thank you for reading my blog post and for using the CAP.
I'd like to point out that this blog is based on the "old" Java stack, which in the meantime has been replaced by new java stack.
Please build your app with new stack, as the old one is not supported anymore.
Here's the new docu: https://cap.cloud.sap/docs/
With new java stack, which is based on the new java implementation of CQL, the deep insert works out of the box, no custom code required.
Kind Regards,
Carlos