Skip to main content

Use the OData v2 Type-safe Client API

Prerequisites

To leverage the type-safe OData v2 client, you need to add following dependencies to the dependencies section of your pom.xml:

<dependency>
<groupId>com.sap.cloud.sdk.s4hana</groupId>
<artifactId>s4hana-api-odata</artifactId>
</dependency>
Discover services on the SAP Business Accelerator Hub

If you would like to consume a specific service, try searching for it on the SAP Business Accelerator Hub and visit the SAP Cloud SDK tab on the landing page of that service. There you will find detailed instructions on how to use the SAP Cloud SDK to get a client library for that specific service. Learn more about the integration with the SAP Business Accelerator Hub in the blog.

Build and Execute OData Requests With the Typed OData Client

The typed OData v2 client allows building type-safe OData v2 requests for a given service. The Java classes represent the data model and the available operations of the service. As a consequence all requests that are built through the typed OData v2 client are not only syntactically valid but also semantically valid.

Generic OData Client

The type-safe client may in some situations not be suitable for your needs. For instance a service might not fully conform to the OData standard, forcing you to perform customized requests. Or the typed API doesn't support a specific OData feature just yet.

For such cases we recommend leveraging our low-level generic OData client. Or to fallback directly to the HTTP client level if that's not enough.

In all the other situations the type-safe client is preferable and considered best practice.

Versions of OData v2 Client

The SAP Cloud SDK has two versions of type-safe OData v2 client.

use Tabs to see the API difference

SAP Cloud SDK 3.29.1 releases the productive version of the new, improved OData client. Consequently, the APIs of the old OData client have been deprecated.

The New Improved Client

As of SAP Cloud SDK 3.29.1, we offer a generally available improved OData client.

  • High-performance thanks to optimized handling of metadata (fewer network requests)
  • Developed fully In house and shares code with OData v4 client
  • Allows for faster feature development and bug fixes
  • Has standard and advanced APIs for typed and untyped client
  • More and better-implemented features

The Deprecated Old Client

As of SAP Cloud SDK 3.29.1, we have deprecated the old OData client.

  • Stable and well tested
  • Based on 3rd party dependencies, which slows down the release of advanced features
  • Doesn't share a code base with OData v4 client
  • Has good performance, but not optimized for optimal metadata handling

Which One To Choose?

  • We recommend using the new improved OData client for new development.
  • For existing code, we recommend adjusting your code to switch to the new OData client.
use Tabs to see the API difference

Toggle between version Tabs in this document to see the API difference between these two OData v2 clients. Where API is the same we provide a universal code snippet

Using the Fluent API

The typed OData client consists of service and data model classes. The service classes mirror the API provided by the OData service and serve as entry point for creating requests. They provide a builder that allows for fluently adding further parameters.

To execute HTTP requests the OData client leverages Destinations and are documented in more detail here. The following code snippets assume that such a destination is in place:

HttpDestination destination;

On an abstract level requests are generally build up according to the following pattern:

result = service.operation()
.withParameter(...)
.execute(destination);
  • operation corresponds to the service's capabilities for entities e.g. createEntity or readEntities.
  • withParameter corresponds to:
    • OData query parameters e.g. filter or orderby
    • Or other modifiers like custom headers
  • Which OData parameters are available depends on the operation. For example, when updating entities the $filter parameter is not available.

Below different OData features are documented using the Business Partner Service on SAP S/4HANA as an example. It is represented by the BusinessPartnerService class which is part of the pregenerated SAP S/4HANA Virtual Data Model (VDM). The following code snippets assume that an instance of this service is set up:

BusinessPartnerService service = new DefaultBusinessPartnerService();

OData Features

Basic CRUD Operations

Create, Read, Update and Delete operations on entities are build from the associated service class:

service.createBusinessPartner(partner);
service.getBusinessPartnerByKey("id");
service.getAllBusinessPartner();
service.updateBusinessPartner(partner);
service.deleteBusinessPartnerAddress(address);

Each of the above statements returns a builder object that allows for specifying certain request parameters, depending on the operation.

The following query parameters and request options are available for these operations:

Query parameters:

  • $select and $expand are available on reading a single or multiple entities
  • $filter, $top, $skip and $orderby are available only when reading a collection of entities

