EML: Entity Manipulation Language - The Complete Guide

Category
ABAP-Statements
Published
Author
Johannes

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 logic
UPDATE ztravel SET status = 'A' WHERE travel_id = '00000001'.
COMMIT WORK.
" -> Validations, Determinations, Actions are NOT executed!
" -> No error handling
" -> No transactional consistency

Solution with EML:

" GOOD: EML respects business logic
MODIFY 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 guaranteed

EML 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 list
READ 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 Bookings
READ 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|.
" Only the relationships, not the data
READ 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 operations
READ 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 travel
MODIFY 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 description
MODIFY 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 updated

DELETE: 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 travel
MODIFY 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 template
MODIFY 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 structure
MODIFY 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 action
DATA(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 here
COMMIT 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 entities
IF 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 messages
LOOP 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 keys
WRITE: / |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 DB
MODIFY 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 updated

Checking 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 once
READ 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 reads
LOOP 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 necessary
READ ENTITIES OF zi_travel
ENTITY Travel ALL FIELDS " Reads ALL fields + associations
WITH ...
" GOOD: Read association only when needed
READ 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: MODIFY does NOT write to DB - only COMMIT ENTITIES does that
  • Error handling: ALWAYS evaluate FAILED and REPORTED
  • %cid for Mapping: Assign unique %cid for CREATE for later key mapping
  • Bulk instead of Loop: Performance-critical - use bulk operations
  • Specify Fields: FIELDS ( ... ) is more performant than ALL FIELDS
  • Transactional Consistency: All MODIFY + COMMIT form one transaction (all or nothing)
  • Actions for Business Logic: Don’t use UPDATE when 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