Skip to Content

The following tutorial will introduce you to the latest features of the S/4HANA Cloud SDK regarding extensibility, eager and type-safe expand as well as dependency injection with the Virtual Data Model for OData for any 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

This blog post introduces to you the latest virtual data model features of the SAP S/4HANA Cloud SDK. After this blog you will be able to understand

  • How you can use custom field extensions from S/4HANA within the virtual data model for OData.
  • How you can join connected entities from the virtual data model in eager fashion.
  • How to leverage dependency injection to decouple your client code better from the SDK-provided classes.

Prerequisites

To successfully go through this tutorial you have to complete the tutorial at least until:

Furthermore, it is helpful to checkout the Virtual Data Model for OData upfront:

In addition, to use all features please update at least to S/4HANA Cloud SDK version 1.6.0. Dependency injection and extensibility works already as of version 1.5.0. Therefore, please make sure, your SDK Bill of Material is updated accordingly like shown below:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.sap.cloud.s4hana</groupId>
            <artifactId>sdk-bom</artifactId>
            <version>1.6.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
    <!-- possibly further managed dependencies ... -->
</dependencyManagement>

As the basis for all three features we will use the following GetBusinessPartnerCommand to the business partner API which has been wrapped into a resilient Hystrix command. The call itself requests the first- and lastnames from all business partners which are customers.

public class GetBusinessPartnerCommand extends ErpCommand<List<BusinessPartner>> {

    private BusinessPartnerService businessPartnerService;

    public GetBusinessPartnerCommand(ErpConfigContext erpConfigContext, BusinessPartnerService businessPartnerService) {
        super(GetBusinessPartnerCommand.class, erpConfigContext);
        this.businessPartnerService = businessPartnerService;
    }

    @Override
    protected List<BusinessPartner> run() throws Exception {

        try {

            return businessPartnerService.getAllBusinessPartner()
                        .filter(BusinessPartner.CUSTOMER.ne(""))
                        .select(BusinessPartner.FIRST_NAME,
                                BusinessPartner.LAST_NAME)
                        .execute(getConfigContext());

        } catch (final ODataException e) {
            throw new HystrixBadRequestException(e.getMessage(), e);
        }
    }
}

We will also use the following simple Servlet that consumes our GetBusinessPartnerCommand:

@WebServlet("/businessPartners")
public class BusinessPartnerServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        final List<BusinessPartner> businessPartnerList =
                new GetBusinessPartnerCommand(new ErpConfigContext(), new DefaultBusinessPartnerService())
                        .execute();

        final String jsonOutput = new Gson().toJson(businessPartnerList);
        response.getOutputStream().print(jsonOutput);
    }
}

Hint: Both files go to: <projectroot>/application/src/main/java in your generated project structure. Please put them either into the default package or some newly created package (e.g., com.yourcompany).

When we call this servlet on the server, we can see a result like this:

If you have a similar result, you are ready to proceed with this tutorial.

Custom Field Extensibility

Motivation

Oftentimes, businesses require to enhance the standard data model of the SAP S/4HANA system. Using tools of the so-called In-App Extensibility concept, key users are able to introduce additional fields to certain data structures. As an application provider, this mechanism can be also used to introduce new data fields which are relevant to your application extension.

Either way, we want to be able to consume and work with such custom fields in our application code.

How-to

In our example, we have enhanced the business partner data model with two custom fields to record by whom the business partner was originally proposed (field: YY1_ProposedBy_bus) and if the business partner was approved after proposal (field: YY1_ApprovedBy_bus). We did this modification by the so-called In-App extensibility capability of SAP S/4HANA which you can use based on your Fiori Launchpad with an authorized user. You can check out Thomas Schneider’s great blog for more details: The Key User Extensibility Tools of S/4 HANA.

Based on this extension we can, for example, check which business partners have to be approved and if they are, we consider them as valid business partners.

Based on these two extended fields, we can now use these two additional fields as part of our projection criteria. As we are only interested in extension fields from the business partner, the API only accepts extension fields typed under BusinessPartnerField.

public class GetBusinessPartnerCommand extends ErpCommand<List<BusinessPartner>> {
    
    private BusinessPartnerService businessPartnerService;

    public GetBusinessPartnerCommand(ErpConfigContext erpConfigContext, BusinessPartnerService businessPartnerService) {
        super(GetBusinessPartnerCommand.class, erpConfigContext);
        this.businessPartnerService = businessPartnerService;
    }

    @Override
    protected List<BusinessPartner> run() throws Exception {

        try {

            return businessPartnerService.getAllBusinessPartner()
                        .filter(BusinessPartner.CUSTOMER.ne(""))
                        .select(BusinessPartner.FIRST_NAME,
                                BusinessPartner.LAST_NAME,
                                new BusinessPartnerField<String>("YY1_ApprovedBy_bus"),
                                new BusinessPartnerField<String>("YY1_ProposedBy_bus"))
                        .execute(getConfigContext());

        } catch (final ODataException e) {
            throw new HystrixBadRequestException(e.getMessage(), e);
        }
    }
}