Request parameters:

  • All operations allow for adding custom headers via withHeader(...)
  • Update operations allow to set either modifyingEntity() or replacingEntity() which will result in HTTP PATCH or HTTP PUT respectively. By default, entities are modified via PATCH.

Handling of ETags

An ETag is a version identifier that is often used to implement an optimistic locking mechanism. The SAP Cloud SDK will try to read version identifiers from responses and set them when sending OData requests that manipulate data.

Consider the following example:

partner = service.getBusinessPartnerByKey("id")
.executeRequest(destination);
response = service.updateBusinessPartner(partner)
.executeRequest(destination);
// update the partner reference
partner = response.getModifiedEntity();

On the read request, the SAP Cloud SDK will automatically try to extract the version identifier from the response and store it within the partner object. When updating it will be taken from there and sent with the If-match header.

note

If a service requires this header to be sent: Fetching the entity from the service first is essential to ensure that the ETag is present and up to date.

This behavior can be customized. The following code sends a * in the IF-MATCH header which essentially corresponds to a forced overwrite.

service.updateBusinessPartner(partner)
.matchAnyVersionIdentifier()
.executeRequest(destination);

Handling of Cross-Site Request Forgery Tokens

For create, update and delete requests the SAP Cloud SDK will try to send a cross-site request forgery (CSRF) token. Upon execution, the request will try to fetch a token first before issuing the create request. Many services require this behavior for security reasons. However, the create request will be made without a CSRF token if none could be obtained.

Select

When reading entities, the API offers select( ... ) on the builders. Through it, the query parameters $select and $expand are set. It takes in the properties of the entity being queried. Primitive properties are added to $select while complex and navigational properties are added to $expand. This handling is done automatically by the SAP Cloud SDK.

The properties that can be selected or expanded are represented via static fields on the entity class. There will be a field for each property. E.g. for the business partner entity one can find BusinessPartner.FIRST_NAME and BusinessPartner.LAST_NAME.

service.getBusinessPartnerByKey("id")
.select(BusinessPartner.FIRST_NAME, BusinessPartner.LAST_NAME, BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS)
.executeRequest(destination);

The above translates to the following query parameters:

$select=FirstName,LastName,to_BusinessPartnerAddress/*&$expand=to_BusinessPartnerAddress

One can also apply select again to the expanded object:

service.getBusinessPartnerByKey("id")
.select(BusinessPartner.FIRST_NAME,
BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS
.select(BusinessPartnerAddress.ADDRESS_ID,
BusinessPartnerAddress.CITY_CODE))
.executeRequest(destination);

The above translates to the following query parameters:

$select=FirstName,to_BusinessPartnerAddress/AddressID,to_BusinessPartnerAddress/CityCode&$expand=to_BusinessPartnerAddress

Filter

When operating on a collection of entities, the API offers filter( ... ) on the builders. It directly corresponds to the $filter parameter of the request. Filters are also built via the static property fields on entities.

The following example:

/*
Get all business partners that either:
- Have first name 'Alice' but not last name 'Bob'
- Or have first name 'Mallory'
*/
service.getAllBusinessPartner()
.filter(BusinessPartner.FIRST_NAME.eq("Alice")
.and(BusinessPartner.LAST_NAME.ne("Bob"))
.or(BusinessPartner.FIRST_NAME.eq("Mallory"))
)
.executeRequest(destination);

Will translate to this filter parameter:

$filter=(((FirstName eq 'Alice') and (LastName ne 'Bob')) or (FirstName eq 'Mallory'))

Take note of the order of and and or. As or is invoked on the result of and it will form the outer expression while and is an inner expression in the first branch of or.

To achieve a different order with and as the top level statement one would nest the or within and(...):

.and(BusinessPartner.LAST_NAME.ne("Bob")
.or(BusinessPartner.FIRST_NAME.eq("Mallory"))
)
.and(BusinessPartner.LAST_NAME.ne("Bob")
.or(BusinessPartner.FIRST_NAME.eq("Mallory"))
)

Available Filter Expressions

The OData v2 standard allows for a limited range of filter expressions compared to OData v4. A detailed list of what is available in the SAP Cloud SDK can be obtained from the Javadoc. The functionality can also be discovered through the fluent API.

Custom Filter Expressions

