~/abap.dev — code, crafted in SAP
florin @ s4hana : ~/abap/posts/odata-v4-deep-insert $ cat README.md

OData v4 Deep Insert with ABAP RAP

Create a header and its child items in a single POST request using the RAP framework's built-in deep insert support — no manual loop required.

Overview

OData v4 deep insert lets you create a parent entity together with its associated child entities in one atomic request. In ABAP RAP, this maps directly to the CREATE ENTITY ... FROM ... syntax with nested structures.

The key prerequisites are:

  • The child entity is declared as a composition in the CDS model
  • The behavior definition exposes create on the child association
  • The managed runtime handles the actual DB inserts

The ABAP Behavior Definition

First, declare the association as composition in your behavior definition so that RAP knows to handle the child as part of the same transaction:

* Behavior definition — manage draft + deep insert
managed implementation in class ZBP_SALES_ORDER unique;
strict ( 2 );

define behavior for ZSALES_ORDER_HDR alias SalesOrder
  persistent table zsales_hdr
  draft table zsales_hdr_d
  lock master total etag LastChangedAt
  authorization master ( global ) {

  field ( readonly ) SalesOrderID, CreatedAt;
  create; update; delete;
  draft action Activate;
  draft action Discard;
  draft determine action Prepare;

  " deep composition — items travel with the header
  association _Items { create; with draft; }
}

The CREATE Handler

The managed runtime handles deep insert automatically when the request body contains nested items. If you need custom validation, override augmentCreate:

CLASS ZBP_SALES_ORDER DEFINITION
  PUBLIC FINAL CREATE PRIVATE.

  PUBLIC SECTION.
    INTERFACES if_abap_behv.

ENDCLASS.

CLASS ZBP_SALES_ORDER IMPLEMENTATION.

  METHOD augmentCreate.
    DATA ls_order  TYPE STRUCTURE FOR CREATE ZSALES_ORDER_HDR.

    LOOP AT entities INTO ls_order.
      " stamp server-side fields before persistence
      ls_order-CreatedAt = cl_abap_context_info=>get_system_date( ).
      ls_order-CreatedBy = cl_abap_context_info=>get_user_alias( ).
      ls_order-%control-CreatedAt = if_abap_behv=>mk-on.
      ls_order-%control-CreatedBy = if_abap_behv=>mk-on.
      MODIFY ENTITIES OF ZSALES_ORDER_HDR IN LOCAL MODE
        ENTITY SalesOrder UPDATE FIELDS ( CreatedAt CreatedBy )
          WITH VALUE #( ( ls_order ) )
        REPORTED DATA( ls_reported ).
    ENDLOOP.
  ENDMETHOD.

ENDCLASS.

The OData request payload

A deep insert POST to /sap/opu/odata4/sap/zsales/srvd/sap/zsales_srv/0001/SalesOrder:

{
  "SoldToParty": "CUST-0042",
  "Currency":    "EUR",
  "_Items": [
    { "Material": "MAT-001", "Quantity": 10, "NetAmount": 250.00 },
    { "Material": "MAT-007", "Quantity":  3, "NetAmount":  99.90 }
  ]
}

Key Takeaways

  • Declare the child association as composition with create permission
  • The managed runtime maps the nested JSON array to child entity creates automatically
  • Use augmentCreate only for server-side field defaulting — don’t re-implement what managed gives you
  • Enable with draft on the association if the parent uses draft handling