The only two lines we added to the initial example are:

  • new BusinessPartnerField<String>(“YY1_ApprovedBy_bus”)
  • new BusinessPartnerField<String>(“YY1_ProposedBy_bus”))

After deploying this again, we can now see that custom fields are correctly served:

 

Working programmatically with extension fields

Of course, extension fields cannot only be provided as part of GET requests to the API, but you can work programmatically with them. For example, on each object instance you can access which custom fields are defined and get their names leveraging the following methods:

  • entity.getCustomFieldNames();
  • entity.getCustomFields();
  • entity.setCustomField();

Example:

 

As a small demonstration, we would like to expose the clients of our application a nicer representation as we do not like these too technical extension field names.

Therefore, we write our own business partner entity called MyBusinessPartner that inherits from the provided BusinessPartner entity.

Here we map only some fields of the business partner entity into better readable names:

public class MyBusinessPartner extends BusinessPartner {

    @SerializedName("FullName")
    private String fullName;

    @SerializedName("Proposer")
    private String proposedBy;

    @SerializedName("Approver")
    private String approvedBy;

    public MyBusinessPartner(final BusinessPartner businessPartner) {
        this.fullName = businessPartner.getFirstName()+" "+businessPartner.getLastName();
        this.proposedBy = businessPartner.getCustomField("YY1_ProposedBy_bus");
        this.approvedBy = businessPartner.getCustomField("YY1_ApprovedBy_bus");
    }
}

In our servlet, we need to adapt the logic a bit to wrap and unwrap the original business partner into our own business partner entity:

@WebServlet("/businessPartners")
public class BusinessPartnerServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        final List<BusinessPartner> businessPartnerList =
                new GetBusinessPartnerCommand(new ErpConfigContext(), new DefaultBusinessPartnerService())
                        .execute();

        final List<MyBusinessPartner> myBusinessPartners = Lists.newArrayList();

        for (final BusinessPartner businessPartner : businessPartnerList) {
            myBusinessPartners.add(new MyBusinessPartner(businessPartner));
        }

        final String jsonOutput = new Gson().toJson(myBusinessPartners);
        response.getOutputStream().print(jsonOutput);
    }
}

Homework

Feel free to bring in here even more capabilities such as a type safe proposer/approver and the like.

In addition, you may write your own serializer using the Jackson framework.

As a result, we can now expose our new MyBusinessPartner entity via our initial servlet which should lead to the following result:

Type-safe and Eager Expand

Motivation

So far, we have been working with the BusinessPartner entity only. However, this is merely the root entity of a more complex data model. For example, each BusinessPartner can be associated with zero-to-many BusinessPartnerAddresses which again can be associated with zero-to-many BusinessPartnerEMailAddresses. Another popular association in an ERP context are header-item relationships such as an invoice header and invoice line items.

One possibility is to consider a lazy fetch of connected entities only using the fetchXY() methods that each instance has:

List<BusinessPartnerAddress> addresses = businessPartner.fetchBusinessPartnerAddress();

This can be a beneficial approach in cases where the entities contain large data volumes and the interaction with the data allows for a step-by-step resolution of the model (e.g., lazily loading entities for the UI).

However, if you want to get addresses of many business partners, this approach leads to significant performance issues as each method call corresponds to one remote function call to the S/4HANA APIs. Furthermore, the lazy fetch also gets all fields from the connected entity per default, however, sometimes we may want to select only certain fields.

In such cases, we rather prefer to resolve the association already eagerly upon the first API call. In OData language, this is called an expand on navigational properties, in SQL speak this refers to a left outer join between parent and child tables.

How-to

In the example below, we present an example that expands for every business partner its corresponding list of addressed, followed by a partial projection on the City and Country properties of the associated BusinessPartnerAddress entity, followed by another expand to the AddressEMailAddress entity where we project on the EMail_Address property only.

public class GetBusinessPartnerCommand extends ErpCommand<List<BusinessPartner>> {

    private BusinessPartnerService businessPartnerService;

    public GetBusinessPartnerCommand(ErpConfigContext erpConfigContext, BusinessPartnerService businessPartnerService) {
        super(GetBusinessPartnerCommand.class, erpConfigContext);
        this.businessPartnerService = businessPartnerService;
    }