The SAP Cloud SDK for Java also allows for providing custom filter expressions. This is useful for instance when the fields to filter by are not known at design time and have to be passed dynamically.

For example, to filter based on a field ShoeSize use:

ValueBoolean filterExpression = FieldReference.of("ShoeSize").equalTo(42);
UncheckedFilterExpression<MyEntity> customFilter =
new UncheckedFilterExpression<>(filterExpression);

// use the custom filter in the .filter() method of your request builder
myService.getAllBusinessPartner().filter(customFilter).executeRequest(destination);

Which produces the HTTP query:

?$filter=(ShoeSize eq 42)

Accessing Extensible or Custom Fields

The concept of In-app Extensibility allows users to introduce additional fields to an existing SAP S/4HANA service. To consume and work with such custom fields in your application code, SAP Cloud SDK introduces the concept of custom fields.

You can set custom fields on an entity through the .setCustomField method and read the value in the entity using .getCustomField(fieldName)

businessPartner.setCustomField("shoeSize", 10);

final int shoeSize = businessPartner.getCustomField("shoeSize");

If you are unsure about the custom fields added by the customer, you can still use getCustomFieldNames() to obtain the set of custom fields introduced. Use this set further on to pull values from the entity.

For e.g.

final Set<String> customfieldNames = businessPartner.getCustomFieldNames();

for(String customFieldName: customfieldNames){
final Object customField = businessPartner.getCustomField(customFieldName);
}

The accessors for custom fields respect multitenancy. getCustomFieldNames(), for example, fetches only custom field names available in the context of the current tenant.

Using Custom Fields in System Query Options

You can also pass custom fields to select or filter system query options:

service().getAllBusinessPartner()
.select(new BusinessPartnerField<>("ShoeSize"))
.select(()->"AnotherCustomField")
.filter(new BusinessPartnerField<>("Shoesize").eq(42))
.executeRequest(destination)

Please be aware that such an example can work for all tenants only if ShoeSize and AnotherCustomField are custom fields that have been defined by all tenants.

Batch Requests

Batch requests allow wrapping multiple OData requests into one single batch call. Thereby we reduce the number of round trips to the remote server. Refer to the official OData V2 spec for further reference about batch requests, their semantics, and the request/response format.

Execute Batch Request

The service object offers the method batch which allows access to the batch-related methods that help build the batch request.

Multiple single requests like createBusinessPartnerAddress and others can be wrapped into an OData BATCH ChangeSet. OData BATCH changesets are meant to wrap transactional requests together for better performance. OData Function Import can be also used inside OData Batch changesets, for that use the method addFunctionImport(...). Each changeset is opened with beginChangeSet and closed with endChangeSet. You can wrap multiple changesets into one batch request.

Non-modification or in other words Read requests can be added outside changesets with the addReadOperations method.

note

Please be aware that function imports using GET cannot be included inside a Batch changeset using addFunctionImport(...) Add them into batch requests outside changesets using addReadOperations method.

Use executeRequest to issue the batch request to the remote system. We receive an instance of BatchResponse as the result object.

BusinessPartnerAddress addressToCreate1;
BusinessPartnerAddress addressToCreate2;

BusinessPartnerFluentHelper requestTenEntities = service.getAllBusinessPartner().top(10);
BusinessPartnerByKeyFluentHelper requestSingleEntity = service.getBusinessPartnerByKey("bupa9000");
BusinessPartnerFictionalFluentHelper callFictionalFunction = service.fictionalFunction(); //Assuming this fictional function import uses GET

BatchResponse result =
service
.batch()
.beginChangeSet()
.createBusinessPartnerAddress(addressToCreate1)
.createBusinessPartnerAddress(addressToCreate2)
.endChangeSet()
.addReadOperations(requestTenEntities)
.addReadOperations(requestSingleEntity)
.addReadOperations(fictionalFunction)
.executeRequest(destination);

Alternatively, you can also assemble changesets from individual requests with addChangeSet. This option allows you to further configure your requests by, for example, attaching custom headers to special parts of your changeset like so:

BusinessPartnerAddress addressToCreate;
BusinessPartnerAddress addressToUpdate;
BusinessPartnerAddress addressToDelete;

