Skip to Content

The following steps will explain how to use the SAP S/4HANA Cloud SDK’s Virtual Data Model (VDM) to simplify the communication with your SAP S/4HANA System.

Note: This post is part of a series. For a complete overview visit the SAP S/4HANA Cloud SDK Overview.

Goal of this blog post

In this blog post we will take a look at how the Virtual Data Model simplifies reading and writing data from an SAP S/4HANA system and the advantages it brings compared to the previously described approach.

We will also take a look at the example application from Step 5: Resilience to demonstrate how to use the OData VDM in your SAP S/4HANA Cloud application on SAP Cloud Platform.

If you want to follow this tutorial, we highly recommend checking out these previous tutorials:

For a complete overview visit the SAP S/4HANA Cloud SDK Overview.

Note: This tutorial requires access to an SAP ERP system as explained in Step 4.

Virtual Data Model

The data stored in an S/4HANA system is inherently complexly structured and therefore difficult to query manually. Therefore, HANA introduced a Virtual Data Model (VDM) that aims to abstract from this complexity and provide data in a semantically meaningful and easy to consume way. The preferred way to consume data from an S/4HANA system is via the OData protocol. While BAPIs are also supported for compatibility reasons, OData should always be your first choice. You can find a list of all the available OData endpoints for S/4HANA Cloud systems in SAP’s API Business Hub.

The S/4HANA Cloud SDK now brings the VDM for OData to the Java world to make the type-safe consumption of OData endpoints even more simple! The VDM is generated using the information from SAP’s API Business Hub. This means that it’s compatible with every API offered in the API Business Hub and therefore also compatible with every S/4HANA Cloud system.

The old way to OData

Let’s take a look at the code we have written in Step 4: Calling an OData Service where we retrieved a list of CostCenters from an S/4HANA system:

final ErpEndpoint endpoint = new ErpEndpoint();
final List<CostCenterDetails> costCenters = ODataQueryBuilder
    .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
    .select("CostCenterID", "Status", "CompanyCode", "Category", "CostCenterDescription")
    .build()
    .execute(endpoint)
    .asList(CostCenterDetails.class);

The ODataQueryBuilder represents a simple and generic approach to consuming OData services in your application and is well suited to support arbitrary services. However, there are quite a few pitfalls you can fall into.

For .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection") you already need to know three things: the OData endpoints service path (/sap/opu/odata/sap), the endpoints name (FCO_PI_COST_CENTER) and the name of the entity collection (CostCenterCollection) as defined in the endpoints metadata.

Then, when you want to select specific attributes from the CostCenter entity with the select() function, you need to know how these fields are called. But since they are only represented as strings in this code, you need to look at the metadata to find out how they’re called. The same also applies for functions like order() and filter(). And of course using strings as parameters is prone to spelling errors that your IDE most likely won’t be able to catch for you.

Finally, you need to define a class such as CostCenterDetails with specific annotations that represents the properties and their types of the result. For this you again need to know a lot of details about the OData service.

Virtual Data Model: The new way to OData

Now that we have explained the possible pitfalls of the current aproach, let’s take a look at how the OData VDM tackles the same task.

import static com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.ReadCostCenterDataNamespace.CostCenter;

final List<CostCenter> costCenters =
    new DefaultReadCostCenterDataService().getAllCostCenter()
        .select(
            CostCenter.COMPANY_CODE,
            CostCenter.COST_CENTER_ID,
            CostCenter.STATUS,
            CostCenter.CATEGORY,
            CostCenter.COST_CENTER_DESCRIPTION)
        .execute(erpConfigContext);

Using the OData VDM we now have access to an object representation of a specific OData service, in this case the DefaultReadCostCenterDataService (default implementation of the interface ReadCostCenterDataService). So now there’s no more need to know the endpoint’s service path, service name or entity collection name. We can call this service’s getAllCostCenter() function to retrieve a list of all the cost centers from the system.

