sapui5tutors SAPUI5 • Fiori • SAP BTP Step-by-step tutorials Real project examples Interview Q&A
Practical SAPUI5 • Fiori • SAP BTP tutorials and interview prep
★ Featured S5.
Need SAPUI5 / Fiori help? Custom Fiori apps, enhancements, performance fixes and mentoring — from an SAP BTP & Fiori consultant.
Hire me

🏆 Test Your Interview Skills

Topic quizzes, a daily question challenge, and timed mock exams. Your best scores are saved on this device.

🏆 Weekly leaderboard

Loading scores…

Latest Tutorials

sapui5tutors

Apache Camel Runtime in SAP Cloud Integration Explained

12:39:00

Open any iFlow in SAP Cloud Integration and you're looking at Apache Camel — even if the name never appears on screen. The graphical steps you drag onto the canvas (Content Modifier, Router, Splitter, Message Mapping) are Camel processors under the hood, and the runtime executing your flow is the Camel routing engine. Understanding that mapping makes iFlow behavior far less mysterious.


iFlow = Camel route

In Camel terminology, a route is a sequence of processing steps applied to a message: from an endpoint, through processors, to an endpoint. That's exactly what an iFlow is. The sender channel (HTTPS, SFTP, IDoc, OData...) is the Camel consumer endpoint; the receiver channel is the producer endpoint; everything between is a chain of processors.

This isn't trivia — it explains real behavior. Message headers? Those are Camel exchange headers. The message body? The Camel exchange body. Properties you set in a Content Modifier? Exchange properties. When documentation talks about the "exchange pattern" (InOnly vs InOut), it's describing whether the route expects a response — which is why a synchronous HTTPS call behaves differently from a fire-and-forget SFTP drop.

Key takeaway: an iFlow is a Camel route. Sender/receiver channels are endpoints; the steps between are processors operating on a Camel exchange (headers + body + properties).

Steps = processors

Each iFlow step maps to a Camel processor or EIP (Enterprise Integration Pattern) implementation:

Content-Based Router → Camel's choice/content-based router. Evaluates conditions in order, routes to the first match.
Splitter / Aggregator / Gather → Camel's splitter and aggregator EIPs for breaking messages apart and reassembling them.
Content Modifier → header/property/body manipulation on the exchange.
Message Mapping → transformation step (often XSLT or graphical mapping) applied to the body.
Request-Reply → synchronous call-out with the response fed back into the route.

Knowing the EIP behind the step helps when behavior surprises you. The aggregator's completion conditions (timeout, size, predicate) are straight out of the Camel aggregator pattern. The splitter's parallel processing option maps to Camel's parallel processing flag. The graphical step is a friendly face on a well-documented pattern.


How the runtime executes flows

When a message arrives, the runtime creates an exchange and pushes it through the route. Processing is fundamentally sequential per message (unless a step explicitly parallelizes), which keeps the mental model simple: the message enters at the top and exits at the bottom, transformed along the way.

A few runtime realities worth knowing:

Stateless by default. Each message gets its own exchange. Steps shouldn't rely on leftover state from previous messages — that's what data stores and variables with proper scope are for.

