Skip to Content

The following steps will explain how you can use deep insert with the virtual data model for OData to post complex data structures in one API call to SAP S/4HANA.

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 create and deep insert functionality for OData as supported by the SAP S/4HANA Cloud SDK in more detail. After this blog you will be able to understand

  • How to build up a complex data structure using the virtual data model.
  • How to write deeply nested data to SAP S/4HANA in a single call.
  • How to write tests for deep insertion as unit as well as integration test.

Prerequisites

In order to successfully go through this tutorial you have to complete the tutorial at least until

In addition, we will use the virtual data model for OData as introduced in

As we will also utilize mocking for writing our unit tests, you should be familiar, with the basic mocking capabilities of the SDK as introduced in:

In addition, deep insert of S/4HANA APIs works as of S/4HANA Cloud SDK 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.5.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
    <!-- possibly further managed dependencies ... -->
</dependencyManagement>

 

Deep Insert

Motivation

Deep Insert is already part of the OData specification, version 2 without this explicit name. Although not supported yet, the OData specification, version 4 is much more explicit on the semantics. Citing from the spec, Deep Insert is defined as:

  • A request to create an entity that includes related entities, represented using the appropriate inline representation, is referred to as a “deep insert”.
  • On success, the service MUST create all entities and relate them.
  • On failure, the service MUST NOT create any of the entities.

This means deep insert is an atomic operation that is either successful or fails for all entities. Furthermore, it is for insert-only operations, i.e., the OData spec does not foresee any “deep update” operation yet (to be fair, it is part of the 4.01 working draft spec, however, we are not aware of any provider implementations yet, in particular as S/4HANA APIs are based on OData V2).

How-to

Writing the application code

To get started, we first of all create a new ErpCommand called StoreBusinessPartnerCommand because also write operations shall be contained within Hystrix-based command patterns. If you did our previous tutorials that should be now straightforward to you:

public class StoreBusinessPartnerCommand extends ErpCommand<BusinessPartner> {

    private BusinessPartnerService businessPartnerService;
    private BusinessPartner businessPartner;

    public StoreBusinessPartnerCommand(final ErpConfigContext erpConfigContext, final BusinessPartnerService businessPartnerService, final BusinessPartner businessPartner) {
        super(StoreBusinessPartnerCommand.class, erpConfigContext);
        this.businessPartnerService = businessPartnerService;
        this.businessPartner = businessPartner;
    }

    @Override
    protected BusinessPartner run() {

        try {

            return businessPartnerService
                    .createBusinessPartner(businessPartner)
                    .execute(getConfigContext());

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

Hint: This code goes to your <projectroot>/application/src/main/java folder, either into the default package or a package of your choice.

____________________

What the code does

The code shows a new StoreBusinessPartnerCommand which assumes an ErpConfigContext, a BusinessPartnerService and a concrete BusinessPartner instance upon creation time.

Within the run() method, i.e., whenever the command is executed, it calls the businesspartner service with the create method and executes against the current multi-tenant ERPContext as explained in previous tutorials.
____________________

 

The StoreBusinessPartnerCommand takes a businesspartner instance as input. This can be a potentially complex data type. Therefore, in the next step we need to create a nested data structure based on the BusinessPartner data model.

The structure we are interested in is presented below. The root entity will be the business partner which is connected to zero-to-many BusinessPartnerRoles and BusinessPartnerAddresses which is again connected to zero-to-many EMailAddresses:

 

 

For this purpose, we are creating a new simple servlet that exposes a POST method to our clients:

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

    private static final Logger logger = CloudLoggerFactory.getLogger(BusinessPartnerServlet.class);

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

        final String firstname = request.getParameter("firstname");
        final String lastname = request.getParameter("lastname");
        final String country = request.getParameter("country");
        final String city = request.getParameter("city");
        final String email = request.getParameter("email");

        //do consistency checks here...

        final AddressEmailAddress emailAddress = AddressEmailAddress.builder()
                .emailAddress(email)
                .build();

        final BusinessPartnerAddress businessPartnerAddress = BusinessPartnerAddress.builder()
                .country(country)
                .cityName(city)
                .toEmailAddress(Lists.newArrayList(emailAddress))
                .build();

        final BusinessPartnerRole businessPartnerRole = BusinessPartnerRole.builder()
                .businessPartnerRole("FLCU01")
                .build();

        final BusinessPartner businessPartner = BusinessPartner.builder()
                .firstName(firstname)
                .lastName(lastname)
                .businessPartnerCategory("1")
                .correspondenceLanguage("EN")
                .toBusinessPartnerAddress(Lists.newArrayList(businessPartnerAddress))
                .toBusinessPartnerRole(Lists.newArrayList(businessPartnerRole))
                .build();

        String responseBody;

        try {
            final BusinessPartner storedBusinessPartner = new StoreBusinessPartnerCommand(new ErpConfigContext(), new DefaultBusinessPartnerService(), businessPartner).execute();
            responseBody = new Gson().toJson(storedBusinessPartner);
            response.setStatus(HttpServletResponse.SC_CREATED);

        } catch(final HystrixBadRequestException e) {
            responseBody = e.getMessage();
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            logger.error(e.getMessage(), e);
        }

        response.setContentType("application/json");
        response.getOutputStream().print(responseBody);
    }
}

____________________

What the code does

The code implements a new Servlet exposed under the /businessPartner URL path. It expects five parameters to be set: firstname, lastname, country, city and e-mail. For readability reasons, we omit here details for checking that these parameters are actually set and throw corresponding error messages to the client, an aspect you should definitively do in any productive code.

Based on the five input parameters, we are creating the various entities. First, an entity to store the E-Mail Address using the exposed builder pattern method. Secondly, we create one BusinessPartnerAddress based on the city and country parameter as well as the e-mail address entity from the first step. Thirdly, we create a business partner role using the FLCU01 role (which actually stands for a customer). Fourthly, the final business partner entity which consumes the remaining parameters and the entity from the steps before.

Finally, we use our StoreBusinessPartnerCommand to store the created business partner entity. As a result we will get the stored entity which will be enriched by an ID that is given by the S/4HANA system which then is serialized into a JSON for the client.

In case of an exception, we simply return the error message, ignoring any pretty printing or JSON formatting here for simplicity reasons.
____________________

When we deploy the above created code to SAP Cloud Platform or using a local instance (please consider previous tutorials such as Step 3 with SAP S/4HANA Cloud SDK: HelloWorld on SCP CloudFoundry). In this example, we have used a mvn clean install && mvn tomee:run to run it on localhost.

Then we can use a tool like Postman or Curl to check whether the code works. As you can see in this example, the business partner has been successfully posted and contains a BusinessPartner ID and UUID which was enriched by S/4HANA:

Writing a unit test

As learned in Step 19 with S/4HANA Cloud SDK: Mocking S/4HANA calls or how to develop an S/4HANA extension without an S/4HANA system, we can utilize mocking to test the functionality without an S/4HANA system to achieve code coverage, fast running tests and better testable code.

For this purpose, we are creating the following test class which checks the basic assumptions of our API as well as the failure case:

@RunWith(MockitoJUnitRunner.Silent.class)
public class GetBusinessPartnerMockedTest
{
    private static final MockUtil mockUtil = new MockUtil();

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private BusinessPartnerService service;