Now take a look at the select() function. Instead of passing strings that represent the field of the entity, we can simply use the static fields provided by the CostCenter class. So not only have we eliminated the risk of spelling errors, we also made it type-safe! Again, the same applies for filter() and orderBy(). For example, filtering to a specific company code becomes as easy as .filter(CostCenter.COMPANY_CODE.eq(companyCode)).

An additional benefit of this approach is discoverability. Since everything is represented as code, you can simply use your IDE’s autocompletion features to see which functions a service supports and which fields an entity consists of: start by looking at the different services that are available in the package com.sap.cloud.sdk.s4hana.datamodel.odata.services, instantiate the default implementation of the service you need (class name prefixed with Default), and then look for the methods of the service class that represent the different available operations. Based on this, you can choose the fields to select and filters to apply using the fields of the return type.

Each service is described by a Java interface, for example, ReadCostCenterDataService. The SDK provides a default, complete implementation of each service interface. The corresponding implementation is available in a class whose name is the name of the interface prefixed with Default, for example, DefaultReadCostCenterDataService. You can either simply instantiate that class, or use dependency injection with a corresponding Java framework (we may cover dependency injection in a future blog post). The benefit of the interfaces is better testing and extensibility support.

To sum up the advantages of the OData VDM:

  • No more hardcoded strings
  • No more spelling errors
  • Type safety for functions like filter, select and orderBy
  • Java data types for the result provided out of the box, including appropriate conversions
  • Discoverability by autocompletion
  • SAP S/4HANA services can be easily mocked during testing based on the service interface in Java

Currently the VDM supports retrieving entities by key and retrieving lists of entites along with filter()select()orderBy()top() and skip(). You can also resolve navigation properties on demand and use function imports. Future releases will bring even more enhancements to its functionality.

For all the use cases not yet supported by the VDM and for OData services not part of SAP’s API Business Hub, the ODataQueryBuilder still is the goto approach for consumption.

Use the VDM for your resilient OData call

To finish this blog post, let’s take a look at how to use the VDM in our example application. The code below shows the GetCostCentersCommand we have written in Step 5: Resilience with Hystrix.

package com.sap.cloud.sdk.tutorial;

import org.slf4j.Logger;
 
import java.util.List;
import java.util.Collections;
 
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.frameworks.hystrix.HystrixUtil;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.s4hana.connectivity.ErpCommand;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
 
public class GetCostCentersCommand extends ErpCommand<List<CostCenterDetails>>
{
    private static final Logger logger = CloudLoggerFactory.getLogger(GetCostCentersCommand.class);
 
    protected GetCostCentersCommand( final ErpConfigContext configContext )
    {
        super(
            HystrixUtil
                .getDefaultErpCommandSetter(
                    GetCostCentersCommand.class,
                    HystrixUtil.getDefaultErpCommandProperties().withExecutionTimeoutInMilliseconds(5000))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(20)),
            configContext);
    }
 
    @Override
    protected List<CostCenterDetails> run()
        throws Exception
    {
        final List<CostCenterDetails> costCenters =
            ODataQueryBuilder
                .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                .select("CostCenterID", "Status", "CompanyCode", "Category", "CostCenterDescription")
                .build()
                .execute(getConfigContext())
                .asList(CostCenterDetails.class);
 
        return costCenters;
    }
 
    @Override
    protected List<CostCenterDetails> getFallback() {
        return Collections.emptyList();
    }
}

To use the VDM we simply need to replace the usage of the ODataQueryBuilder with a call to the corresponding service class from the OData VDM.

The resulting code looks like this (if you are using caching as described in Tutorial Step 6, apply similar changes to the GetCachedCostCentersCommand):

package com.sap.cloud.sdk.tutorial;

import org.slf4j.Logger;
 
import java.util.Collections;
import java.util.List;

import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.frameworks.hystrix.HystrixUtil;
import com.sap.cloud.sdk.s4hana.connectivity.ErpCommand;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.readcostcenterdata.CostCenter;
import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultReadCostCenterDataService;
import com.sap.cloud.sdk.s4hana.datamodel.odata.services.ReadCostCenterDataService;
 
public class GetCostCentersCommand extends ErpCommand<List<CostCenter>>
{
    private static final Logger logger = CloudLoggerFactory.getLogger(GetCostCentersCommand.class);
 
