Typed client to consume OData v2 Services for Java

Build and execute OData Requests with the typed OData client#

The typed OData v2 client allows to build 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 build through the typed OData v2 client are not only syntactically valid but also semantically valid.

Versions of OData v2 client#

The 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 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 which allows for adding further parameters in a fluent way.

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 S/4HANA as an example. It is represented by the BusinessPartnerService class which is part of the pre-generated 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 which is often used to implement an optimistic locking mechanism. The 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 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 CSRF tokens#

For create, update and delete requests the SDK will try to send a CSRF token. Upon execution the request will try to fetch a token first before issuing the actual 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 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 SDK.

The properties that can be selected or expanded are represented via static fields on the entity class. So 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 build 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 SDK can be obtained from the Javadoc. The functionality can also be discovered through the fluent API.

Batch Requests#

Batch requests allow wrapping multiple OData requests into one single batch call. Thereby we reduce the number of roundtrips 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) can be wrapped into so-called change sets. Each change set is opened with beginChangeSet and closed with endChangeSet. You can wrap multiple change sets into one batch request.

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

final BatchResponse result =
service
.batch()
.beginChangeSet()
.createBusinessPartnerAddress(address1)
.createBusinessPartnerAddress(address2)
.endChangeSet()
.executeRequest(destination);

Access Batch Response#

Use the method get to access the individual change set responses by index.

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

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

In case the change set 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 sub classes.

Entity Update Strategies#

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

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 Cloud SDK to add the mentioned fields explicitly in the update request. This is useful for backend systems which require some unchanged fields in the request payload for given reasons.

This update strategy can be explicitly chosen 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 explicitly to the update request
.includingFields(BusinessPartner.BUSINESS_PARTNER_FULL_NAME)
.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 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 there are dedicated exceptions inheriting from ODataException. Please tend to the documentation for all the exception types.

In order 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
}

Navigation properties#

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 server and 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, in case generic query options 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. 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 sub classes. 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.
Last updated on by Alexander Dümont