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: 
CarlosRoggan
Product and Topic Expert
Product and Topic Expert
In one of the previous blogs, we’ve already learned the basics about implementing write operations like CREATE and UPDATE
In the present blog, we’re going to discuss some more advanced details:
An UPDATE operation can be issued via PUT or PATCH request, see below for differences
An UPDATE operation can be accompanied by a "Prefer"-header, which has some impact, read on to learn which impact.

 

Oh - by the way - this blog is about creating an OData V4 service in Java, using the SAP Cloud Platform SDK for service development. Find an introduction here and an overview of the series of blogs here.

 

UPDATE == PUT || PATCH


For reasons of simplification, in the SDK, there’s only one annotation to enable callback for UPDATE operation.
However, there are 2 HTTP verbs for issuing an UPDATE: the PUT request and the PATCH request
In both cases, the SDK redirects the request to our implementation method, annotated with @Update.
There, we can obtain the request body from the UpdateRequest instance and then handle the changes in our data source.

Now, let’s discuss that in detail:
What is the difference between PUT and PATCH?

Example:

In our People collection, we've stored a Person who in the meantime has changed the e-mail address. No prob, we have a nice OData service and we can easily UPDATE the stored data.
We do a QUERY operation to get the full list, then we find the Person entry which we want to change and we do a READ request of the single entry
Fine, we see the wrong e-mail address and everything else is correct.
As such, we want to change only one property, the Email.
So we prepare an HTTP request, 
we send only one property in the request body, and we choose the HTTP verb as PATCH.
And send the request.
As a result, only the Email will change and the rest of the data of the person will remain the same.

This means, when using a PATCH request, ONLY those properties which are sent in the request body, have to be changed, all others are not touched at all.

We can do the same with PUT.
But in this case, we have to copy ALL the properties from the READ request of the single person.
Then send ALL the properties, not only the changed Email property, with a PUT request.

One advantage of PATCH is obvious: the network traffic is much smaller.

BTW, the OData V4 specification recommends to use PATCH. You might wish to check the What’s new in V4 section.

BTW, PATCH was already supported in OData V2

 

PATCH in detail:


We’ve already mentioned the relevant details: the user of our service sends only those properties which he wants to change. And he doesn't care about generated values, non-editable values, etc
Little bit more info can be found in the spec

 

PUT in detail:


Here we have to deal with 2 additional details:

Imagine, the user sends only the Email property, nothing else, but uses a PUT request instead of a PATCH.

What will happen in the backend?

The answer can be found in the same spec which I've linked above:
http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part1-protocol/odata-v4.0-errata03-...

The spec is clear:
Missing properties MUST be re-set.

Which means that – in our example - if the user sends only the Email property in his PUT request, only the Email would be updated to new value, all the other (missing) properties like name etc would be set to null.

BUT: if there’s a default value, then the missing properties are re-set to their default values.

AND: if a property is declared as not-nullable: then it can never be null. So either it must be present in the request body, or the value is generated in the service implementation

AND: of course, the key property can anyways never be changed. In both cases, PUT and PATCH, the key property can be omitted from the request body

There are some more advanced details which we don’t want to discuss here, but can be found in the same spec via again the same link

 

 

 

CODE


Our blogs are tutorials for beginners, it is all about Java code. So we can't miss it in this blog.
Some people use to say that a blog without code is like a talk without a joke...

However, in this case I'd like to point out that the code which we're going to discuss is meant for clarification purpose, I hope that it is never required to write such code in productive service.

I'd like to use it to showcase the difference between treating properties.
In fact, using the SDK, the developer shouldn't be required to distinguish between the HTTP verbs used by the user.

Having said this, we can go ahead and do exactly that, to get the HTTP verb in order to distinguish:
String httpMethod = request.getHttpMethod();

 

PATCH