    protected GetCostCentersCommand( final ErpConfigContext configContext )
    {
        super(
            HystrixUtil
                .getDefaultErpCommandSetter(
                    GetCostCentersCommand.class,
                    HystrixUtil.getDefaultErpCommandProperties().withExecutionTimeoutInMilliseconds(5000))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(20)),
            configContext);
    }

    @Override
    protected List<CostCenter> run()
        throws Exception
    {
        final List<CostCenter> costCenters = new DefaultReadCostCenterDataService().getAllCostCenter()
                .select(CostCenter.COST_CENTER_ID,
                        CostCenter.STATUS,
                        CostCenter.COMPANY_CODE,
                        CostCenter.CATEGORY,
                        CostCenter.COST_CENTER_DESCRIPTION)
                .execute(getConfigContext());
 
        return costCenters;
    }

    @Override
    protected List<CostCenter> getFallback() {
        return Collections.emptyList();
    }
}

Now we only need to slightly adapt our CostCenterServlet to account for the changes to the return type of the command:

package com.sap.cloud.sdk.tutorial;

import com.google.gson.Gson;
import org.slf4j.Logger;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.readcostcenterdata.CostCenter;

@WebServlet("/costcenters")
public class CostCenterServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = CloudLoggerFactory.getLogger(CostCenterServlet.class);
 
    @Override
    protected void doGet( final HttpServletRequest request, final HttpServletResponse response )
        throws ServletException,
            IOException
    {
        final ErpConfigContext configContext = new ErpConfigContext();
 
        final List<CostCenter> result = new GetCachedCostCentersCommand(configContext).execute();
 
        response.setContentType("application/json");
        response.getWriter().write(new Gson().toJson(result));
    }
}

Finally, you can now also safely remove the CostCenterDetails class, because we are using the Java type provided by the virtual data model instead.

Before running the integration tests, you need to make two small adjustments:

  • In the GetCostCentersCommandTest, change the return type of the method getCostCenters to List<CostCenter> and import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.readcostcenterdata.CostCenter.
  • In the file integration-tests/src/test/resources/costcenters-schema.json, change the property required to "required": ["costCenterID", "companyCode"].

Deploy to Cloud Foundry

Finally, we can rebuild our application and deploy it to CloudFoundry using the following commands:

mvn clean install
cf push

Make sure you add the destination for your S/4HANA system to your application’s environment variables, using either CloudFoundry’s cockpit or the cf command line tool (pay attention to quote escaping when using the command line).

cf set-env firstapp destinations '[{name: "ErpQueryEndpoint", url: "https://URL", username: "USER", password: "PASSWORD"}]'

Also, if you haven’t secured your application yet (as described in Step 7: Secure your application on SCP CloudFoundry), make sure to also add the following to your application’s environment variables, but be aware of the security implications outlined in Step 5 of this series:

cf set-env firstapp ALLOW_MOCKED_AUTH_HEADER true

Now you have the application up and running on SAP Cloud Platform Cloud Foundry using the SAP S/4HANA Virtual Data Model provided by the SAP S/4HANA Cloud SDK. The already previously available /costcenters API now uses a type-safe approach to access SAP S/4HANA. Adding additional functionality to this integration is much easier thanks to the easily discoverable Virtual Data Model – try it yourself and for example add a filter to the cost center query or implement a second API for business partners.

This wraps up today’s blog post! Stay tuned for the upcoming blog posts about the S/4HANA Cloud SDK on topics like Continuous Delivery or Testing! In the meantime, experiment yourself with the advanced features of the Virtual Data Model.

 

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

  1. Henning Heitkoetter Post author

    Change log (November 9, 2017):

    •  Updated to account for changes in version 1.3.0 of the SAP S/4HANA Cloud SDK (see release notes)
      • Instantiate new DefaultReadCostCenterDataService() instead of static ReadCostCenterDataService
      • Minor update to CostCenter package and field name
    • Explain benefits of new service interfaces in VDM
    (0) 

Leave a Reply