BusinessPartnerAddressCreateFluentHelper createRequest =
service
.createBusinessPartnerAddress(addressToCreate)
.withHeader("header-for-create", "create value");
BusinessPartnerAddressUpdateFluentHelper updateRequest =
service
.updateBusinessPartnerAddress(addressToUpdate)
.withHeader("header-for-update", "update value");
BusinessPartnerAddressDeleteFluentHelper deleteRequest =
service
.deleteBusinessPartnerAddress(addressToDelete)
.withHeader("header-for-delete", "delete value");

BatchResponse result =
service
.batch()
.addChangeSet(createRequest, updateRequest, deleteRequest)
.executeRequest(destination);

Access Batch Response

In OData Batch, the response is structured similarly to the request. The unordered responses of data modification requests are accessible with a given changeset index. The ordered responses of data retrieval requests are accessible with the reference of the original request object.

Changeset Response

Use the method get to access the individual changeset responses by index.

Try<BatchResponseChangeSet> changeSetTry = result.get(0);

The returned Try allows checking if the respective changeset was processed successfully by the remote server. Per the specification, either all operations or none within the same changeset are successful. Hence, if changeSetTry.isSuccess() is true, you can derive that all operations in the changeset are successful.

In case the changeset contained at least one create operation, you can gain access to the created entity representations by using the method getCreatedEntities.

if (changeSetTry.isSuccess()) {
//now you can access the created entities
final List<VdmEntity<?>> createdEntities = changeSet.get().getCreatedEntities();
}
note

You have to cast the entity representations from the generic type VdmEntity to the respective subclasses.

Read Response

Use the method getReadResult to extract the deserialized response of a given read operation:

List<BusinessPartner> entities = result.getReadResult(requestTenEntities);
BusinessPartner entity = result.getReadResult(requestSingleEntity);
List<functionResult> functionResult = result.getReadResult(callFictionalFunction);

You can use the same methods to extract deserialized responses of function imports that use GET.

Instead of an index to indicate the batch position, the API expects the original request reference. The request-response mapping and deserialization are happening internally.

Entity Update Strategies

The SAP Cloud SDK supports different strategies for updating entities which differ in the HTTP method and the payload of the update request.

info

To update an entity it should first be retrieved from the service. This ensures the latest state of the entity is updated. Otherwise, updating with old data could erase changes that have been made to the entity in the meantime. Many services will enforce this behavior with optimistic locking mechanisms. Refer to the section about ETags for details.

The default strategy is the modifying entity update strategy which attempts to modify only the necessary entity fields in the remote system. It issues a PATCH request and includes only the fields in the request payload whose values were changed by its setter method. Hence, fields whose values remain unchanged are not sent to the target system. Calling the method includingFields(fields ...) instructs the SAP Cloud SDK to add the mentioned fields explicitly in the update request. You could also add navigation properties to your request using the method. This is useful for backend systems that require some unchanged fields in the request payload for given reasons.

This update strategy can be explicitly chosen by invoking the method modifyingEntity() while building the update request.

service.updateBusinessPartner(partner)
//method call optional, since active by default
.modifyingEntity()
//add the Business Partner full name and address explicitly to the update request
.includingFields(BusinessPartner.BUSINESS_PARTNER_FULL_NAME,BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS)
.executeRequest(destination);
note

It depends on the capabilities of the specific OData service in the remote system which update strategies are supported in an end-to-end scenario. For example, there are cases where OData services do not support PUT requests.

Error Handling

Sometimes requests fail and the SAP Cloud SDK provides a flexible way to deal with such failures on multiple levels. All executeRequest methods may throw a runtime exception (extending) ODataException. This will always contain the request which was (attempted to be) sent out as well as the cause of the exception. To handle all kind of failures consider the following code:

try { ... }
catch( final ODataException e ) {
ODataQueryGeneric query = e.getQuery();
logger.debug("The following query failed: {}", query);
// do something else
}

This handling is the most generic, handling all possible failures. For more specific information dedicated exceptions are inheriting from ODataException. Please check the documentation for details on all the exception types.

To handle different kinds of failure one can list multiple catch clauses to cover different levels or cases that might occur, e.g.:

try { ... }
catch( ODataServiceErrorException e ) {
// handle the specific error message from the response payload
ODataServiceError odataError = e.getODataError();
logger.debug("The OData service responded with an error: {}", odataError);
} catch( ODataDeserializationException e ) {
// handle failures in deserialization
} catch( ODataResponseException e ) {
// handle all other errors originating from handling the HTTP response
int httpCode = e.getHttpCode();
Collection<Header> httpCode = e.getHttpHeaders();
Option<String> httpBody = e.getHttpBody();
}