    @Mock
    private BusinessPartner alice;

    @Before
    public void before() {
        mockUtil.mockDefaults();
        mockUtil.mockDestination("ErpQueryEndpoint", URI.create(""));

        when(alice.getFirstName()).thenReturn("Alice");
    }

    @Test
    public void testBusinessPartnerCreateSuccessful() throws Exception {

        final BusinessPartner enrichedAlice = Mockito.mock(BusinessPartner.class);
        when(enrichedAlice.getFirstName()).thenReturn("Alice");
        when(enrichedAlice.getBusinessPartner()).thenReturn("123");

        when(service
                .createBusinessPartner(alice)
                .execute(any(ErpConfigContext.class)))
                .thenReturn(enrichedAlice);

        BusinessPartner partner = new StoreBusinessPartnerCommand(new ErpConfigContext(), service, alice).execute();
        assertEquals(enrichedAlice, partner);
        assertEquals("123", enrichedAlice.getBusinessPartner());
    }

    @Test(expected = HystrixBadRequestException.class)
    public void testBusinessPartnerCreateNotSuccessful() throws Exception {

        when(service
                .createBusinessPartner(alice)
                .execute(any(ErpConfigContext.class)))
                .thenThrow(new ODataException());

        new StoreBusinessPartnerCommand(new ErpConfigContext(), service, alice).execute();
    }
}

Hint: This code goes to your <projectroot>/unit-tests/src/test/java folder.

Writing an integration test

Just the unit test might not be sufficient when we want to test the real integration with S/4HANA. Therefore, we would also like to leverage an integration test as used in previous tutorials:

import static com.jayway.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@RunWith(Arquillian.class)
public class BusinessPartnerDeepInsertTest {
    private static final MockUtil mockUtil = new MockUtil();

    @ArquillianResource
    private URL baseUrl;

    @Deployment
    public static WebArchive createDeployment() {
        return TestUtil.createDeployment(BusinessPartnerServlet.class,
                BusinessPartner.class,
                StoreBusinessPartnerCommand.class,
                DefaultBusinessPartnerService.class);
    }

    @BeforeClass
    public static void beforeClass() throws URISyntaxException {
        mockUtil.mockDefaults();
        mockUtil.mockErpDestination("ErpQueryEndpoint", "S4HANA");
    }

    @Before
    public void before() {
        RestAssured.baseURI = baseUrl.toExternalForm();
    }

    @Test
    public void testStoreAndGetCustomers() {

        given()
                .parameters("firstname", "John", "lastname", "Doe", "country", "US", "city", "Tuxedo", "email", "john@doe.com")
                .when()
                    .post("/businessPartners")
                .then()
                    .log().all()
                    .statusCode(201)
                    .and()
                    .body("BusinessPartner", not(isEmptyString()))
                    .and()
                    .body("BusinessPartnerUUID", not(isEmptyString()));
    }
}

Hint: This code goes to <projectroot>/integration-tests/src/test/java. In addition, we are using a system alias “S4HANA” in this example, which is stored inside the<projectroot>/integration-tests/src/test/resources/systems.yml (the basics of the credentials.yml / systems.yml approach was introduced in Step 5 with SAP S/4HANA Cloud SDK: Resilience with Hystrix).

Both tests together give us a code coverage of 91%:

Summary

In this tutorial, we have shown how you can leverage the deep insert functionality of the S/4HANA Cloud SDK to easily insert deeply nested data to SAP S/4HANA in a single call. Besides the pure functionality, we have also shown you how to implement unit and integration tests for this functionality.

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