Exceptions propagate up. An unhandled error in a processor bubbles up the route to the exception handler (the iFlow's error end event). This is why a single "catch" at the flow level works — and why understanding where in the route the failure happened matters for debugging.

Transactions are local. The runtime coordinates transactional resources (JMS, database) within the flow, but a route spanning HTTPS → SFTP → IDoc is not one atomic transaction. Design for idempotency and redelivery rather than assuming rollback.


Why this matters in practice

Three situations where the Camel mental model pays off. Debugging: trace the exchange through processors instead of guessing what a step "does." Performance: recognize when a splitter-aggregator round-trip or an oversized message mapping is the bottleneck — both are classic Camel-level concerns. Design: reach for the right EIP (wire tap for logging, multicast for fan-out, circuit breaker for flaky endpoints) instead of reinventing it with script steps.

You don't need to become a Camel committer. But knowing your iFlow is a Camel route turns the step palette from a bag of magic icons into a set of well-understood patterns — and that's when integration work starts feeling predictable.


More EIPs you'll meet

Beyond the basics, a few patterns show up constantly in real flows. Wire Tap copies the message to a secondary route (logging, auditing) without disturbing the main flow — the right way to add observability. Multicast sends the same message to several endpoints in parallel and optionally aggregates responses — fan-out done properly. Content Enricher calls out to fetch additional data (a material master lookup) and merges it into the message. Circuit Breaker stops calling a failing endpoint for a cooldown period instead of hammering it — essential for flaky partner systems. Recognizing these by name lets you read any complex iFlow as a composition of known patterns.


Idempotency: designing for redelivery

Because transactions don't span the whole route, the same message can arrive twice — after a timeout, a retry, a redelivery. Idempotent Receiver is the EIP answer: track processed message IDs and skip duplicates. In practice, that means designing your receiver logic to tolerate replays: upserts instead of blind inserts, natural keys for deduplication, and a processed-message log with a sensible retention window. "What happens if this flow runs twice for the same order?" is the single most important design question for any iFlow touching a system of record — answer it before go-live, not after the duplicate invoices.


Putting patterns to work: a worked example

Consider an order-to-cash flow: HTTPS order intake → validate → enrich with customer master → route by region → map to IDoc → send to S/4HANA, with failures alerted. In EIP terms: consumer endpoint → content-based router (validation branch) → content enricher → content-based router (region) → message translator (mapping) → producer endpoint, plus an error handler route with a wire tap for audit logging. Describing it this way isn't academic — it tells the next developer exactly where to look when the French orders fail, and it tells the reviewer the design is sound before a single test message flows.

Key takeaway: Cloud Integration's runtime is Apache Camel. iFlows are routes, steps are EIP processors, messages are exchanges. Learn that mapping and debugging, performance tuning, and flow design all get easier.

Read more →
sapui5tutors

Trading Partner Management (B2B) in SAP Integration Suite

12:38:00

Connecting to one SaaS app is plumbing. Connecting to five hundred business partners — each with their own identifiers, protocols, certificates, and quirks — is a different discipline entirely. That's B2B integration, and Trading Partner Management (TPM) is the Integration Suite capability built for it.


What TPM actually manages

Every B2B relationship has the same moving parts. Partner profiles hold who the partner is: identifiers (DUNS, GLN, mutually agreed IDs), contact details, and technical endpoints. Agreements define how you exchange documents with that partner: which message types (orders, invoices, shipping notices), which protocol (AS2, SFTP, VAN), which security (certificates, encryption, signatures), and the mapping between your internal format and theirs.

Without TPM, all of this lives scattered across iFlows, certificates in keystores, and tribal knowledge in someone's head. With TPM, it's structured data: onboard a partner once, and every flow that talks to them references the profile and agreement instead of hardcoding partner specifics.

Key takeaway: TPM turns partner connectivity from scattered configuration into managed master data — profiles for who, agreements for how.

Protocols: AS2, SFTP, and the real world

B2B runs on old, battle-tested protocols. AS2 (Applicability Statement 2) is the workhorse of EDI over the internet: signed, encrypted payloads over HTTPS with non-repudiation receipts (MDNs) proving delivery. Your automotive or retail partners almost certainly speak it.

SFTP covers file-based exchange — drop a file, pick a file up, with SSH keys for authentication. Less ceremony than AS2, still everywhere. TPM manages the connectivity details for both: hostnames, ports, credentials, certificates, so iFlow developers work with partner names, not connection strings.

Certificates deserve a special mention. B2B security runs on them — signing, encryption, TLS — and they expire at the worst possible moment. TPM's centralized certificate view, with expiry visibility per partner, is the difference between a planned renewal and a 2 AM "why did orders stop" incident.


The onboarding flow

Onboarding a new partner typically goes like this:

1. Create the partner profile — identifiers, contacts, technical details.
2. Exchange security artifacts — swap certificates, agree on encryption and signing.
3. Define the agreement — message types, protocol, schedule, mappings.
4. Test end-to-end — exchange test documents, validate mappings, confirm MDNs.
5. Go live and monitor — message monitoring shows per-partner, per-agreement status.

Steps 1–3 are configuration in TPM, not code changes. That's the payoff: the tenth partner costs a fraction of the first, because the pattern is established and the iFlows are partner-agnostic — they look up the agreement at runtime.


Where TPM meets the rest of the suite

TPM doesn't work alone. Partner-specific mappings are where Integration Advisor earns its keep (proposing mappings from B2B standards). Inbound partner messages arrive through Cloud Integration iFlows that resolve the TPM agreement. And Integration Assessment can help scope which B2B scenarios to tackle first.

If your landscape exchanges EDI or structured documents with external partners at any real volume, TPM is the difference between a manageable B2B operation and a collection of fragile point-to-point connections.


Common EDI message types

A quick glossary of the documents you'll see constantly in B2B. In EDIFACT: ORDERS (purchase order), ORDRSP (order response), DESADV (despatch advice / ASN), INVOIC (invoice), RECADV (receiving advice). In ANSI X12 (North America): 850 (purchase order), 855 (PO acknowledgment), 856 (ship notice), 810 (invoice). You don't need to memorize them — but recognizing "we need the 856 flow" in a requirements meeting saves an embarrassing pause.


Partner onboarding checklist

A practical checklist distilled from the onboarding flow, suitable for pinning to the project wiki:

Commercial: trading partner agreement signed, document types and volumes agreed, support contacts exchanged.
Technical: protocol chosen (AS2/SFTP), endpoints and ports confirmed, certificates exchanged and expiry dates recorded, firewall rules requested.
Functional: MIG received or created, mappings built and unit-tested, codelist translations documented.
Cutover: end-to-end test with real-ish data, MDN/receipt verification, monitoring alerts configured, rollback plan for the first production exchange.

The items people skip — certificate expiry tracking and codelist documentation — are the items that cause incidents six months later.


Monitoring B2B flows

B2B monitoring differs from regular iFlow monitoring in one respect: the unit of interest is the business document, not the technical message. "Did partner X's invoice batch process?" matters more than "did message 8472 succeed?" TPM-aware monitoring correlates technical messages to partners and agreements, so the support team sees partner-centric status. Set up alerts per partner for missed expected documents (the EDI 997/ CONTRL functional acknowledgment not arriving is often the first sign of trouble) rather than only alerting on hard failures.

Key takeaway: Trading Partner Management structures B2B chaos — partner profiles, agreements, protocol handling (AS2/SFTP), and certificate lifecycle in one place, so onboarding the fiftieth partner is routine instead of a project.

Read more →
sapui5tutors

SAP API Management: Gateway, Policies, Portal and Analytics

12:36:00

Every company eventually faces the same mess: dozens of APIs built by different teams, no consistent security, no throttling, no idea who's calling what. Partners get a wiki page with an endpoint and a prayer. SAP API Management — part of SAP Integration Suite — exists to turn that chaos into a governed product.

It's the capability that acts as the API gateway: the single front door for publishing, securing, and throttling APIs.


The gateway: one front door for all APIs

Instead of exposing backend services directly, you put the API gateway in front. Consumers call the gateway; the gateway calls the backend. That indirection is where all the governance lives:

Security — OAuth2, API keys, mutual TLS, JWT validation. The gateway enforces authentication before any request reaches your backend, so individual services don't each reinvent it (badly).

Traffic management — rate limiting and quotas per consumer or plan. Your Gold partners get 10,000 calls/day; the trial tier gets 100. Spike protection keeps one misbehaving client from flattening the backend.

Mediation — protocol conversion (SOAP to REST), message transformation, header enrichment. The backend can stay as it is; the gateway presents the clean contract consumers expect.

A typical flow: a partner app calls api.company.com/v1/orders with an API key. The gateway validates the key, checks the quota, applies a JSON-threat-protection policy, transforms the request, and forwards it to the S/4HANA-backed service — all in milliseconds, all logged.

Key takeaway: the API gateway centralizes security, throttling, and mediation so backends stay simple and every consumer gets a consistent, governed contract.

Policies: governance as configuration

Policies are the building blocks of API proxies — small, reusable behaviors you attach to the request/response flow. SAP API Management ships with a catalog: verify API key, OAuth2 enforcement, quota, spike arrest, JSON/XML threat protection, CORS, caching, request/response transformation, and more.

The power is in composition. A production-grade proxy might chain: CORS → verify API key → quota check → threat protection → backend call → response caching → transform. Each policy is configured, not coded — which means the security team can audit the proxy definition and actually understand it.


Developer portal: treating APIs as products

An API nobody can discover might as well not exist. The developer portal is the storefront: published APIs with documentation, try-it-now consoles, and self-service onboarding. External developers register, subscribe to a plan, get credentials, and start building — without emailing your integration team.

This is the mindset shift API Management pushes: APIs as products, with an audience, versioning, deprecation policies, and SLAs — not as accidental endpoints leaked from a project.


Analytics: knowing what's actually happening

The gateway sees every call, so it can tell you everything: traffic by API and consumer, error rates, latency percentiles, quota consumption, geographic distribution. When a partner reports "your API is slow," you check the dashboard instead of guessing. When leadership asks which APIs justify investment, you show usage data.

Analytics also feeds operations: alert on error-rate spikes, watch quota exhaustion before partners hit walls, spot the endpoints nobody calls (deprecation candidates).


API lifecycle: version, deprecate, retire

APIs live for years and consumers depend on them, so versioning isn't optional. The standard approach: version in the path (/v1/orders, /v2/orders), run versions side by side, and give consumers a real deprecation timeline — announce, warn via headers, then retire. The gateway makes this manageable: route by version, apply different policies per version, and monitor who's still on the old one before you pull the plug.

Nothing destroys partner trust faster than a surprise breaking change. A published deprecation policy (e.g., "versions supported for 12 months after successor release") turns an emotional argument into a process.


Plans and entitlements

Not all consumers are equal, and plans make that explicit. A plan bundles quota, rate limits, and access rights: "Bronze" gets 1,000 calls/day on read-only endpoints, "Gold" gets 100,000/day plus write access. Partners self-subscribe through the portal; the gateway enforces the plan automatically. When sales wants to offer a premium API tier, you're configuring a plan, not rebuilding infrastructure.


Security in depth

Worth spelling out because it's where API programs fail: never rely on a single mechanism. API keys identify the caller but don't encrypt; always pair them with TLS. OAuth2 with scopes gives you per-consumer, per-operation authorization — the right default for partner APIs. Add threat protection policies (payload size limits, JSON structure validation) because a 2 GB JSON body is a denial-of-service attack wearing a trench coat. And log everything: in a dispute about "your API lost my order," the gateway's access logs are the source of truth.

Key takeaway: SAP API Management turns scattered endpoints into governed products — gateway for security and throttling, policies for reusable governance, portal for discovery and onboarding, analytics for insight. If your integration landscape has APIs facing partners or the public internet, this is the capability that keeps them sane.

Read more →
sapui5tutors

Multi-Model Data in SAP HANA: JSON, Spatial and Graph Explained

12:27:00

Most databases pick a lane: relational, document, graph, geospatial. Your data, unfortunately, doesn't. A logistics app needs delivery addresses (relational), route geometries (spatial), driver check-ins as JSON blobs (document), and a network of warehouses and routes (graph) — often in the same application.

The traditional answer is polyglot persistence: four databases, four drivers, four operational headaches, and application code stitching it all together. HANA's answer is different: one database, multiple engines, and SQL that reaches all of them.


One database, several engines

HANA is a multi-model database. Alongside the familiar relational (columnar and row) engines, it ships with:

Document store — schemaless JSON collections. You store JSON documents as-is, query into their structure with SQL, and index the fields you filter on. No shredding documents into relational tables, no object-relational mapping gymnastics.

Spatial engine — native geometry and geography types with spatial predicates: distance, intersection, containment. "Find all warehouses within 50 km of this point" is a single query, not a post-processing loop in application code.

Graph engine — nodes and edges with built-in traversal: shortest path, neighborhood expansion, pattern matching. Supply-chain networks, bill-of-material explosions, fraud rings — anything where the connections matter as much as the entities.

The point isn't that each engine exists — it's that they coexist. A single query can join a relational customer table to a JSON order document to a spatial warehouse lookup. Try doing that across four separate databases without crying.

Key takeaway: multi-model means one database serving relational, document, spatial, and graph workloads together — so cross-model queries stay inside the database instead of being stitched together in application code.

When to use each model

Document store fits semi-structured data that resists a fixed schema: IoT telemetry with varying fields per device type, user preferences, audit payloads, API request logs. If your relational design is sprouting "custom_field_1 … custom_field_47" columns, that's a document-shaped problem.

-- Querying into a JSON document collection
SELECT doc.checkin_id,
       doc.driver ->> '$.name' AS driver_name
FROM DRIVER_CHECKINS AS doc
WHERE doc.payload ->> '$.status' = 'DELAYED';

Spatial fits anything with a location question: delivery zones, asset tracking, store catchment analysis, route planning. HANA understands both planar geometry and real earth geography, so distance calculations come out in actual meters, not degrees of wishful thinking.

Graph fits connected data where traversal is the workload: BOM explosions ("all components under this assembly, recursively"), organizational hierarchies, network impact analysis, recommendation paths. Recursive SQL can do some of this, but the graph engine's traversal algorithms are built for it and dramatically faster at depth.


The practical payoff

Consider a delivery-tracking scenario. Orders live in relational tables. Driver devices send JSON check-ins with GPS coordinates (document + spatial). Warehouses, hubs, and routes form a network (graph). One operational question — "which delayed shipments are heading to warehouses affected by the storm zone?" — touches all four models.

In a multi-model HANA setup, that's one query with joins across models. In a polyglot setup, it's four database round-trips, application-side joins, and consistency headaches. The operational savings compound too: one backup strategy, one HA setup, one security model, one team that knows the system.

That's the real argument for multi-model. Not a feature checklist — fewer moving parts.


Modeling tips for the document store

Flexible schema doesn't mean no schema thinking. A few habits keep JSON collections fast: index the fields you filter on (HANA supports indexes into document structure), keep documents reasonably sized (a 5 MB document per row will hurt), and decide up front which fields are queryable versus opaque payload. The most common mistake is treating the document store as a dumping ground — schemaless storage still rewards deliberate design.

Versioning deserves thought too. When the producing system adds fields, old documents won't have them — write queries defensively (WHERE doc.payload ->> '$.newField' returns null for old docs, which is usually what you want, but verify).


Graph in practice: a traversal example

Graphs in HANA are typically modeled as vertex and edge tables. A supply-chain example: warehouses and plants as vertices, transport lanes as edges with a cost attribute. The classic question — cheapest path from plant to customer region — is a shortest-path traversal:

-- Shortest path over a weighted graph workspace
SELECT * FROM GRAPH_TABLE ( SUPPLY_NETWORK
    MATCH SHORTEST_PATH ( :start_vertex TO :end_vertex )
    COLUMNS ( vertex_id, edge_cost )
);

Writing this as recursive SQL is possible and painful; the graph engine's traversal operators handle cycles, depth limits, and weighted costs natively. If your "hierarchy" queries are getting deeper than three levels of recursion, that's the signal to model it as a graph.


Spatial indexing and real queries

Spatial queries live or die by the spatial index. Without one, "warehouses within 50 km" becomes a full scan computing distances for every row. HANA builds spatial indexes automatically for geometry columns in most cases, but verify on large tables — and remember that mixing coordinate systems (WGS84 vs. planar) in one query is a classic source of silently wrong results. Keep a consistent SRID across your spatial columns and say so in your data dictionary.

Key takeaway: reach for document, spatial, or graph models when the data's shape demands it — and keep it all in HANA so cross-model questions stay as single queries, not multi-database plumbing.

Read more →
sapui5tutors

SQLScript and Calculation Views in SAP HANA: A Practical Guide

12:25:00

Sooner or later, every HANA developer hits a wall with plain SQL. You need loops, conditionals, cursors, exception handling — real procedural logic — but you need it running inside the database, close to the data, not in a loop in ABAP or Node.js dragging rows back and forth. That's what SQLScript is for.

This post covers SQLScript, how ABAP calls it through AMDP, and where calculation views fit — including what's deprecated and what replaced it.


SQLScript: procedural logic inside HANA

SQLScript is HANA's extension of SQL with procedural constructs: variables, IF/CASE branches, loops, cursors, exception handlers, and table variables. The key idea is set-based processing — you operate on whole tables at once instead of row-by-row, and the database engine parallelizes it across cores.

A simple example — a procedure that computes total revenue per region:

CREATE PROCEDURE GET_REVENUE_BY_REGION (
    OUT result TABLE (REGION NVARCHAR(50), REVENUE DECIMAL(15,2))
)
LANGUAGE SQLSCRIPT AS
BEGIN
    result = SELECT region,
                    SUM(amount) AS revenue
             FROM SALES_ORDERS
             GROUP BY region;
END;

Nothing fancy, but notice the pattern: SQLScript procedures take table-typed parameters and return tables. They compose — one procedure's output feeds the next. The golden rule: keep it declarative and set-based. The moment you write a cursor loop over a million rows, you've usually lost the plot; there's almost always a set-based rewrite.

Key takeaway: SQLScript brings procedural logic into the database so compute-heavy work runs where the data lives. Write it set-based — table in, table out — and let HANA parallelize it.

AMDP: calling SQLScript from ABAP

ABAP developers don't write SQLScript directly. They use AMDP — ABAP Managed Database Procedures. An AMDP is an ABAP class method whose body is SQLScript, managed by the ABAP development tools (transport, syntax check, activation) but executed on the HANA database.

CLASS zcl_revenue_calc DEFINITION PUBLIC.
  PUBLIC SECTION.
    INTERFACES if_amdp_marker_hdb.
    METHODS get_revenue
      IMPORTING VALUE(iv_year)   TYPE gjahr
      EXPORTING VALUE(et_result) TYPE ztt_revenue.
ENDCLASS.

CLASS zcl_revenue_calc IMPLEMENTATION.
  METHOD get_revenue BY DATABASE PROCEDURE
         FOR HDB LANGUAGE SQLSCRIPT.
    et_result = SELECT region, SUM(amount) AS revenue
                FROM sales_orders
                WHERE year = :iv_year
                GROUP BY region;
  ENDMETHOD.
ENDCLASS.

The classic use case: logic too complex for a CDS view but too data-intensive to pull into the ABAP layer. Aggregations over huge tables, multi-step transformations, anything where moving the data would cost more than moving the logic. If you find yourself SELECTing a million rows into an internal table just to loop and aggregate — that's an AMDP-shaped problem.


Calculation views: the modeling layer

Calculation views are HANA's graphical (and SQL-based) modeling artifacts — the successors of the old analytic/attribute views. You build them in the database explorer by combining tables with joins, unions, aggregations, and filters, or you write them in SQL. They expose a clean, consumable result set that reporting tools, CDS table functions, and applications query like a table.

A typical pattern: a calculation view joins sales orders to customers and products, computes derived measures, and exposes it all as one virtual table. Downstream consumers never see the complexity.

Under the hood, calculation views execute on HANA's calculation engine — the optimizer path specifically built for these composed, columnar operations. When someone asks "which engine processes calculation views," that's the answer: not the row engine, not the join engine — the calculation engine.


What's deprecated (and what replaced it)

This trips people up in interviews and in real projects: scripted calculation views were deprecated back in HANA 1.0 SPS 11. The old approach — embedding SQLScript directly inside a calculation view — is dead.

The replacement is the CDS table function: you write the SQLScript in an AMDP method, then expose it as a CDS entity via a table function definition. Consumers query it like any other CDS view, but the heavy logic runs as SQLScript on the database.

-- CDS table function definition
@EndUserText.label: 'Revenue by region'
DEFINE TABLE FUNCTION ZTF_REVENUE_BY_REGION
WITH PARAMETERS @Environment.systemField: #CLIENT
                clnt : abap.clnt,
                year : abap.numc(4)
RETURNS {
  client : abap.clnt;
  region : abap.char(50);
  revenue : abap.curr(15,2);
}
IMPLEMENTED BY METHOD zcl_revenue_calc=>get_revenue;

So the modern stack is: SQLScript in AMDP for the logic, table function to expose it, calculation views (graphical or SQL-based) for pure modeling. Scripted calc views belong in migration plans, not new designs.

Key takeaway: put procedural logic in SQLScript (via AMDP from ABAP), model with graphical or SQL calculation views, and expose scripted logic through CDS table functions. Scripted calculation views are deprecated — don't build new ones.

Read more →
sapui5tutors

SAP HANA Architecture: Column Store, Dictionary Compression and Delta Merge Explained

12:24:00

Ask any SAP developer what makes HANA fast and you'll hear "in-memory" within seconds. That's true, but it's only half the story. Plenty of databases can hold data in RAM. What sets HANA apart is how it organizes that data — and the column store is the heart of it.

This post walks through the three ideas that do most of the heavy lifting in HANA's architecture: the column store, dictionary compression, and the delta merge. Understand these and a lot of HANA behavior — good and bad — starts making sense.


Why column store wins for analytics

Traditional databases store tables row by row. A sales order row — order number, customer, date, ten line items, amounts, statuses — sits together in one contiguous block. That's perfect when your workload reads or writes whole rows, like posting a document or displaying a single order.

Analytical workloads don't work like that. A typical report asks: "give me total revenue by region for the last quarter." It needs one or two columns out of fifty, across millions of rows. In a row store, the engine drags every full row off disk (or memory) just to read two fields. Most of the data it touches gets thrown away.

A column store flips the layout: each column is stored contiguously. Reading "revenue" and "region" means scanning exactly those two columns — nothing else. For wide tables with selective queries, that's an enormous win. It's the single biggest reason HANA chews through aggregations that would choke a row-based system.

There's a second, subtler advantage. Values inside one column look alike. A "country" column contains a few dozen distinct values repeated millions of times; a "status" column maybe five. Similar values compress far better than the mixed bag inside a row. Which brings us to...


Dictionary compression: small IDs instead of long strings

Here's the trick. Instead of storing the string "Germany" a million times, HANA builds a dictionary: a sorted list of every distinct value in the column. "Germany" becomes value ID 7, stored as a tiny integer. The column itself is just a dense array of small integers.

Two things fall out of this. First, memory usage collapses — a column of repeated strings shrinks to a fraction of its original size. Second, and less obvious, many operations get faster. Comparing integers is cheaper than comparing strings, so filters, joins, and group-bys on compressed columns run on the small IDs and only translate back to real values at the very end.

The dictionary is sorted, which enables another optimization: range queries and binary search on the dictionary itself. And because the IDs are assigned in sort order, min/max aggregations can sometimes be answered from the dictionary alone without touching the column data at all.

Key takeaway: dictionary compression isn't just about saving memory. Storing small integer value IDs instead of raw values makes scans, filters, and aggregations genuinely faster.

The write problem — and the delta store

Column stores have one famous weakness: writes. Appending a single row means touching every column's data structure — fifty separate writes for one logical insert. Do that for every incoming sales order and performance falls off a cliff.

HANA's answer is the delta store: a small, row-oriented staging area sitting in front of each column table. All inserts, updates, and deletes land in the delta first, where row-wise writes are cheap. Reads transparently combine both stores, so queries always see fresh data.

Think of it like a desk in-tray. New paperwork piles up in the tray (fast to drop in, messy to search), and periodically someone files it all into the cabinet (the columnar main store) in one organized batch. That batch filing is the delta merge.


Delta merge: filing the in-tray

During a delta merge, HANA takes everything accumulated in the delta store, sorts and compresses it, and folds it into the main column store — rebuilding dictionaries as needed. It's a heavier operation, so HANA doesn't do it on every write. Instead it triggers merges based on heuristics: delta size, time since last merge, memory pressure.

This is worth knowing because a bloated delta store is one of the classic HANA performance culprits. If writes vastly outpace merges, queries slow down — they're scanning an ever-larger uncompressed row store alongside the main store. Symptoms: gradually degrading read performance on a table with heavy insert activity.

You can check delta sizes in the HANA cockpit and, if needed, trigger a manual merge:

-- Force a delta merge on a specific table
MERGE DELTA OF "MYSCHEMA"."SALES_ORDERS";

In practice, HANA's automatic merge usually keeps up. Manual merges are for the exceptions — bulk loads, data migrations, or troubleshooting a slow table.


Putting it together

The full picture is elegant: writes land in the row-based delta (fast inserts), reads hit the compressed columnar main store (fast analytics), and the delta merge continuously converts one into the other. Dictionary compression keeps the main store small and scans quick.

Next time someone says "HANA is fast because it's in-memory," you'll know better. Memory helps. But the column store, the dictionaries, and the delta merge are doing the real work.

Key takeaway: HANA's speed comes from architecture, not just RAM — columnar layout for selective scans, dictionary compression for size and speed, and a delta store plus merge to keep writes cheap without sacrificing read performance.

Read more →
sapui5tutors

@OData.publish: Exposing CDS Views as OData Services

12:22:00

You have a CDS view with clean data and good annotations, and someone asks: "Can we get this as an API?" On a modern ABAP stack, the answer can be one annotation. But that one annotation comes with boundaries worth understanding before you lean on it — what it generates, what it cannot do, and when to graduate to a fuller approach.

What it does

The @OData.publish: true annotation is the fastest way to turn a CDS view into a working OData service. Add it to your CDS view definition, activate, and the framework generates the service artifacts for you — no SEGW project, no manual service definition.

@AbapCatalog.sqlViewName: 'ZV_BOOKING'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Booking view'
@OData.publish: true
define view Z_C_Booking as select from zbooking {
  key booking_id,
      customer,
      flight_date,
      @Semantics.amount.currencyCode: 'currency'
      price
}

What Gets Generated

When you activate a CDS view annotated with @OData.publish: true, the system generates:

  • An OData service named after the view (typically <view>_CDS).
  • An entity set exposing the view's fields.
  • Basic query support ($filter, $orderby, $top, $skip) derived from the view definition.

You can find the generated service in transaction /IWFND/MAINT_SERVICE and register it on the hub system like any other Gateway service.


Which CDS views qualify

Not every CDS view can be published this way. The annotation works on view entities that the framework can map cleanly to an OData entity set:

  • Read-shaped views: standard define view entity definitions with a key. The key becomes the OData entity key.
  • No input parameters: views with with parameters cannot be published — OData entity sets have no parameter concept, so the framework refuses to generate the service. Remove the parameters or wrap the view in a parameter-free projection.
  • Annotation-driven UI: @UI annotations on the view flow through to the generated service's metadata, which is why a published CDS view can feed a Fiori Elements app with almost no extra work.

If activation throws an error mentioning OData generation, the view definition is usually the cause — check for parameters first, then for unsupported constructs in the projection list.

@OData.publish only works on parameter-free view entities. If your view takes input parameters, expose a parameter-free projection on top of it and publish that instead.

@OData.publish vs SEGW vs RAP: choosing the path

Three generations of "expose ABAP data as OData" coexist on SAP systems. Knowing which one fits saves real rework:

@OData.publishSEGW projectRAP service
EffortOne annotationModel + code-based implementationService definition + binding
ReadsYes, full query supportYes, whatever you implementYes
Writes (CUD)NoYes, hand-codedYes, with draft + validations
Custom actionsNoYes (function imports)Yes (actions/functions)
Best forQuick read APIs, lookupsLegacy Gateway servicesTransactional Fiori apps

The honest rule of thumb: @OData.publish is the fastest route to a read API and the wrong route to a transactional app. SEGW is the legacy path — maintain existing projects there, but do not start new ones. RAP is where new transactional development belongs.


When to use it — and when not to

@OData.publish is ideal for read-only scenarios: exposing master data, analytical views, or simple lookups where you don't need transactional behavior. It's the quickest path from a CDS view to a consumable API.

It is not a replacement for RAP. If you need create/update/delete with draft handling, validations, and behavior logic, build a RAP business object with a service definition and service binding instead. @OData.publish gives you a flat read service; RAP gives you a full application model.


A Practical Flow

  1. Write your CDS view with the annotations your UI needs (@UI, @Semantics).
  2. Add @OData.publish: true at the header.
  3. Activate. Note the generated _CDS service name.
  4. Register and test the service in /IWFND/GW_CLIENT.
  5. Consume it from a Fiori app or external client.

A note on step 4: /IWFND/GW_CLIENT lets you fire raw requests at the service without building a client first. Test the entity set read, a $filter, and an $expand on any associations before you hand the URL to an app team. Catching a missing key or a misbehaving association here takes minutes; catching it from a Fiori app's network tab takes much longer.

Also worth knowing: the generated service gets a version from the system, and regenerating after view changes is automatic on reactivation. If you rename the CDS view, the service name changes with it — treat the published service name as derived, not stable, and point consumers at it through stable documentation rather than tribal knowledge.

Key takeaway: @OData.publish: true turns a CDS view into a live OData service at activation — perfect for read-only exposure. For transactional apps with behavior logic, reach for RAP instead.


Bottom line

One annotation, one activation, one working read API — @OData.publish is the shortest path from a CDS view to a consumable OData service, and for lookups, master data, and analytical views it is all you need. Just respect its boundaries: parameter-free views only, reads only, no behavior logic. The moment you need writes, drafts, or validations, that boundary is telling you to move up to RAP — which builds on the same CDS views you already wrote.

Read more →
sapui5tutors

OData V2 vs V4: Actions, DateTime Literals and Prefer Header

12:20:00

Most SAP developers meet OData V4 while a V2 service is still running in production next to it. The two versions look similar at a glance — same entity sets, same query options, same $metadata — but the details differ in ways that break code silently. Here are the differences you will actually hit, in the order you will hit them.

The big picture

OData V4 isn't just a newer version — it cleaned up several V2 quirks that tripped up every SAP developer. Three of the most visible changes: function imports became actions and functions, datetime literals switched to ISO 8601, and the Prefer header gave clients control over response payloads. If you're moving from V2 (still common in older Gateway services) to V4, these are the differences you'll hit first.


Function Imports → Actions and Functions

In OData V2, custom operations were exposed as function imports — a single concept covering everything. V4 splits them into two precise concepts:

  • Functions are side-effect free. They return data and must use GET. Think of them as computed queries.
  • Actions can have side effects. They use POST and can change data.
// V2: one function import, invoked via GET
GET /sap/opu/odata/sap/ZBOOKING_SRV/ApproveBooking?BookingId='42'

// V4: a bound action, invoked via POST
POST /sap/opu/odata4/sap/zbooking/0001/Bookings('42')/com.sap.approve
Content-Type: application/json

The V4 approach is more explicit about intent, and bound actions read naturally as operations on a specific entity.


DateTime Literals: Goodbye datetime'...'

V2 wrapped datetime values in a verbose literal syntax. V4 adopts the ISO 8601 standard that every other modern API uses.

// V2 literal
$filter=CreatedAt eq datetime'2026-09-29T10:30:00'

// V4 literal — plain ISO 8601
$filter=createdAt eq 2026-09-29T10:30:00Z

No wrapper, no quotes. If your V4 queries fail with literal errors, this is usually why — old V2 habits die hard.


The Prefer Header: return=minimal

When you POST a new entity, do you want the created entity echoed back in the response, or just a confirmation? V4 lets the client decide with the Prefer header:

POST /sap/opu/odata4/sap/zbooking/0001/Bookings
Prefer: return=minimal
Content-Type: application/json

{"bookingId": "43", "customer": "1001"}
  • Without the header: the service returns 201 Created with the full entity in the body.
  • With Prefer: return=minimal: the service returns 204 No Content — just the status, no body.

Use return=minimal when you already have the data client-side and don't need the round-trip payload. It saves bandwidth, which matters on mobile and high-volume scenarios.


The JSON got leaner: goodbye d and __metadata

The first thing you notice reading a V4 response is how much less of it there is. V2 wrapped every payload in a d object and attached a __metadata block to every single entity:

// V2 response — notice the wrappers
{
  "d": {
    "__metadata": {
      "uri": ".../Bookings('42')",
      "type": "ZBOOKING_SRV.Booking"
    },
    "BookingId": "42",
    "Customer": "1001"
  }
}

V4 drops both. The payload is just the data, with a small @odata.context URL at the top describing the shape:

// V4 response — just the data
{
  "@odata.context": "$metadata#Bookings/$entity",
  "bookingId": "42",
  "customer": "1001"
}

This matters more than it looks. Client code written against V2 almost always navigates through response.d.results — every one of those paths breaks on V4, where the array sits directly in value (or the entity sits at the root). It is a mechanical fix, but it touches every read in the codebase, which is why migration estimates that only count "changed endpoints" come in low.

If your V4 migration estimate only counts changed endpoints, it is missing the biggest line item: every client read path that navigates through the old d wrapper needs rewriting.

What stayed the same (reassuringly)

Not everything changed. The core query language — $filter, $orderby, $top, $skip, $expand, $select — works the same way, with the same operators. Key addressing in URLs (Bookings('42')) is unchanged. And $metadata still describes the model, so tools and client generators keep working with minor adjustments.

The mental model carries over too: entity sets, entity types, navigation properties, and the idea that the URL addresses the data model. V4 refined the protocol; it did not redesign the concepts. A developer fluent in V2 query writing is productive in V4 within a day — the learning curve is in the payload details, not the querying.


A practical migration checklist

When you sit down to move a service or a client from V2 to V4, work through these in order:

  1. Client read paths: replace d.results / d navigation with V4's value / root-entity shape.
  2. Count handling: $inlinecount=allpages becomes $count=true, and the __count property becomes @odata.count.
  3. String search: substringof('x', prop) eq true becomes contains(prop, 'x') — arguments flip.
  4. Datetime literals: drop the datetime'...' wrapper, use plain ISO 8601.
  5. Custom operations: split function imports into GET functions (read-only) and POST actions (side effects).
  6. Write methods: replace MERGE with PATCH; check every PUT for accidental full-replacement semantics.
  7. Create responses: decide per call whether you want the entity echoed back or Prefer: return=minimal for a lean 204.

Run the old and new services side by side during the transition and diff the payloads for a few representative entities. The structural differences are predictable; the bugs come from the one read path or literal format nobody remembered to check.


Quick Reference

  • V2 function imports → V4 actions (POST, side effects) and functions (GET, read-only).
  • V2 datetime'...' → V4 plain ISO 8601.
  • Prefer: return=minimal → 204 No Content instead of 201 with the entity.

Key takeaway: OData V4 didn't just renumber the version — it replaced V2's function imports with explicit actions/functions, adopted ISO 8601 datetimes, and gave clients payload control via the Prefer header. Learn these three and V4 starts feeling familiar fast.


Bottom line

V4 is V2 with the sharp edges removed: leaner JSON, explicit actions versus functions, standard datetime literals, proper PATCH semantics, and client-controlled response payloads. The query language you already know carries over untouched. Work through the migration checklist methodically — read paths, counts, string functions, literals, operations — and the transition is a series of small mechanical changes rather than one big rewrite.

Read more →
sapui5tutors

OData Query Options: $count and $filter Functions Explained

12:17:00

Two OData query features cause a disproportionate share of confusion: $count, because V2 and V4 spell it differently, and the $filter string functions, because substringof still shows up in old code and Stack Overflow answers. Get these two right and your queries become both correct and portable.


$count: knowing how many before you fetch

When a UI shows "Showing 20 of 1,347 orders," the 1,347 comes from a count. In OData V4 you ask for it with $count=true:

GET /odata/v4/sales/Orders?$filter=status eq 'OPEN'&$count=true&$top=20

The response includes the total alongside the page of results:

{
  "@odata.count": 1347,
  "value": [
    { "orderId": "450001", "status": "OPEN", ... },
    ...
  ]
}

Note the details: @odata.count reflects the count after $filter but before $top/$skip. That is exactly what pagination needs — the total matching set, not the page size.

You can also request just the number, without any entities:

GET /odata/v4/sales/Orders/$count?$filter=status eq 'OPEN'
→ 1347  (plain text response)

The V2 equivalent was $inlinecount=allpages, which returned __count in the payload. If you are migrating V2 code, this is a straight mechanical replacement — but watch out for any client code that reads the old __count property name.

$count=true gives you the filtered total with your page of data. /$count gives you just the number. Both count after $filter, before $top/$skip.

$filter functions: contains() is the V4 way

Filtering on exact matches (status eq 'OPEN') is straightforward. Substring search is where versions diverge. In V4, the canonical function is contains:

GET /odata/v4/sales/Customers?$filter=contains(name, 'Acme')

In V2, the equivalent was substringof — with the arguments reversed:

GET /odata/v2/sales/Customers?$filter=substringof('Acme', name) eq true

That reversed argument order is a classic migration trap. V4's contains(property, 'text') reads naturally: does this property contain this text? V2's version reads backwards and returns an explicit boolean you had to compare with eq true.

Other string functions you will use constantly in V4 filters:

$filter=startswith(orderId, '450')      → prefix match
$filter=endswith(email, '@example.com')  → suffix match
$filter=contains(tolower(name), 'acme')  → case-insensitive search
$filter=length(description) gt 100       → length check

The tolower trick deserves emphasis: contains is case-sensitive, so wrapping the property in tolower (and lowercasing your search term) is the standard way to do case-insensitive search. Whether the backend translates this efficiently depends on the service — on HANA-backed CAP services it generally becomes a proper SQL predicate.


Combining filters

Real filters combine conditions with and, or, and not. Parentheses control precedence, and the rules are the same as in most languages — and binds tighter than or:

$filter=status eq 'OPEN' and (contains(name, 'Acme') or total gt 1000)

A few practical rules of thumb:

  • Filter before you count, count before you page. $filter + $count=true + $top/$skip is the standard pagination recipe.
  • Prefer server-side filtering over client-side. Fetching 10,000 entities to filter in the browser is never the answer — if a filter is not supported, that is a service gap to fix, not a client workaround to build.
  • Check $metadata for filterable properties. Services can mark properties as non-filterable. The metadata tells you; guessing does not.
When migrating V2 queries: $inlinecount=allpages becomes $count=true, and substringof('x', prop) becomes contains(prop, 'x'). The argument order flips — that is the detail that breaks migrations.

Nulls, dates, and the in operator

Three more filter details that come up in real queries. First, null checks use plain comparison — no is null keyword:

$filter=shippedAt eq null        → not yet shipped
$filter=customerId ne null       → has a customer assigned

Second, date and time parts have their own functions, which is far cleaner than string-slicing an ISO timestamp:

$filter=year(orderDate) eq 2026
$filter=month(orderDate) eq 9 and day(orderDate) ge 15

Third, V4.01 added the in operator, which replaces long chains of or conditions:

" instead of this:
$filter=status eq 'OPEN' or status eq 'PENDING' or status eq 'BACKORDER'

" write this:
$filter=status in ('OPEN', 'PENDING', 'BACKORDER')

Support for in depends on the service version — CAP-based V4 services generally handle it, older Gateway V4 stacks may not. If it fails, fall back to the or chain; the semantics are identical.


Counting at scale: a performance note

$count=true is convenient, but on very large entity sets the count itself can be expensive — the database still has to evaluate the full filtered set to count it. A few guidelines from the field:

Count once, page many. The total only changes when the underlying data or the filter changes. Cache @odata.count client-side for the session rather than requesting it on every page turn.

Watch unfiltered counts on huge sets. GET /Orders/$count with no filter on a table with tens of millions of rows is a full table scan. If your UI only needs "more than 10,000," consider whether the service offers a capped count instead.

Let the database do the work. A filter the backend can translate into a SQL predicate (equality, ranges, contains on indexed columns) keeps both the count and the page fast. A filter on a computed or unmapped property may force the service to materialize rows first — check $metadata for which properties are actually filterable and sortable.

Pagination is a contract between client and service: $filter narrows, $count=true totals, $top/$skip pages. Put all three in one request and the round trip stays constant no matter how deep into the list the user scrolls.

Quick reference

NeedV4V2 (for migration)
Total with results$count=true → @odata.count$inlinecount=allpages → __count
Just the number/EntitySet/$count/EntitySet/$count
Substring matchcontains(prop,'x')substringof('x',prop) eq true
Prefix matchstartswith(prop,'x')startswith(prop,'x') eq true

Bottom line

$count=true plus $filter plus $top/$skip is the complete pagination recipe — one request returns a page of data and the total it was drawn from. For text search, contains() is the V4 function to reach for, with tolower() as the standard case-insensitivity trick. And when you migrate old V2 queries, watch the two traps: the __count property name disappears, and substringof's arguments flip order. Get those right and your queries work first time on any V4 service.

Read more →
sapui5tutors

OData V4 CRUD Deep-Dive: PATCH, Deep Insert and $batch

12:15:00

Reading data with OData is the easy part — GET an entity set, maybe add a $filter, done. Writing data is where V4 shows how much it improved over V2. Three operations cover nearly every write scenario you will meet: PATCH for partial updates, deep insert for creating graphs of related entities, and $batch with change sets for atomic multi-operation writes.


PATCH: partial updates done right

In OData V2, partial updates used MERGE — a method that was never part of the HTTP standard and confused every proxy and firewall it met. V4 replaced it with PATCH, a proper HTTP method with clear semantics: send only the properties you want to change.

PATCH /odata/v4/sales/Orders('450001')
Content-Type: application/json

{
  "status": "SHIPPED",
  "shippedAt": "2026-09-29T10:30:00Z"
}

Only status and shippedAt change. Every other property keeps its current value. Contrast this with PUT, which in V4 means full replacement — any property you omit gets reset to its default. Mixing those two up is one of the most common OData bugs, and it is worth stating plainly: PATCH for partial, PUT for full replacement.

The response to a successful PATCH is 204 No Content by default (or 200 OK with the updated entity if the client asked for it via the Prefer header — more on that in the V2-vs-V4 post).

PATCH sends only what changed. PUT replaces the whole entity. Choosing the wrong one either silently wipes fields or needlessly ships the entire payload.

Deep insert: creating an entity graph in one request

Suppose you need to create an order along with its line items. The naive approach is N+1 requests: one POST for the order, then one POST per item into the navigation property. Deep insert collapses that into a single POST by nesting the related entities in the payload:

POST /odata/v4/sales/Orders
Content-Type: application/json

{
  "customerId": "1001",
  "status": "OPEN",
  "items": [
    { "productId": "P-100", "quantity": 2, "price": "25.00" },
    { "productId": "P-205", "quantity": 1, "price": "99.00" }
  ]
}

The service creates the order and both items atomically and returns 201 Created with the full graph (including generated keys). The nested property name (items here) is the navigation property from the metadata.

Deep insert only works for navigation properties that the service allows to be created inline — check $metadata for the navigation property's capabilities. When it is supported, it eliminates an entire category of "half-created" data problems, because the client never holds a partially built graph.


$batch: atomic writes with change sets

Deep insert handles one entity graph. But what about updating three unrelated orders, or deleting five line items, as a single atomic unit? That is what $batch is for.

A batch request is a multipart/mixed payload containing individual HTTP requests. Requests grouped inside a change set execute atomically — all succeed, or all roll back:

POST /odata/v4/sales/$batch
Content-Type: multipart/mixed; boundary=batch_123

--batch_123
Content-Type: multipart/mixed; boundary=changeset_1

--changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary

PATCH /odata/v4/sales/Orders('450001')
Content-Type: application/json

{"status": "SHIPPED"}

--changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary

PATCH /odata/v4/sales/Orders('450002')
Content-Type: application/json

{"status": "SHIPPED"}

--changeset_1--
--batch_123--

Both orders ship, or neither does. Requests outside a change set (directly in the batch body) run independently — useful for bundling reads, where atomicity does not matter and you just want fewer round trips.

A practical note: change sets cannot reference entities created earlier in the same batch via the $1 content-ID syntax in every implementation — support varies. If your scenario needs "create the order, then patch it in the same batch," test it against your specific service before relying on it. When in doubt, deep insert covers the create-graph case more portably.

Use deep insert for creating related entities together. Use $batch change sets when independent write operations must succeed or fail as one unit.

Choosing between the three

ScenarioUse
Change a few fields on one entityPATCH
Create an entity with its childrenDeep insert (single POST)
Multiple unrelated writes, all-or-nothing$batch with a change set
Bundle independent reads$batch without a change set

Master these three and you have covered the write side of OData V4. The read side — query options like $count and $filter functions — deserves its own post, which is exactly what comes next.


Bottom line

PATCH for surgical field updates, deep insert for creating whole object graphs in one round trip, and $batch change sets when independent writes must live or die together. Reach for the lightest option that fits — most write scenarios need nothing more than a well-placed PATCH — and reserve batch payloads for the cases where atomicity genuinely matters. Your payloads stay small, your data stays consistent, and your error handling stays simple.

Read more →