In case of PATCH, we simply loop over all properties in the payload.
Missing properties can be ignored.
We only need to handle the key property which should not be modified
if(httpMethod.equals("PATCH")) {
// only replace the properties which are present in request body
Set<String> keySet = requestBody.keySet();
for (Iterator<String> iterator = keySet.iterator(); iterator.hasNext();) {
String propertyName = (String) iterator.next();
if(propertyName.equals("PersonId")) {
continue; // ignore key field
}
existingPersonMap.replace(propertyName, requestBody.get(propertyName));
}

 

I would say: that’s already it.

 

Now PUT:

Here we have to consider the different cases, as mentioned above:

  1. The property FirstName is marked as Nullable=false in edmxIf it is present – fine, update it.
    If it is not present: in this case, we behave like the spec tells us:Omitting a non-nullable property with no service-generated or default value from a PUT request results in a 400 Bad Request error.BTW, this is again described in the same link and the same spec – how should it be different???We don’t have service-generated code, so we have to return the error response with status code 400
    if(requestBody.containsKey("FirstName")) {
    existingPersonMap.replace("FirstName", requestBody.get("FirstName"));
    }else {
    return UpdateResponse.setError(ErrorResponse.getBuilder()
    .setMessage("Error: property FirstName must not be null")
    .setStatusCode(400)
    .response());
    }

     

    Note:
    what if the property is present in the request body, but the user has specified a value as null?In this special case, the problem is detected and handled by the FWK, so we don’t need that additional check in our code.

  2. The property LastName is not so picky, it allows null.
    Since it doesn’t have a default value, we set the value to null, if the property is missing in the request body
    if(requestBody.containsKey("LastName")) {
    existingPersonMap.replace("LastName", requestBody.get("LastName"));
    }else {
    existingPersonMap.replace("LastName", null);
    }​


  3. The property Email has a property facet “DefaultValue” specified in the edmx file.
    So, if the property Email is missing in the request body, we set the value to the default value (from edmx), not to null.
    if(requestBody.containsKey("Email")) {
    existingPersonMap.replace("Email", requestBody.get("Email"));
    }else {
    existingPersonMap.replace("Email", "Unknown");
    }


  4. The last thing to consider: the property PersonId is the key property.
    The key property is completely ignored, only for better understanding, I've added the lines:
    if(requestBody.containsKey("PersonId")) {
    // do nothing
    }

     


Please find the complete source code at the end of this page.
But don't forget: that's only comprehensive code with many ifs and elses for treating each property separately. It is only meant for better understanding.

Talking about sources: I’ve added an alternative sample implementation to the appendix section.

 

Prefer header


If you’re familiar with OData V2, then you know that the PUT or PATCH request in case of success  always returns an empty response with status code 204 – No Content

In this respect, the OData V4 spec is more flexible.
It says:
"On success, the response MUST be a valid success response"

 

Yes, this is quite flexible.

And this is where the "Prefer" header comes into the game.

By adding the "Prefer" header with one of below values, the user can influence the behavior on response.

For a PUT/PATCH requests, the following 2 values for Prefer header have an impact on the success response:
















Header name Header value
Prefer return=representation
Prefer return=minimal


 

The OData V4 spec gives all details

So, in case of PUT/PATCH, we can say:

Even in OData V4, the default is that the success response body is empty, status code 204
The reason is that in most cases, it is not needed. Other than for a CREATE request, we know the key property value(s) and as such are not waiting for any interesting news from the response.

As such, the header "Prefer" with value "return=minimal" is not required for PUT/PATCH requests, as it is the default.

If the user fires a PUT or PATCH request and adds the header "Prefer" with value "return=representation", then the service must return the modified entity in the response body and the status code is 200.

This is used, e.g. by Web application UIs which allow to do a modification to an entry and want to show the result of the modification. In OData V2, such client had to fire an additional READ request to the same resource. Now, it just adds the Prefer header and gets the updated entity in the response.

 

You're still wondering why it is needed?


We have our example:

We fire a PUT request to a Person entity, but we don’t specify the property Email.
In such a case, the service has to reset the Email property to the default value.
As such, the result of the PUT looks different that the body of the PUT request and also different from the previous state of the resource.
The response of the PUT request with prefer:return=representation, must contain the correct entity, so it cannot be simply the copy of the request body.

 

For me, the question is now: how is that solved in the SDK?


Since we're all humans, even the SDKs cannot do magic, so it is handled like this:
After the UPDATE, the Framework calls the READ and the result of the READ is given as response of the UPDATE.

Aha, this is interesting


Whenever the FWK recognizes that there’s a PUT/PATCH request with Prefer header and value return=representation, then the FWK invokes the @Update implementation and afterwards the @Read implementation
Then it takes the response of the READ and sets it as response of the  PUT/PATCH request.
This is done fully automatically.
The service developer doesn’t need to write any additional line.

The only thing is:
The @Read implementation has to be there, otherwise, the PUT/PATCH call with Prefer header will fail.

Shall we give this some more prominent place, since it is a requirement?
Note:
If UPDATE with return=representation is desired, then it is a prerequisite that the READ must be implemented

 

One more info:
After executing the PUT/PATCH request with the prefer header, you can check the response headers. There, you can find the Preference-Applied header:

"preference-applied": "return=representation"

 

The following screenshot shows how we send a PATCH request with Prefer header, and the response contains the full entity:



 

Summary


After going through the previous blog and this blog, you’ve learned how to implement a basic OData service.

The next blog will be discussing one more interesting aspect related to UPDATE: it will be about UPSERT.

 

Links


Overview of blog series and link collection.

 

Appendix 1: Source code of Model file: DemoService.xml


<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="demo" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Person">
<Key>
<PropertyRef Name="PersonId" />
</Key>
<Property Name="PersonId" Type="Edm.Int32"/>
<Property Name="FirstName" Type="Edm.String" Nullable="false"/>
<Property Name="LastName" Type="Edm.String"/>
<Property Name="Email" Type="Edm.String" DefaultValue="Unknown"/>
<Property Name="JobId" Type="Edm.Int32" />
</EntityType>
<EntityContainer Name="container">
<EntitySet Name="People" EntityType="demo.Person"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>

 

 

Appendix 2: Source code of Java class file: ServiceImplementation.java


package com.example.DemoService;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import com.sap.cloud.sdk.service.prov.api.operations.Query;
import com.sap.cloud.sdk.service.prov.api.operations.Read;
import com.sap.cloud.sdk.service.prov.api.operations.Update;
import com.sap.cloud.sdk.service.prov.api.request.QueryRequest;
import com.sap.cloud.sdk.service.prov.api.request.ReadRequest;
import com.sap.cloud.sdk.service.prov.api.request.UpdateRequest;
import com.sap.cloud.sdk.service.prov.api.response.ErrorResponse;
import com.sap.cloud.sdk.service.prov.api.response.QueryResponse;
import com.sap.cloud.sdk.service.prov.api.response.ReadResponse;
import com.sap.cloud.sdk.service.prov.api.response.UpdateResponse;

public class ServiceImplementation {

private static final Logger logger = LoggerFactory.getLogger(ServiceImplementation.class);

private static final List<Map<String, Object>> peopleList = new ArrayList<Map<String, Object>>();


@Query(serviceName="DemoService", entity="People")
public QueryResponse getPeople(QueryRequest request) {
List<Map<String, Object>> peopleList = getAllPeople();
return QueryResponse.setSuccess().setDataAsMap(peopleList).response();
}


@Read(serviceName="DemoService", entity="People")
public ReadResponse getPerson(ReadRequest request) {

Integer id = (Integer) request.getKeys().get("PersonId");

Map<String, Object> personMap = findPerson(id);

// check if the requested person exists
if(personMap == null) {
logger.error("Person with PersonId " + id + " doesn't exist! Service request was invoked with invalid key value!");
return ReadResponse.setError(
ErrorResponse.getBuilder()
.setMessage("Person with PersonId " + id + " doesn't exist!")
.setStatusCode(404)
.response());
}

return ReadResponse.setSuccess().setData(personMap).response();
}


@Update(serviceName = "DemoService", entity = "People")
public UpdateResponse updatePerson(UpdateRequest request) {

// retrieve from request: which person to update
Integer personIdForUpdate = (Integer)request.getKeys().get("PersonId");

// retrieve from request: the data to modify
Map<String, Object> requestBody = request.getMapData();

// retrieve from database: the person to modify
Map<String, Object> existingPersonMap = findPerson(personIdForUpdate);

// check if the requested person exists
if(existingPersonMap == null) {
logger.error("Person with PersonId " + personIdForUpdate + " doesn't exist! Service request was invoked with invalid key value!");
return UpdateResponse.setError(
ErrorResponse.getBuilder()
.setMessage("Person with PersonId " + personIdForUpdate + " doesn't exist!")
.setStatusCode(404)
.response());
}

String httpMethod = request.getHttpMethod();

if(httpMethod.equals("PATCH")) {
// only replace the properties which are present in request body
Set<String> keySet = requestBody.keySet();
for (Iterator<String> iterator = keySet.iterator(); iterator.hasNext();) {
String propertyName = (String) iterator.next();
if(propertyName.equals("PersonId")) {
continue; // ignore key field
}
existingPersonMap.replace(propertyName, requestBody.get(propertyName));
}
}else if (httpMethod.equals("PUT")) {
// in case of PUT, all properties have to be replaced, either with new value or reset

// FistName can NOT be null but also no Default value. Here it is either a service-generated value, or throw error
if(requestBody.containsKey("FirstName")) {
existingPersonMap.replace("FirstName", requestBody.get("FirstName"));
}else {
return UpdateResponse.setError(ErrorResponse.getBuilder().setMessage("Error: property FirstName must not be null").setStatusCode(400).response());
}

// LastName can be null
if(requestBody.containsKey("LastName")) {
existingPersonMap.replace("LastName", requestBody.get("LastName"));
}else {
existingPersonMap.replace("LastName", null);
}

// Email: reset to default, as specified in edmx: <Property Name="Email" ... DefaultValue="Unknown"/>
if(requestBody.containsKey("Email")) {
existingPersonMap.replace("Email", requestBody.get("Email"));
}else {
existingPersonMap.replace("Email", "Unknown");
}

// JobId can be null
if(requestBody.containsKey("JobId")) {
existingPersonMap.replace("JobId", requestBody.get("JobId"));
}else {
existingPersonMap.replace("JobId", null);
}

// PersonId is key field, don't change it, to keep database consistent
if(requestBody.containsKey("PersonId")) {
// do nothing
}
}

return UpdateResponse.setSuccess().response();
}



/* Dummy Database */

private List<Map<String, Object>> getAllPeople(){
// init the "database"
if(peopleList.isEmpty()) {
createPerson(1, "Anna", "Martinez", "Anna@mail.com", 1);
createPerson(2, "Berta", "Hughes", "Berta@webmail.com", 2);
createPerson(3, "Claudia", "Forrester", "CF@fastmail.com", 1);
createPerson(4, "Debbie", "Huffington", "debbie@dbmail.com", 2);
}

return peopleList;
}

// used for READ operation. Go through the list and find the person with requested PersonId
private Map<String, Object> findPerson(Integer requiredPersonId){
List<Map<String,Object>> peopleList = getAllPeople();
for(Map<String, Object> personMap : peopleList) {
if(requiredPersonId.equals((Integer)personMap.get("PersonId"))) {
return personMap;
}
}
return null;
}

private Map<String, Object> createPerson(Integer personId, String firstName, String lastName, String mail, Integer jobId){
Map<String, Object> personMap = new HashMap<String, Object>();

personMap.put("PersonId", personId);
personMap.put("FirstName", firstName);
personMap.put("LastName", lastName);
personMap.put("JobId", jobId);

if(mail != null) {
personMap.put("Email", mail);
}else {
personMap.put("Email", "Unknown"); //default value which we've specified in edmx file
}

peopleList.add(personMap);

return personMap;
}
}

 

 

Appendix 3: Source code of Java class file: ServiceImplementation.java


Alternative implementation.

I shows that we can outsource a generic helper method that provides the missing properties in case of PUT.
Like that, in the UPDATE implementation we can have one dedicated section for doing the special treatment of the special properties (default value, no-nullable), and we don’t need to have separate code for PUT and PATCH. However, we do need to know what we’re doing.
Also, the special treatment can be moved into separate method.
The actual update is moved into a doUpdate method, to illustrate that anything different is done in productive use cases.
Anyways, the code looks little better now.

What do you think?

 
package com.example.DemoService;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import com.sap.cloud.sdk.service.prov.api.operations.Query;
import com.sap.cloud.sdk.service.prov.api.operations.Read;
import com.sap.cloud.sdk.service.prov.api.operations.Update;
import com.sap.cloud.sdk.service.prov.api.request.QueryRequest;
import com.sap.cloud.sdk.service.prov.api.request.ReadRequest;
import com.sap.cloud.sdk.service.prov.api.request.UpdateRequest;
import com.sap.cloud.sdk.service.prov.api.response.ErrorResponse;
import com.sap.cloud.sdk.service.prov.api.response.QueryResponse;
import com.sap.cloud.sdk.service.prov.api.response.ReadResponse;
import com.sap.cloud.sdk.service.prov.api.response.UpdateResponse;

public class ServiceImplementationGeneric {

private static final List<Map<String, Object>> peopleList = new ArrayList<Map<String, Object>>();


@Query(serviceName="DemoService", entity="People")
public QueryResponse getPeople(QueryRequest request) {
List<Map<String, Object>> peopleList = getAllPeople();
return QueryResponse.setSuccess().setDataAsMap(peopleList).response();
}

@Read(serviceName="DemoService", entity="People")
public ReadResponse getPerson(ReadRequest request) {
Integer id = (Integer) request.getKeys().get("PersonId");
Map<String, Object> personMap = findPerson(id);
return ReadResponse.setSuccess().setData(personMap).response();
}

@Update(serviceName = "DemoService", entity = "People")
public UpdateResponse updatePerson(UpdateRequest request) {
Map<String, Object> requestBody = request.getMapData();
// get properties which are missing in request body. In case of PATCH: empty list.
List<String> missingProperties = propertiesMissingInRequestBody(request);
for(String missingProperty : missingProperties) {
// in case of PUT, handle those missing properties, which require special treatment
if(missingProperty.equals("PersonId")) {
continue;
}else if(missingProperty.equals("Email")) {
requestBody.put(missingProperty, "Unknown");//reset to default value
// ... etc
}else if(missingProperty.equals("FirstName")) { // handle non-nullable field
return UpdateResponse.setError(ErrorResponse.getBuilder().setMessage("Error in request: property FirstName must not be null").setStatusCode(400).response());
}else {
requestBody.put(missingProperty, null); // all others reset to null
}
}
requestBody.remove("PersonId"); // can still be there

// now that the request body is complete, we can move over to daily work
Integer personIdForUpdate = (Integer)request.getKeys().get("PersonId");
Map<String, Object> existingPersonMap = findPerson(personIdForUpdate);

// do checks on existingPerson

// do update
doUpdateInBackend(requestBody, existingPersonMap);

return UpdateResponse.setSuccess().response();
}

private void doUpdateInBackend(Map<String, Object> newData, Map<String, Object> personToBeModified) {
Set<String> newDataProperties = newData.keySet();
for (Iterator<String> iterator = newDataProperties.iterator(); iterator.hasNext();) {
String propertyName = (String) iterator.next();
personToBeModified.replace(propertyName, newData.get(propertyName)); // this will replace also values passed as null
}
}

private List<String> propertiesMissingInRequestBody(UpdateRequest request){
// as specified, in case of PATCH, missing props don't need to be handled, so return empty list
if(request.getHttpMethod().equals("PATCH")) {
return Collections.emptyList();
}

// in case of PUT, we collect the properties which are defined for the entity, but missing in the request body
List<String> missingPropertiesList = new ArrayList<String>();
List<String> allPropertiesList = request.getEntityMetadata().getElementNames();

Map<String, Object> requestBodyMap = request.getMapData();
for(String definedProperty : allPropertiesList) {
if (! requestBodyMap.containsKey(definedProperty)) {
missingPropertiesList.add(definedProperty);
}
}

return missingPropertiesList;
}



/* Dummy Database */

private List<Map<String, Object>> getAllPeople(){

// init the "database"
if(peopleList.isEmpty()) {
createPerson(1, "Anna", "Martinez", "Anna@mail.com", 1);
createPerson(2, "Berta", "Hughes", "Berta@webmail.com", 2);
createPerson(3, "Claudia", "Forrester", "CF@fastmail.com", 1);
createPerson(4, "Debbie", "Huffington", "debbie@dbmail.com", 2);
}

return peopleList;
}


// used for READ operation. Go through the list and find the person with requested PersonId
private Map<String, Object> findPerson(Integer requiredPersonId){
List<Map<String,Object>> peopleList = getAllPeople();
for(Map<String, Object> personMap : peopleList) {
if(requiredPersonId.equals((Integer)personMap.get("PersonId"))) {
return personMap;
}
}
return null;
}

private Map<String, Object> createPerson(Integer personId, String firstName, String lastName, String mail, Integer jobId){
Map<String, Object> personMap = new HashMap<String, Object>();

personMap.put("PersonId", personId);
personMap.put("FirstName", firstName);
personMap.put("LastName", lastName);
personMap.put("JobId", jobId);

if(mail != null) {
personMap.put("Email", mail);
}else {
personMap.put("Email", "Unknown"); //default value which we've specified in edmx file
}

peopleList.add(personMap);

return personMap;
}

}
2 Comments