Skip to content

Commit ddce866

Browse files
committed
Splitted overly lengthy calesi guide
1 parent 4f3fe22 commit ddce866

File tree

7 files changed

+733
-707
lines changed

7 files changed

+733
-707
lines changed

guides/databases/_menu.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
<!-- # [CAP-level Database Usage](index.md) -->
2-
# [CDL Compiled to DDL](cdl-to-ddl)
3-
# [CQL Compiled to SQL](cql-to-sql)
4-
# [Adding Initial Data](initial-data.md)
1+
<!-- # [ CAP-level Database Usage ](index.md) -->
2+
# [ Mocked Out of the Box ](cql-to-sql)
3+
# [ CDL Compiled to DDL ](cdl-to-ddl)
4+
# [ Adding Initial Data ](initial-data.md)
55
# Deployed to ...
6-
## [SAP HANA](hana.md)
7-
### [SAP HANA Native](hana-native.md)
8-
## [SQLite](sqlite.md)
9-
## [H2 (Java)](h2.md)
10-
## [PostgreSQL](postgres.md)
11-
# [Schema Evolution](schema-evolution.md)
12-
# [Performance Guide](performance.md)
6+
## [ SAP HANA ](hana.md)
7+
### [ SAP HANA Native ](hana-native.md)
8+
## [ SQLite ](sqlite.md)
9+
## [ H2 (Java) ](h2.md)
10+
## [ PostgreSQL ](postgres.md)
11+
# [ Schema Evolution ](schema-evolution.md)
12+
# [ Performance Guide ](performance.md)

guides/integration/_menu.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

22
# [ CAP-level Service Integration ](calesi.md)
33
# [ CAP-level Data Federation ](data-federation.md) <!-- UNRELEASED -->
4+
# [ Inner Loop Development ](inner-loops.md) <!-- UNRELEASED -->
45
# [ Reuse & Compose ](reuse-and-compose.md)
6+
# [ Service Bindings ](service-bindings.md) <!-- UNRELEASED -->
57

68
# Platform Services <!-- UNRELEASED -->
79

guides/integration/calesi.md

Lines changed: 153 additions & 462 deletions
Large diffs are not rendered by default.

guides/integration/data-federation.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,219 @@ CAP applications can integrate and federate data from multiple external data sou
44
{.abstract}
55

66