    @Override
    protected List<BusinessPartner> run() throws Exception {

        try {

            return businessPartnerService.getAllBusinessPartner()
                        .filter(BusinessPartner.CUSTOMER.ne(""))
                        .select(BusinessPartner.FIRST_NAME,
                                BusinessPartner.LAST_NAME,
                                new BusinessPartnerField<String>("YY1_ApprovedBy_bus"),
                                new BusinessPartnerField<String>("YY1_ProposedBy_bus"),
                                BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS
                                        .select(
                                                BusinessPartnerAddress.CITY_NAME,
                                                BusinessPartnerAddress.COUNTRY,
                                                BusinessPartnerAddress.TO_EMAIL_ADDRESS
                                                    .select(
                                                            AddressEmailAddress.EMAIL_ADDRESS
                                                    )
                                                )
                        )
                        .execute(getConfigContext());

        } catch (final ODataException e) {
            throw new HystrixBadRequestException(e.getMessage(), e);
        }
    }
}

Without further modifications, this will immediately yield the following serialization result to our client (hint: we are now again assume to use the original BusinessPartner entity being serialized to the client, not the MyBusinessPartner entity)

Readers who are familiar with the OData query language might wonder about the missing expand() syntax. In the OData query language, expand and select are two independent keywords.

In the OData VDM for Java, we have decided to combine both methods with each other to keep up the type-safety principle. Unfortunately, the type-system of Java is not powerful enough to preserve the type information from an explicit expand() statement to an underlying select() statement. We believe that the proposed solution of using a fluent select API over connected entities is better suited for a clean and safe Java API.

Working with expanded entities

After we did a successful API call, we may want to work with the associated entity collections. For this purpose, the VDM provides two important methods on each entity instance that can be used for retrieval:

First, the getOrFetch() method:

 List<BusinessPartnerAddress> businessPartnerAddresses = businessPartner.getBusinessPartnerAddressOrFetch();

This method either returns the list of connected entities, if previously eagerly fetched or will lazily fetch the entities, if not. Therefore, this method guarantees to not return any null values but might break due to a thrown ODataException, in case a lazy fetch is initiated due to missing authorizations, timeouts or system unavailability.

Secondly, a getOrNull() method:

Optional<List<BusinessPartnerAddress>> businessPartnerAddresses = 
                businessPartner.getBusinessPartnerAddressOrNull();

This method returns an Optional of the return type signifying that the connected entity might be null as no lazy fetch is initiated. Therefore, this method guarantees to do no lazy fetch but cannot guarantee to return a value. As a consequence, this method also does not throw any ODataException.

Dependency Injection

Motivation

With SAP S/4HANA Cloud SDK version 1.5.0, we have also introduce the possibility to use dependency injection with the virtual data model for OData and BAPI. In a nutshell, dependency injection is a major object-oriented inversion of control principle that allows to decouple the call direction from the initialization direction. This leads to less coupled dependencies which are easier to maintain, for example, if a dependency changes, the client does not need to be touched.

Sounds complicated? Nope, it is really a 25-dollar term for a 5-cent concept, however, it makes your code cleaner and less dependent on the actual implementation.

How-to

So far, we have initialized the business partner service directly from our servlet like this:

BusinessPartnerService businessPartnerService = new DefaultBusinessPartnerService();

This unnecessarily exposes implementation details to the client code. Instead, in our client code we just want to declare which kind of interface we would like to consume and get the corresponding implementation “injected” from the outside during runtime to achieve a higher degree of decoupling.

To do this, we can rewrite our initial servlet with the @Inject annotation like this:

@WebServlet("/businessPartners")
public class BusinessPartnerServlet extends HttpServlet {

    @Inject
    private BusinessPartnerService businessPartnerService;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        final List<BusinessPartner> businessPartnerList =
                new GetBusinessPartnerCommand(new ErpConfigContext(), businessPartnerService).execute();

        final String jsonOutput = new Gson().toJson(businessPartnerList);
        response.getOutputStream().print(jsonOutput);
    }
}

That’s it: The only thing we really did is get rid of the new DefaultBusinessPartnerImplementation() term. Therefore, in the future whenever the implementing service changes (its name, its package, its module, etc.) your client code will not notice this and is therefore less prone to changes.

Hint: When writing integration tests as learned in previous tutorials and you require dependency injection from your application code, please make sure that the implementing class is part of the minimal assembly. In other words, don’t forget to add the class to the TestUtil deployment creator:

TestUtil.createDeployment(..., DefaultBusinessPartnerService.class);

Summary

In this tutorial, we have shown how you can leverage the latest capabilities of the Virtual Data Model for OData using the SAP S/4HANA Cloud SDK. This includes to use custom fields from SAP S/4HANA within all your logic, type-safe expand features for GET calls as well as dependency injection of our VDM service classes. This gives you even greater capabilities and lets you integrate with SAP S/4HANA even faster and easier.

Questions?

Reach out to us on Stackoverflow via our s4sdk tag. We actively monitor this tag in our core engineering teams.

Alternatively, we are happy to receive your comments to this blog below.

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