Skip to Content

The following steps will explain how to introduce caching to your application using the SAP S/4HANA Cloud SDK. If you want to follow this tutorial, we highly recommend checking out the previous parts of this series, especially the article about resilience with Hystrix based commands.

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

This blog post covers the following steps:

  1. Explain what caching is and why you should care about it.
  2. Cache repetitive calls to the OData Service by using CachingErpCommand.
  3. Cache parameterized calls to the OData Service.

Caching

Sometimes service calls from your application to external servers turn out to be quite expensive in terms of performance and latency. Further evaluation of the queried data can make things even worse once critical response times are reached for the clients and customers.

To improve responsiveness to the users, the data requested internally by your application can often be stored for subsequent calls. This can be achieved in such a way, that for every request the information previously stored by the application can be re-used. This general behavior is called a cache. A cache stores copies of information passing through it. Besides serving improved responsiveness, the technical goal is to reduce the overall required bandwidth and processing requirements, thus eventually lessening the sever load and perceived lag. This way the amount of information, that needs to be transmitted across networks, can be reduced.

Caches are very important in a wide variety of use cases. It is one of the reasons for the advancements of our modern internet experience, like on-demand multimedia streaming and persistent cloud storage. Unlike web caches, which save whole request and response documents, an internal application cache serves the purpose of persisting interim data for multiple intended uses. Whenever information is expensive to compute or retrieve, and its value on a certain input is needed more than once, a cache should be considered.

How does it work?

A cache generally works by the action of requesting information to a given subject, called a key. If an information to a given key was previously requested, since stored and now available to read, a so called “cache hit” occurs: the data can be found and will be loaded. A “cache miss” occurs when it cannot.

The most important aspects of caches are their size and their item life time. Both should be limited with regards to the use case, to avoid an outdated state or disproportionate memory consumption in the application. The biggest effect of using a cache can be perceived, when the application is repetitively reading larger chunks of data from external sources. Then the fraction of bandwidth required for transmitting information will be reduced effectively.

Caching is applicable whenever:

  • You are willing to spend some memory to improve speed.
  • You expect that keys will sometimes get queried more than once.
  • Your cache will not need to store more data than what would fit in RAM. (By default, the cache is local to a single run of your application. It does not store data in files, or on outside servers.)

If each of these options apply to your use case, then we highly recommend the caching features provided by SAP S/4HANA Cloud SDK.

Caching Command with Cloud SDK

The command cache allows parallel execution and asynchronous computation for efficient programming practices. Stored information is organized as a local key-value store. For each unique key the same response is expected in case of a “cache hit”. Otherwise the cache response will be computed.

As underlying technology Guava is being used, which gives you options to customize the cache configuration.

Cache size Number of items being stored by the cache. If the number of items exceeds this limit, the most outdated value will be removed from the cache.
Item expiration time Items are checked for their age. When the individual expiration time is exceeded, the value will be removed from the cache.
Concurrency level Expected number of threads in the application able to change the cache concurrently.
Eviction Size based and timed eviction (default), reference-based (weak keys, weak values, soft values).

 

Cache your OData call

Now that we have covered why caching is important and how it can help us improve performance and responsiveness, it’s finally time to introduce it into our application. In the last tutorial we created a simple ErpCommand that uses the SDK’s OData abstractions to retrieve costcenters from an ERP system while guaranteeing resilience provided by Hystrix. In order to make this call cacheable, we have to start with a new type extending CachingErpCommand. So first we will create the following class: GetCachedCostCentersCommand

import java.util.List;
 
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
 
import com.sap.cloud.sdk.cloudplatform.cache.CacheKey;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.s4hana.connectivity.CachingErpCommand;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.opensap.datamodel.s4.CostCenterDetails;
 
import lombok.NonNull;
 
public class GetCachedCostCentersCommand extends CachingErpCommand<List<CostCenterDetails>>
{
    private static final Cache<CacheKey, List<CostCenterDetails>> cache =
        CacheBuilder.newBuilder().build();
 
    public GetCachedCostCentersCommand( @NonNull final ErpConfigContext configContext )
    {
        super(GetCachedCostCentersCommand.class, configContext);
    }

    @Override
    protected Cache<CacheKey, List<CostCenterDetails>> getCache()
    {
        return cache;
    }
 
    @Override
    protected List<CostCenterDetails> runCacheable() throws ODataException {
        try {
            final List<CostCenterDetails> costCenters = ODataQueryBuilder
                .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                .build()
                .execute(getErpEndpoint())
                .asList(CostCenterDetails.class);
                     
            return costCenters;
        }
        catch( final Exception e) {
            throw new ODataException("Failed to get CostCenters from OData command.", e);
        }
    }
}

The GetCachedCostCentersCommand class inherits from CachingErpCommand, which is the SDK’s abstraction to provide easy to use cacheable Hystrix commands. To implement a valid CachingErpCommand we need to do the three things:

  1. Provide a constructor. Here we simply add a constructor that takes an ErpConfigContext as parameter.
  2. Override the runCacheable() method. As you might have noticed already, we can simply take the code we used to call our OData service from the previous blog posts, and put it into the method. You can even specify Java exceptions. So no changes needed!
  3. Override the getCache() method. Since it might be possible for you to use any cache utility, you need to instantiate the Cache object yourself and provide a method for the internal usage.