A navigation property describes a unidirectional relationship between two entity types. Like other properties, it has a name and declares a multiplicity, i.e. whether to expect a single or multiple values. Additionally, a navigation property allows for dedicated CRUD operations, that may not be exposed by default on entity sets of the service root. Such operations also provide a convenient way to access the nested resources of entities.

The typed OData client for OData v2 supports the following operations on (first-level only) navigation properties:

  • Create

The below example leverages the creation of a nested entity in relation to an existing entity:

/*
Create a new address for a specific business partner.
*/
BusinessPartner businessPartnerById = BusinessPartner.builder().businessPartner("123").build();
BusinessPartnerAddress addressItem = BusinessPartnerAddress.builder().country("DE").build();

service.createBusinessPartnerAddress( addressItem )
.asChildOf( businessPartnerById, BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS )
.executeRequest( destination );

This sample API call translates to the following service request:

POST /ODataService/API_BUSINESS_PARTNER/A_BusinessPartner(123)/to_BusinessPartnerAddress
{
"country": "de"
}

Server-Driven Paging

Pagination describes the practice of splitting a collection of entities from reading requests into one or many pages. The paging behavior is determined by both the server and the client.

OData service operators may decide to enable server-driven pagination to limit the amount of data that is fetched and sent over the network to the client. Generic query options can yield huge amounts of data. By splitting the result-set into sequential pages of entities, the data can be requested incrementally. This reduces initial network load and improves overall response times. If the payload of an OData response contains a link including a $skiptoken, then it indicates a next page to the result-set. The iterable result-set is a consistent snapshot of the data, it can not change between reading individual pages.

In comparison, the OData consumer may use query options $top and $skip (client-driven paging) to read partial data from the result-set. But relying on a consistent state while browsing through the data can be problematic. Between individual requests, another user could delete or add an item. This would result in an inconsistent aggregation of data.

By default, the SAP Cloud SDK automatically resolves all pages of a result-set if server-driven paging is encountered. For the API consumer, it is not necessary to parse the next link and instantiate follow-up requests to aggregate the full result-set.

List<BusinessPartner> iterablePagesOfEntities = service
.getAllBusinessPartner()
.executeRequest( destination );

In case memory efficiency and response time of the consuming application becomes a priority, then the advanced API provides additional means to manually iterate through the internal pages. While accessing the following methods, the internal HTTP requests are executed lazily:

Iterable<List<BusinessPartner>> iterablePagesOfEntities = service
.getAllBusinessPartner()
.iteratingPages()
.executeRequest( destination );

Iterable<BusinessPartner> iterableEntities = service
.getAllBusinessPartner()
.iteratingEntities()
.executeRequest(destination);

Stream<BusinessPartner> streamingEntities = service
.getAllBusinessPartner()
.streamingEntities()
.executeRequest(destination);

The request builder allows for setting the optional parameter for preferred page size, e.g.:

Iterable<List<BusinessPartner>> iterablePagesOfEntities = service
.getAllBusinessPartner()
.withPreferredPageSize(20)
.iteratingPages()
.executeRequest( destination );
caution

Please note that the OData service is not obliged to respect this setting.

Switch to Improved OData v2 Type-Safe Client

  • For any operation type, replace a call to execute with executeRequest.
service.createBusinessPartner(partner).executeRequest(destination);
  • For Create and Update operations, call getModifiedEntity() to obtain the created entity representation.
final BusinessPartner partner = service.createBusinessPartner(partner)
.executeRequest(destination)
.getModifiedEntity();
  • executeRequest throws the runtime exception ODataException and respective subclasses. Hence, adjust your exception handling as described under the tab OData V2 (Beta) in the section Error Handling. Note that any ErrorResultHandler registered by withErrorHandler() is not considered.
  • Remove any call to cachingMetadata() and withoutCachingMetadata() as they are obsolete.
  • Remove any call to ignoringVersionIdentifier(boolean) or ignoreVersionIdentifier(). Use matchAnyVersionIdentifier() or disableVersionIdentifier() instead.