EML (Entity Manipulation Language) is the specialized language in ABAP for interacting with RAP Business Objects. Instead of direct database access (SELECT, UPDATE, DELETE), you use EML for type-safe, transactional operations with full business logic.
Why EML?
Problem with classic ABAP:
" BAD: Direct DB access bypasses business logicUPDATE ztravel SET status = 'A' WHERE travel_id = '00000001'.COMMIT WORK." -> Validations, Determinations, Actions are NOT executed!" -> No error handling" -> No transactional consistencySolution with EML:
" GOOD: EML respects business logicMODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) ) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES." -> All Validations/Determinations from BDEF are executed" -> Structured error handling via FAILED/REPORTED" -> Transactional consistency guaranteedEML Basic Structure
All EML operations follow this pattern:
<OPERATION> ENTITIES OF <root_entity> ENTITY <entity_alias> <OPERATION_DETAILS> [MAPPED DATA(mapped)] " New keys after Create [FAILED DATA(failed)] " Error information [REPORTED DATA(reported)]. " Messages for UI
COMMIT ENTITIES [RESPONSE OF <root_entity> FAILED DATA(commit_failed) REPORTED DATA(commit_reported)].READ ENTITIES: Reading Data
Basic Syntax
READ ENTITIES OF zi_travel ENTITY Travel FIELDS ( TravelId AgencyId CustomerName Status ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel) FAILED DATA(failed) REPORTED DATA(reported).Reading All Fields
" ALL FIELDS instead of individual field listREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ( TravelId = '00000002' ) ( TravelId = '00000003' ) ) RESULT DATA(lt_travels).
LOOP AT lt_travels INTO DATA(ls_travel). WRITE: / ls_travel-TravelId, ls_travel-Description, ls_travel-Status.ENDLOOP.Reading Associations (BY _Association)
" Read Travel with its BookingsREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel)
" Navigate via association ENTITY Travel BY \_Bookings ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_bookings).
" Now we have:" - lt_travel: The travel itself" - lt_bookings: All bookings for this travel
WRITE: / |Travel { lt_travel[ 1 ]-TravelId } has { lines( lt_bookings ) } bookings|.Reading Only Links (ASSOCIATION LINKS)
" Only the relationships, not the dataREAD ENTITIES OF zi_travel ENTITY Travel BY \_Bookings FROM VALUE #( ( TravelId = '00000001' ) ) LINK DATA(lt_links).
" lt_links contains only keys:" source (TravelId) -> target (TravelId + BookingId)IN LOCAL MODE (without Authorization Check)
" For Behavior Implementations or privileged operationsREAD ENTITIES OF zi_travel IN LOCAL MODE ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel).
" -> No Authorization Checks" -> Use this ONLY in Behavior Implementations!MODIFY ENTITIES: Changing Data
CREATE: Creating New Entities
MODIFY ENTITIES OF zi_travel ENTITY Travel CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate Description ) WITH VALUE #( ( %cid = 'CID_1' " Client-ID for mapping AgencyId = '000001' CustomerId = '000042' BeginDate = cl_abap_context_info=>get_system_date( ) EndDate = cl_abap_context_info=>get_system_date( ) + 14 Description = 'Business Trip Munich' )
( %cid = 'CID_2' AgencyId = '000002' CustomerId = '000099' BeginDate = '20250601' EndDate = '20250615' Description = 'Vacation Mallorca' ) ) MAPPED DATA(mapped) " Contains generated TravelIds FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES RESPONSE OF zi_travel FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
" Retrieve new TravelId via %cid mapping:IF sy-subrc = 0. DATA(lv_new_travel_id) = mapped-travel[ %cid = 'CID_1' ]-TravelId. WRITE: / |New travel created: { lv_new_travel_id }|.ENDIF.CREATE BY: Creating via Association
" Create booking for existing travelMODIFY ENTITIES OF zi_travel ENTITY Travel CREATE BY \_Bookings FIELDS ( BookingDate CustomerId CarrierId FlightPrice ) WITH VALUE #( ( TravelId = '00000001' " Parent Key %target = VALUE #( ( %cid = 'BOOK_1' BookingDate = sy-datum CustomerId = '000042' CarrierId = 'LH' FlightPrice = '499.99' CurrencyCode = 'EUR' ) ) ) ) MAPPED DATA(mapped) FAILED DATA(failed).
COMMIT ENTITIES.
" New BookingId:DATA(lv_booking_id) = mapped-booking[ %cid = 'BOOK_1' ]-BookingId.UPDATE: Updating Fields
" Change status and descriptionMODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status Description ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' " Accepted Description = 'Approved on ' && sy-datum )
( TravelId = '00000002' Status = 'X' " Rejected Description = 'Rejected' ) ) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES.
" Error handling:IF failed-travel IS NOT INITIAL. LOOP AT reported-travel INTO DATA(ls_msg). DATA(lv_text) = ls_msg-%msg->if_message~get_text( ). WRITE: / 'Error:', lv_text. ENDLOOP.ENDIF.UPDATE SET FIELDS (all non-initial)
" UPDATE without explicit FIELDS -> all fields will be overwritten!DATA(ls_update) = VALUE zi_travel( TravelId = '00000001' Status = 'A' Description = 'New description' " BeginDate, EndDate etc. remain unchanged (not specified)).
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE SET FIELDS WITH VALUE #( ( CORRESPONDING #( ls_update ) ) ) FAILED DATA(failed).
" -> Only Status and Description are updatedDELETE: Deleting Entities
" Delete travel(s)MODIFY ENTITIES OF zi_travel ENTITY Travel DELETE FROM VALUE #( ( TravelId = '00000042' ) ( TravelId = '00000043' ) ) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES.
" Check if successful:IF line_exists( failed-travel[ TravelId = '00000042' ] ). WRITE: / 'Delete failed for 00000042'.ELSE. WRITE: / 'Successfully deleted: 00000042'.ENDIF.EXECUTE: Running Actions
Instance Action
" Execute action 'acceptTravel' for a travelMODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE acceptTravel FROM VALUE #( ( TravelId = '00000001' ) ( TravelId = '00000002' ) ) RESULT DATA(result) " If action returns a result FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES.
" result contains updated travel data (if action has `result [1] $self`)LOOP AT result INTO DATA(ls_result). WRITE: / |Travel { ls_result-TravelId } Status: { ls_result-Status }|.ENDLOOP.Static Action
" Static action (without instance)MODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE createDefaultTravel RESULT DATA(result) MAPPED DATA(mapped).
COMMIT ENTITIES.
" New travel was created:DATA(lv_new_id) = mapped-travel[ 1 ]-TravelId.Factory Action
" Factory Action: Creates new instance based on templateMODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE copyTravel FROM VALUE #( ( TravelId = '00000001' " Template %param = VALUE #( Description = 'Copy of Travel 1' ) ) ) MAPPED DATA(mapped).
COMMIT ENTITIES.
" New copied travel:DATA(lv_copy_id) = mapped-travel[ 1 ]-TravelId.Action with Parameters
" Action with parameter structureMODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE setDiscount FROM VALUE #( ( TravelId = '00000001' %param-Percentage = 10 " 10% discount %param-Reason = 'Loyalty discount' ) ) RESULT DATA(result).
COMMIT ENTITIES.
" result-%param contains return values of the actionDATA(lv_new_price) = result[ 1 ]-%param-NewTotalPrice.COMMIT ENTITIES: Completing the Transaction
Simple Commit
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) ).
" Changes are only written to DB hereCOMMIT ENTITIES.
IF sy-subrc = 0. WRITE: / 'Successfully committed'.ENDIF.Commit with Error Handling
MODIFY ENTITIES OF zi_travel ENTITY Travel CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate ) WITH VALUE #( ( %cid = 'CID_1' AgencyId = '999999' ) )." -> AgencyId does not exist -> Validation fails
COMMIT ENTITIES RESPONSE OF zi_travel FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
IF commit_failed-travel IS NOT INITIAL. WRITE: / 'Commit failed!'. LOOP AT commit_reported-travel INTO DATA(ls_msg). WRITE: / ls_msg-%msg->if_message~get_text( ). ENDLOOP.
" Transaction was automatically rolled back!ENDIF.COMMIT with RESPONSE and Mapping
MODIFY ENTITIES OF zi_travel ENTITY Travel CREATE FIELDS ( AgencyId CustomerId ) WITH VALUE #( ( %cid = 'CID_1' AgencyId = '000001' CustomerId = '000042' ) ) MAPPED DATA(mapped).
COMMIT ENTITIES RESPONSE OF zi_travel MAPPED DATA(commit_mapped) " Final keys after commit FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
IF sy-subrc = 0. " commit_mapped overwrites mapped with final DB keys DATA(lv_final_id) = commit_mapped-travel[ %cid = 'CID_1' ]-TravelId. WRITE: / |Final Travel ID: { lv_final_id }|.ENDIF.Error Handling with FAILED & REPORTED
FAILED: Which Entities Failed?
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) ( TravelId = '99999999' Status = 'A' ) " Does not exist ( TravelId = '00000003' Status = 'A' ) ) FAILED DATA(failed).
COMMIT ENTITIES.
" failed-travel contains keys of failed entitiesIF line_exists( failed-travel[ TravelId = '99999999' ] ). WRITE: / 'Travel 99999999 could not be updated'.
" %fail-cause indicates the reason: DATA(ls_failed) = failed-travel[ TravelId = '99999999' ]. CASE ls_failed-%fail-cause. WHEN if_abap_behv=>cause-not_found. WRITE: / '-> Entity not found'. WHEN if_abap_behv=>cause-unauthorized. WRITE: / '-> No authorization'. WHEN if_abap_behv=>cause-unspecific. WRITE: / '-> See REPORTED for details'. ENDCASE.ENDIF.REPORTED: Error Messages for UI
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( BeginDate EndDate ) WITH VALUE #( ( TravelId = '00000001' BeginDate = '20250615' EndDate = '20250601' ) ) " End before Begin! REPORTED DATA(reported).
COMMIT ENTITIES.
" reported-travel contains messagesLOOP AT reported-travel INTO DATA(ls_report). " %msg is of type REF TO if_abap_behv_message DATA(lo_msg) = ls_report-%msg.
" Get text: DATA(lv_text) = lo_msg->if_message~get_text( ). WRITE: / lv_text.
" Check severity: DATA(lv_severity) = lo_msg->if_abap_behv_message~m_severity. IF lv_severity = if_abap_behv_message=>severity-error. WRITE: / '-> Error!'. ENDIF.
" Affected fields: IF ls_report-%element-EndDate = if_abap_behv=>mk-on. WRITE: / '-> Problem with field EndDate'. ENDIF.ENDLOOP.MAPPED: New Keys After CREATE
MODIFY ENTITIES OF zi_travel ENTITY Travel CREATE FIELDS ( AgencyId CustomerId ) WITH VALUE #( ( %cid = 'CID_ALPHA' AgencyId = '000001' CustomerId = '000042' ) ( %cid = 'CID_BETA' AgencyId = '000002' CustomerId = '000099' ) ) MAPPED DATA(mapped).
COMMIT ENTITIES RESPONSE OF zi_travel MAPPED DATA(commit_mapped).
" After CREATE: mapped contains generated keysWRITE: / |Alpha Travel ID: { mapped-travel[ %cid = 'CID_ALPHA' ]-TravelId }|.WRITE: / |Beta Travel ID: { mapped-travel[ %cid = 'CID_BETA' ]-TravelId }|.
" After COMMIT: commit_mapped contains final keys (usually identical, except for Draft)Advanced EML Techniques
Transient Fields (in Memory Only)
" Fields that are NOT persisted to DBMODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( %control-Status ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' %control-Status = if_abap_behv=>mk-on ) ) RESULT DATA(result).
" %control controls which fields are actually updatedChecking Dynamic Feature Control
" Query features (which Actions/Fields are allowed?)READ ENTITIES OF zi_travel ENTITY Travel FIELDS ( TravelId Status ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel)
" Query features for this instance ENTITY Travel EXECUTE get_instance_features FROM VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_features).
" lt_features contains:" %features-%action-acceptTravel = fc-o-enabled / fc-o-disabled" %features-%update = fc-o-enabled / fc-o-disabled" etc.
IF lt_features[ 1 ]-%features-%action-acceptTravel = if_abap_behv=>fc-o-enabled. WRITE: / 'AcceptTravel Action is available'.ELSE. WRITE: / 'AcceptTravel Action is disabled (e.g., status already Accepted)'.ENDIF.Associations with Conditions
" Read only Bookings with Status 'Confirmed'READ ENTITIES OF zi_travel ENTITY Travel BY \_Bookings FIELDS ( BookingId BookingDate Status ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_all_bookings).
" Filter in ABAP (or better: in CDS View with WHERE)DATA(lt_confirmed) = VALUE zi_booking_table( FOR booking IN lt_all_bookings WHERE ( Status = 'C' ) ( booking )).Bulk Operations (Performance)
" Many updates in one operation (more efficient than loop)DATA(lt_updates) = VALUE zi_travel_table( FOR i = 1 UNTIL i > 1000 ( TravelId = |{ i WIDTH = 8 ALIGN = RIGHT PAD = '0' }| Status = 'A' )).
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status ) WITH CORRESPONDING #( lt_updates ) FAILED DATA(failed).
COMMIT ENTITIES.
WRITE: / |{ 1000 - lines( failed-travel ) } Travels updated|.EML in Different Contexts
In ABAP Reports
REPORT z_eml_demo.
START-OF-SELECTION. " EML can be used in any ABAP program READ ENTITIES OF zi_travel ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel).
IF lt_travel IS NOT INITIAL. WRITE: / lt_travel[ 1 ]-Description. ENDIF.In Behavior Implementations
" In zbp_i_travel (Behavior Pool)METHOD acceptTravel. " IMPORTANT: Use IN LOCAL MODE! MODIFY ENTITIES OF zi_travel IN LOCAL MODE ENTITY Travel UPDATE FIELDS ( Status ) WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = 'A' ) ) FAILED failed REPORTED reported.
" Return result READ ENTITIES OF zi_travel IN LOCAL MODE ENTITY Travel ALL FIELDS WITH CORRESPONDING #( keys ) RESULT result.ENDMETHOD.In OData Services (via RAP)
" EML is automatically executed for OData calls!" POST /sap/opu/odata4/sap/zui_travel_o4/Travel" Body: { "AgencyId": "000001", "CustomerId": "000042" }
" -> SAP internally executes:" MODIFY ENTITIES OF zi_travel" ENTITY Travel CREATE ..." COMMIT ENTITIES.In ABAP Unit Tests
METHOD test_accept_travel. " Arrange: Test data with CDS Test Double DATA(lo_env) = cl_cds_test_environment=>create( i_for_entity = 'ZI_Travel' ). lo_env->insert_test_data( VALUE zi_travel( ( TravelId = '00000001' Status = 'O' ) ) ).
" Act: Execute EML MODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE acceptTravel FROM VALUE #( ( TravelId = '00000001' ) ). COMMIT ENTITIES.
" Assert: Check status READ ENTITIES OF zi_travel ENTITY Travel FIELDS ( Status ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel).
cl_abap_unit_assert=>assert_equals( exp = 'A' act = lt_travel[ 1 ]-Status ).
" Cleanup lo_env->destroy( ).ENDMETHOD.Performance Tips
" GOOD: Bulk read with all IDs at onceREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS WITH VALUE #( FOR id IN lt_ids ( TravelId = id ) ) RESULT DATA(lt_travels)." -> 1 DB access
" BAD: Loop with individual readsLOOP AT lt_ids INTO DATA(lv_id). READ ENTITIES OF zi_travel ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelId = lv_id ) ) RESULT DATA(lt_single). APPEND LINES OF lt_single TO lt_travels.ENDLOOP." -> N DB accesses (slow!)
" GOOD: Specify fields explicitly (only what you need)READ ENTITIES OF zi_travel ENTITY Travel FIELDS ( TravelId Status ) " Only 2 fields WITH ...
" AVOID: ALL FIELDS when not necessaryREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS " Reads ALL fields + associations WITH ...
" GOOD: Read association only when neededREAD ENTITIES OF zi_travel ENTITY Travel FIELDS ( TravelId ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel)
" Conditional: Only if Status = 'O' ENTITY Travel BY \_Bookings ALL FIELDS WITH VALUE #( FOR travel IN lt_travel WHERE ( Status = 'O' ) ( TravelId = travel-TravelId ) ) RESULT DATA(lt_bookings).Important Notes / Best Practices
- EML = RAP Standard: ALWAYS use EML for RAP Business Objects (no direct
SELECT/UPDATE) - IN LOCAL MODE: Only use in Behavior Implementations (otherwise authorization checks apply!)
- Don’t forget COMMIT:
MODIFYdoes NOT write to DB - onlyCOMMIT ENTITIESdoes that - Error handling: ALWAYS evaluate
FAILEDandREPORTED - %cid for Mapping: Assign unique %cid for
CREATEfor later key mapping - Bulk instead of Loop: Performance-critical - use bulk operations
- Specify Fields:
FIELDS ( ... )is more performant thanALL FIELDS - Transactional Consistency: All
MODIFY+COMMITform one transaction (all or nothing) - Actions for Business Logic: Don’t use
UPDATEwhen an Action exists - Test Doubles: See Test Doubles & Mocking for Unit Tests with EML
- Draft Handling: For Draft-enabled BOs, there are special Draft Actions (
Edit,Activate, etc.) - Debugging: Set breakpoint in Behavior Implementation to trace EML calls
Further Resources
- RAP Basics: /en/rap-basics/
- RAP Managed vs Unmanaged: /en/rap-managed-vs-unmanaged/
- ABAP Cloud: /en/abap-cloud-definition/