Now that we have a working command with enabled cache features, we can adapt our CostCenterServlet.

Note: With regard to the Hystrix example of the previous blog post, the only change will be a different class name.

import java.io.IOException;
import java.util.List;
 
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 com.google.gson.Gson;
 
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.connectivity.ErpDestination;
import com.sap.cloud.sdk.s4hana.serialization.SapClient;
 
 
@WebServlet( "/costcenters" )
public class CostCenterServlet extends HttpServlet
{
    private static final long serialVersionUID = 1L;
 
    @Override
    protected void doGet(
        final HttpServletRequest request,
        final HttpServletResponse response )
    throws ServletException, IOException
    {
        final SapClient sapClient =
            new SapClient("SAPCLIENT-NUMBER"); // adjust to your respective S/4HANA system

        final ErpConfigContext configContext =
            new ErpConfigContext(ErpDestination.getDefaultName(), sapClient);
 
        final List<CostCenterDetails> result =
            new GetCachedCostCentersCommand(configContext).execute();
 
        response.setContentType("application/json");
        response.getWriter().write(new Gson().toJson(result));
    }
}

Just like in the last blog posts, the first thing we to do is initializing an ErpConfigContext. We simply create a new command, provide it with the ErpConfigContext and execute. As before, we get a list of costcenters as result. It will not fail if the OData service is temporarily unavailable. And now we can have multiple successive calls, but still only request the first one and have the cache to read for the following.

Note: Instead of “execute()” you are now also able to run queue() and observe() for asynchronous evaluation.

Cache configuration and parameterized calls

Now let’s take a look into the important features of the cache, the parameterized cache calls.

You might be working with queries using result changing parameters, like filters for OData requests. A new class declaration for every possible command variant will be impossible. That’s why we encourage developers to pay respect to the cache keys. The cache will dynamically adapt the provided key to your parameter delivery.

For example, given the stated CostCenter command, it would be useful to have an option to filter CostCenter items by a given CompanyCode. Especially when having an option to choose from on a website. Applying a cache will subsequently lessen computation costs. Please find the following class: GetCachedCostCentersByCompanyCodeCommand

import java.util.Collections;
import java.util.List;
 
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
 
import com.sap.cloud.sdk.cloudplatform.cache.CacheKey;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.s4hana.connectivity.CachingErpCommand;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.opensap.datamodel.s4.CostCenterDetails;
 
import lombok.NonNull;
 
 
public class GetCostCentersByCompanyCodeCommand extends CachingErpCommand<List<CostCenterDetails>>
{
    @NonNull
    private final String companyCode;
    public GetCostCentersByCompanyCodeCommand(
            @NonNull final ErpConfigContext configContext,
            @NonNull final String companyCode )
    {
        super(GetCostCentersByCompanyCodeCommand.class, configContext);
        this.companyCode = companyCode;
    }
 
    private static final Cache<CacheKey, List<CostCenterDetails>> cache =
        CacheBuilder.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(60, TimeUnit.SECONDS)
                .concurrencyLevel(10)
                .build();
 
    @Override
    protected Cache<CacheKey, List<CostCenterDetails>> getCache()
    {
        return cache;
    }
 
    @Override
    protected CacheKey getCommandCacheKey()
    {
        return super.getCommandCacheKey().append(companyCode);
    }
 
    @Override
    protected List<CostCenterDetails> runCacheable() throws ODataException {
        try {
            final List<CostCenterDetails> costCenters = ODataQueryBuilder
                .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                .filter(ODataProperty.field("CompanyCode").eq(ODataType.of(companyCode)))
                .build()
                .execute(getErpEndpoint())
                .asList(CostCenterDetails.class);
             
            return costCenters;
        }
        catch( final Exception e) {
            throw new ODataException("Failed to get CostCenters from OData command.", e);
        }
    }
     
    @Override
    protected List<CostCenterDetails> getFallback() {
        return Collections.emptyList();
    }
}

You see the following changes, compared to the simple cached-command example:

  • The constructor features a second parameter, a String representation of a CompanyCode parameter. This parameter is used in the OData query.
  • The cache instance now defines:
    maximum item size of 100 Up to a hundred costcenter queries and their responses will be cached.
    expiration after 60 seconds The item associated to any key which is older than a minute will be re-requested by the command.
    concurrency level of 10 This value provides a hint for the underlying caching API to estimate the number of threads trying to write into and change the cache at the same time. Concurrent reading access will be unaffected from this setting.
  • New methods have been overridden:
    getCommandCacheKey() It does append the provided companyCode to the cacheKey, thus making it distinguishable from the same service calls but with different companyCode parameter.
    getCommandFallback() In case the runCachable procedure fails in any way, we provide a fallback solution. In this example we return an empty list object.

To use the new class in the servlet, just read a parameter from the “request” input variable and use it for instantiating this parameterized command class. Feel free to try it out!

 

This wraps up the tutorial. Stay tuned for more tutorials on the SAP S/4HANA Cloud SDK on topics like UI5 and security!

To report this post you need to login first.

Be the first to leave a comment

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

Leave a Reply