4. Replicating Data - SAP-samples/teched2022-AD265 GitHub Wiki

Enable Replication for Customers

In the incidents list, the application shall display (remote) customer data together with (application-local) incident data. This raises a performance issue: when showing potentially hundreds of incidents, shall the app reach out to the remote system at all? Or just for single records, for all records at once, or for a chunk of records?

We use a different approach by replicating remote data on demand.

The scenario will look like this:

  • The user enters a new incident and selects the customer through a value help. This value help shows only remote customer data.
  • As soon as the incident record is created, the customer data is written to a local replica table.
  • Further requests for the incident's customer are served from this replica table.
  • Replicated records will be updated if a remote customer changes.

Start by adding a persistent table for the replicas. This can be done with just one line in srv/mashup.cds:

annotate s4.simple.Customers with @cds.persistence: { table,skip:false };

The annotation @cds.persistence: {table,skip:false} turns the view above into a table with the same signature (ID and name columns). See the documentation for more on annotations that influence persistence.

Replicate Data On Demand

Now there is code needed to replicate the customer record whenever an incident is created. In file srv/incidents-service.js, add this code (to the outer function):

  const db = await cds.connect.to('db')            // our primary database
  const { Customers }  = db.entities('s4.simple')  // CDS definition of the Customers entity

  this.after (['CREATE','UPDATE'], 'Incidents', async (data) => {
    const { customer_ID: ID } = data
    if (ID) {
      let replicated = await db.exists (Customers,ID)
      if (!replicated) {
        console.log ('>> Updating customer', ID)
        let customer = await S4bupa.read (Customers,ID)
        await INSERT(customer) .into (Customers)
      }
    }
  })

Now create an incident in the UI. Don't forget to select a customer through the value help. In the log, you can see the >> Updating customer line, confirming that replication happens.

Test without UI

With the REST client for VS Code, you can conveniently test the same flow without the UI.

Create a file tests.http and this content:

###
# @name IncidentsCreate

POST http://localhost:4004/incidents/Incidents
Content-Type: application/json

{
  "title": "New incident",
  "customer_ID": "Z100001"
}

###
@id = {{IncidentsCreate.response.body.$.ID}}

POST http://localhost:4004/incidents/Incidents(ID={{id}},IsActiveEntity=false)/draftActivate
Content-Type: application/json
  • Click Send Request above the POST .../Incidents line. This will create the record in a draft tate.

  • Click Send Request above the POST .../draftActivate line. This corresponds to the Save ction in the UI.

    This second request is needed for all changes to entities managed by SAP Fiori's draft mechanism.

You should see the same >> Updating customer server log.

Event-based Replication

We haven't discussed yet how to update the cache table holding the Customers data. We'll use events to inform our application whenever the remote BusinessPartner has changed. Let's see what you need to do.

Add Events to Imported APIs

First, as synchronous and asynchronous APIs from SAP S/4HANA sources are not comprised in the imported API definition (the edmx file), we have to add event defintitions manually. For the business partner model, the event information can be found at https://api.sap.com/event/CE_BUSINESSPARTNEREVENTS/resource

In file srv/external/index.cds, add this:

extend service S4 {
  event BusinessPartner.Changed @(topic: 'sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1') {
    BusinessPartner: S4.A_BusinessPartner:BusinessPartner;
  }
}

This allows CAP's support for events and messaging to kick in, which automatically subscribes to message brokers and emits events behind the scenes.

Also, the event name BusinessPartner.Changed is semantically closer to the domain and easier to read than the underlying technical event sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1.

React to Events

So, the piece to close the loop is code to consume events in the application.

In srv/incidents-service.js, add this event handler (into the body of the outer function):

  // update cache if BusinessPartner has changed
  S4bupa.on('BusinessPartner.Changed', async ({ event, data }) => {
    console.log('<< received', event, data)
    const { BusinessPartner: ID } = data
    const customer = await S4bupa.read (Customers, ID)
    let exists = await db.exists (Customers,ID)
    if (exists)
      await UPDATE (Customers, ID) .with (customer)
    else
      await INSERT.into (Customers) .entries (customer)
  })

Emitting Events from Mocked Services

But who is the event emitter? Usually it's the remote data source, i.e. the SAP S4/HANA system. For local runs, it would be great if something could emit events when testing. Luckily, you can add a simple event emitter in a new file srv/external/API_BUSINESS_PARTNER.js:

module.exports = function () {
  const { A_BusinessPartner } = this.entities;

  this.after('UPDATE', A_BusinessPartner, async data => {
    const event = { BusinessPartner: data.BusinessPartner }
    console.log('>> BusinessPartner.Changed', event)
    await this.emit('BusinessPartner.Changed', event);
  })
}

This means whenever you change data through the API_BUSINESS_PARTNER mock service, a local event is emitted. Also note how the event name BusinessPartner.Changed matches to the event definition from the CDS code above.

Put it all together

Before starting the application again, it's time to turn the current in-memory database into a persistent one. This way, data is not reset after each restart, which is useful if you added data manually.

So, kill cds watch, then execute:

cds deploy --with-mocks --to sqlite

Start the application with mocks:

cds watch

The application runs as before. In the log, however, you no longer see a database deployment, but a line like:

...
[cds] - connect to db > sqlite { url: 'db.sqlite', database: 'db.sqlite' }
...

This also means that after changes to the data model (new fields, entities etc.), you need to execute the cds deploy ... command again. Keep this in mind in case you see errors like table/view not found.

In your file tests.http, first execute the 2 requests to create an incident again (see section above).

Now change customer Z100001 with an HTTP request. Add this request:

###
PUT http://localhost:4004/api-business-partner/A_BusinessPartner/Z100001
Authorization: Basic carol:
Content-Type: application/json

{
  "BusinessPartnerFullName": "Albus Percival Wulfric Brian Dumbledore"
}

After clicking Send Request above the PUT ... line, you should see both the event being emitted as well as received:

>> BusinessPartner.Changed { BusinessPartner: 'Z100001' }
<< received BusinessPartner.Changed { BusinessPartner: 'Z100001' }

The UI also reflects the changed data:

Updated customer list

Note that we can't test the event roundtrip in the cds watch --profile sandbox mode, as the sandbox system of SAP API Business Hub does not support modifications. You would need to use a dedicated SAP S/4HANA system here. See this tutorial for how to register your own SAP S/4HANA system.

Summary

In the next exercise, you will learn how to consolidate the current code into an integration package and how to use this package.