7+
## Introduction
8+
9+
Displaying external data in lists commonly requires fast access to that data. Relying on live calls to remote services per row is clearly not an option, as that would lead to poor performance, excessive load on server, and a nightmare regarding resilience. Instead, we somehow need to ensure that all required data is available locally, so that it can be accessed fast and reliably by UIs, using good old SQL JOINs.
10+
11+
For example, we saw the need for that already in the [CAP-level Service Integration](calesi.md#coding-required) guide, where the `Customer` field in the travel requests list is populated from the remote S/4 Business Partner service, but missing when running the services separately:
12+
13+
1. First run these commands **in two separate terminals**:
14+
15+
```shell
16+
cds mock apis/capire/s4.cds
17+
```
18+
```shell
19+
cds mock apis/capire/xflights.cds
20+
```
21+
22+
2. Start the xtravels server as usual **in a third terminal**, and note that it now _connects_ to the other services instead of mocking them:
23+
24+
```shell
25+
cds watch
26+
```
27+
```zsh
28+
[cds] - connect to S4BusinessPartnerService > odata {
29+
url: 'http://localhost:54476/odata/v4/s4-business-partner'
30+
}
31+
```
32+
```zsh
33+
[cds] - connect to sap.capire.flights.data > hcql {
34+
url: 'http://localhost:54475/hcql/data'
35+
}
36+
```
37+
38+
2. Open the Fiori UI in the browser again -> data from the S/4 service is missing now, as we have not yet implemented the required custom code for the actual data integration, the same applies to the flight data from _xflights_:
39+
40+
![XTravels Fiori list view showing a table of travel requests, with the Customer column empty.](assets/xtravels-list-.png)
41+
42+
![XTravels Fiori details view showing a travel requests, with the flights data missing](assets/xtravels-bookings-.png)
43+
44+
In addition, when we again look into the log output, we see some bulk requests like shown below, which indicates that the Fiori client is desparately trying to fetch the missing customer data. If we'd scroll the list in the UI this would repeat like crazy.
45+
46+
<span style="font-size:63%">
47+
48+
```js
49+
[odata] - POST /odata/v4/travel/$batch
50+
[odata] - > GET /Travels(ID=4133,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
51+
[odata] - > GET /Travels(ID=4132,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
52+
[odata] - > GET /Travels(ID=4131,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
53+
[odata] - > GET /Travels(ID=4130,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
54+
[odata] - > GET /Travels(ID=4129,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
55+
[odata] - > GET /Travels(ID=4128,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
56+
[odata] - > GET /Travels(ID=4127,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
57+
[odata] - > GET /Travels(ID=4126,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
58+
[odata] - > GET /Travels(ID=4125,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
59+
[odata] - > GET /Travels(ID=4124,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
60+
[odata] - > GET /Travels(ID=4123,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
61+
[odata] - > GET /Travels(ID=4122,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
62+
[odata] - > GET /Travels(ID=4121,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
63+
[odata] - > GET /Travels(ID=4120,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
64+
[odata] - > GET /Travels(ID=4119,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
65+
[odata] - > GET /Travels(ID=4118,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
66+
[odata] - > GET /Travels(ID=4117,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
67+
[odata] - > GET /Travels(ID=4116,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
68+
[odata] - > GET /Travels(ID=4115,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
69+
[odata] - > GET /Travels(ID=4114,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
70+
[odata] - > GET /Travels(ID=4113,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
71+
[odata] - > GET /Travels(ID=4112,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
72+
[odata] - > GET /Travels(ID=4111,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
73+
[odata] - > GET /Travels(ID=4110,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
74+
[odata] - > GET /Travels(ID=4109,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
75+
[odata] - > GET /Travels(ID=4108,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
76+
[odata] - > GET /Travels(ID=4107,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
77+
[odata] - > GET /Travels(ID=4106,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
78+
[odata] - > GET /Travels(ID=4105,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
79+
[odata] - > GET /Travels(ID=4104,IsActiveEntity=true) { '$select': 'Customer', '$expand': 'Customer($select=ID,Name)' }
80+
```
81+
</span>
82+
83+
In the xtravels app we accomplished that with a simple, yet quite effective data replication solution, which automatically replicates data as follows...
84+
85+
86+
## Federated Consumption Views
87+
88+
Tag [consumption views](calesi#consumption-views) with the `@federated` annotation, to express your intent to have that data federated, i.e. in close access locally, for example:
89+
90+
```cds :line-numbers=4
91+
@federated entity Customers as projection on S4.A_BusinessPartner { ... }
92+
```
93+
94+
> [!tip] Stay Intentional -> <i>What, not how!</i> -> Minimal Assumptions
95+
>
96+
> By tagging entities with `@federated` we stay _intentional_ about **_what_** we want to achieve, and avoid any premature assumptions about **_how_** things are actually implemented. => This allows CAP runtimes – or your own _generic_ solutions, as in this case – to choose the best possible implementation strategies for the given environment and use case, which may differ between development, testing, and production environments, or might need to evolve over time.
97+
98+
99+
## Generic Implementation
100+
101+
Here's the complete code, placed in file `srv/data-federation.js`:
102+
103+
::: code-group
104+
```js:line-numbers [srv/data-federation.js]
105+
const PROD = process.env.NODE_ENV === 'production' /* eslint-disable no-console */
106+
const cds = require ('@sap/cds')
107+
const feed = []
108+
109+
// Collect all entities to be federated, and prepare replica tables
110+
PROD || cds.on ('loaded', csn => {
111+
for (let e of cds.linked(csn).entities) {
112+
if (e['@federated']) {
113+
let srv = remote_srv4(e)
114+
if (is_remote(srv)) {
115+
e['@cds.persistence.table'] = true //> turn into table for replicas
116+
feed.push ({ entity: e.name, remote: srv })
117+
}
118+
}
119+
}
120+
})
121+
122+
// Setup and schedule replications for all collected entities
123+
PROD || cds.once ('served', () => Promise.all (feed.map (async each => {
124+
const srv = await cds.connect.to (each.remote)
125+
srv._once ??=! srv.on ('replicate', replicate)
126+
await srv.schedule ('replicate', each) .every ('3 seconds')
127+
})))
128+
129+
// Event handler for replicating single entities
130+
async function replicate (req) {
131+
let { entity } = req.data, remote = this
132+
let { latest } = await SELECT.one `max(modifiedAt) as latest` .from (entity)
133+
let rows = await remote.run (
134+
SELECT.from (entity) .where `modifiedAt > ${latest}`
135+
)
136+
if (rows.length) await UPSERT (rows) .into (entity); else return
137+
console.log ('Replicated', rows.length, 'entries', { for: entity, via: this.kind })
138+
}
139+
140+
// Helpers to identify remote services, and check whether they are connected
141+
const remote_srv4 = entity => entity.__proto__._service?.name
142+
const is_remote = srv => cds.requires[srv]?.credentials?.url
143+
```
144+
:::
145+
146+
Let's have a closer look at this code, which handles these main tasks:
147+
148+
1. **Prepare Persistence** – When the model is `loaded`, before it's deployed to the database, we collect all to be `@federated` entities, check whether their respective services are remote, and if so, turn them into tables for local replicas (line 11).
149+
150+
2. **Setup Replication** – Later when all services are `served`, we connect to each remote one (line 20), register a handler for replication (line 21), and schedule it to be invoked every three seconds (line 22).
151+
152+
3. **Replicate Data** – Finally, the `replicate` handler implements a simple polling-based data federation strategy, based on `modifiedAt` timestamps (lines 28-32), with the actual call to remote happening on line 29.
153+
154+
> [!tip] CAP-level Querying -> agnostic to databases & protocols
155+
> We work with **database-agnostic** and **protocol-agnostic** [CQL queries](../../cds/cql) both for interacting with the local database as well as for querying remote services. In effect, we got a fully generic solution for replication, i.e., it works for **_any_** remote service that supports OData, or HCQL.
156+
157+
158+
### Test Drive Locally
159+
160+
Let's see the outcome in action: to activate the above data federation code, edit `srv/server.js` file and uncomment the single line of code in there like this:
161+
162+
::: code-group
163+
```js :line-numbers [srv/server.js]
164+
process.env.NODE_ENV || require ('./data-federation')
165+
```
166+
:::
167+
168+
Restart the Xtravels app, and see these lines in the log output:
169+
170+
```zsh
171+
Replicated 49 entries { for: 'sap.capire.xflights.Supplements', via: 'hcql' }
172+
Replicated 44 entries { for: 'sap.capire.xflights.Flights', via: 'hcql' }
173+
Replicated 727 entries { for: 'sap.capire.s4.Customers', via: 'odata' }
174+
```
175+
176+
The S/4 Business Partner service in terminal 1 shows the incoming OData request(s):
177+
178+
```zsh
179+
[odata] - GET /odata/v4/s4-business-partner/A_BusinessPartner {
180+
'$select': 'BusinessPartner,PersonFullName,LastChangeDate',
181+
'$filter': 'LastChangeDate gt 2024-12-31'
182+
}
183+
```
184+
185+
While the xflights service in terminal 2 shows its incoming HCQL requests like that:
186+
187+
```zsh
188+
[hcql] - GET /hcql/data/ {
189+
SELECT: {
190+
from: { ref: [ 'sap.capire.flights.data.Flights' ] },
191+
columns: [
192+
{ ref: [ 'ID' ], as: 'ID' },
193+
{ ref: [ 'date' ], as: 'date' },
194+
{ ref: [ 'departure' ], as: 'departure' },
195+
{ ref: [ 'arrival' ], as: 'arrival' },
196+
{ ref: [ 'free_seats' ], as: 'free_seats' },
197+
{ ref: [ 'modifiedAt' ], as: 'modifiedAt' },
198+
{ ref: [ 'airline', 'icon' ], as: 'icon' },
199+
{ ref: [ 'airline', 'name' ], as: 'airline' },
200+
{ ref: [ 'origin', 'name' ], as: 'origin' },
201+
{ ref: [ 'destination', 'name' ], as: 'destination' }
202+
],
203+
where: [
204+
{ ref: [ 'modifiedAt' ] },
205+
'>',
206+
{ val: '2026-01-28T17:38:28.929Z' }
207+
]
208+
}
209+
}
210+
```
211+
212+
Finally, open the Fiori UI in the browser again, and see that customer data from S/4 as well as flight data from xflights is now displayed properly, thanks to the data federation implemented above.
213+
214+
![XTravels Fiori list view showing tarvel requests, now with customer names again.](assets/xtravels-list.png)
215+
216+
![XTravels Fiori details view showing a travel requests, now with flight data again.](assets/xtravels-bookings.png)
217+
218+
219+
7220
## Service-level Replication
8221
9222
### Initial Loads

0 commit comments

Comments
 (0)