EML (Entity Manipulation Language) ist die spezialisierte Sprache in ABAP, um mit RAP Business Objects zu interagieren. Statt direkter Datenbank-Zugriffe (SELECT, UPDATE, DELETE) nutzen Sie EML für typsichere, transaktionale Operationen mit voller Geschäftslogik.
Warum EML?
Problem mit klassischem ABAP:
" ❌ Direkte DB-Zugriffe umgehen GeschäftslogikUPDATE ztravel SET status = 'A' WHERE travel_id = '00000001'.COMMIT WORK." → Validierungen, Determinations, Actions NICHT ausgeführt!" → Keine Fehlerbehandlung" → Keine TransaktionskonsistenzLösung mit EML:
" ✅ EML respektiert GeschäftslogikMODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) ) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES." → Alle Validations/Determinations aus BDEF werden ausgeführt" → Strukturierte Fehlerbehandlung via FAILED/REPORTED" → Transaktionale Konsistenz garantiertEML Grundstruktur
Alle EML-Operationen folgen diesem Pattern:
<OPERATION> ENTITIES OF <root_entity> ENTITY <entity_alias> <OPERATION_DETAILS> [MAPPED DATA(mapped)] " Neue Keys nach Create [FAILED DATA(failed)] " Fehler-Informationen [REPORTED DATA(reported)]. " Messages für UI
COMMIT ENTITIES [RESPONSE OF <root_entity> FAILED DATA(commit_failed) REPORTED DATA(commit_reported)].READ ENTITIES: Daten lesen
Grundsyntax
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).Alle Felder lesen
" ALL FIELDS statt einzelner FeldlisteREAD 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.Assoziationen lesen (BY _Association)
" Travel mit seinen Bookings lesenREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel)
" Über Assoziation navigieren ENTITY Travel BY \_Bookings ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_bookings).
" Jetzt haben wir:" - lt_travel: Die Reise selbst" - lt_bookings: Alle Buchungen dieser Reise
WRITE: / |Reise { lt_travel[ 1 ]-TravelId } hat { lines( lt_bookings ) } Buchungen|.Nur Links lesen (ASSOCIATION LINKS)
" Nur die Beziehungen, nicht die DatenREAD ENTITIES OF zi_travel ENTITY Travel BY \_Bookings FROM VALUE #( ( TravelId = '00000001' ) ) LINK DATA(lt_links).
" lt_links enthält nur Keys:" source (TravelId) → target (TravelId + BookingId)IN LOCAL MODE (ohne Berechtigungsprüfung)
" Für Behavior Implementations oder privilegierte OperationsREAD ENTITIES OF zi_travel IN LOCAL MODE ENTITY Travel ALL FIELDS WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel).
" → Keine Authorization Checks" → Nutzen Sie dies NUR in Behavior Implementations!MODIFY ENTITIES: Daten ändern
CREATE: Neue Entities erstellen
MODIFY ENTITIES OF zi_travel ENTITY Travel CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate Description ) WITH VALUE #( ( %cid = 'CID_1' " Client-ID für Mapping AgencyId = '000001' CustomerId = '000042' BeginDate = cl_abap_context_info=>get_system_date( ) EndDate = cl_abap_context_info=>get_system_date( ) + 14 Description = 'Geschäftsreise München' )
( %cid = 'CID_2' AgencyId = '000002' CustomerId = '000099' BeginDate = '20250601' EndDate = '20250615' Description = 'Urlaub Mallorca' ) ) MAPPED DATA(mapped) " Enthält generierte TravelIds FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES RESPONSE OF zi_travel FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
" Neue TravelId abrufen via %cid Mapping:IF sy-subrc = 0. DATA(lv_new_travel_id) = mapped-travel[ %cid = 'CID_1' ]-TravelId. WRITE: / |Neue Reise erstellt: { lv_new_travel_id }|.ENDIF.CREATE BY: Via Assoziation erstellen
" Booking für existierende Travel erstellenMODIFY 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.
" Neue BookingId:DATA(lv_booking_id) = mapped-booking[ %cid = 'BOOK_1' ]-BookingId.UPDATE: Felder aktualisieren
" Status und Beschreibung ändernMODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status Description ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' " Accepted Description = 'Genehmigt am ' && sy-datum )
( TravelId = '00000002' Status = 'X' " Rejected Description = 'Abgelehnt' ) ) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES.
" Fehlerbehandlung: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: / 'Fehler:', lv_text. ENDLOOP.ENDIF.UPDATE SET FIELDS (alle nicht-initial)
" UPDATE ohne explizite FIELDS → alle Felder werden überschrieben!DATA(ls_update) = VALUE zi_travel( TravelId = '00000001' Status = 'A' Description = 'Neue Beschreibung' " BeginDate, EndDate etc. bleiben unverändert (nicht angegeben)).
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE SET FIELDS WITH VALUE #( ( CORRESPONDING #( ls_update ) ) ) FAILED DATA(failed).
" → Nur Status und Description werden aktualisiertDELETE: Entities löschen
" Travel(s) löschenMODIFY ENTITIES OF zi_travel ENTITY Travel DELETE FROM VALUE #( ( TravelId = '00000042' ) ( TravelId = '00000043' ) ) FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES.
" Prüfen, ob erfolgreich:IF line_exists( failed-travel[ TravelId = '00000042' ] ). WRITE: / 'Löschen fehlgeschlagen für 00000042'.ELSE. WRITE: / 'Erfolgreich gelöscht: 00000042'.ENDIF.EXECUTE: Actions ausführen
Instanz-Action
" Action 'acceptTravel' für eine Travel ausführenMODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE acceptTravel FROM VALUE #( ( TravelId = '00000001' ) ( TravelId = '00000002' ) ) RESULT DATA(result) " Falls Action ein Ergebnis zurückgibt FAILED DATA(failed) REPORTED DATA(reported).
COMMIT ENTITIES.
" result enthält aktualisierte Travel-Daten (wenn Action `result [1] $self` hat)LOOP AT result INTO DATA(ls_result). WRITE: / |Travel { ls_result-TravelId } Status: { ls_result-Status }|.ENDLOOP.Statische Action
" Statische Action (ohne Instanz)MODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE createDefaultTravel RESULT DATA(result) MAPPED DATA(mapped).
COMMIT ENTITIES.
" Neue Travel wurde erstellt:DATA(lv_new_id) = mapped-travel[ 1 ]-TravelId.Factory Action
" Factory Action: Erstellt neue Instanz basierend auf TemplateMODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE copyTravel FROM VALUE #( ( TravelId = '00000001' " Template %param = VALUE #( Description = 'Kopie von Reise 1' ) ) ) MAPPED DATA(mapped).
COMMIT ENTITIES.
" Neue kopierte Travel:DATA(lv_copy_id) = mapped-travel[ 1 ]-TravelId.Action mit Parametern
" Action mit Parameter-StrukturMODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE setDiscount FROM VALUE #( ( TravelId = '00000001' %param-Percentage = 10 " 10% Rabatt %param-Reason = 'Treuerabatt' ) ) RESULT DATA(result).
COMMIT ENTITIES.
" result-%param enthält Rückgabewerte der ActionDATA(lv_new_price) = result[ 1 ]-%param-NewTotalPrice.COMMIT ENTITIES: Transaktion abschließen
Einfaches Commit
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) ).
" Änderungen erst hier in die DB schreibenCOMMIT ENTITIES.
IF sy-subrc = 0. WRITE: / 'Erfolgreich committed'.ENDIF.Commit mit Fehlerbehandlung
MODIFY ENTITIES OF zi_travel ENTITY Travel CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate ) WITH VALUE #( ( %cid = 'CID_1' AgencyId = '999999' ) )." → AgencyId existiert nicht → Validation schlägt fehl
COMMIT ENTITIES RESPONSE OF zi_travel FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
IF commit_failed-travel IS NOT INITIAL. WRITE: / 'Commit fehlgeschlagen!'. LOOP AT commit_reported-travel INTO DATA(ls_msg). WRITE: / ls_msg-%msg->if_message~get_text( ). ENDLOOP.
" Transaktion wurde automatisch zurückgerollt!ENDIF.COMMIT mit RESPONSE und 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) " Finale Keys nach Commit FAILED DATA(commit_failed) REPORTED DATA(commit_reported).
IF sy-subrc = 0. " commit_mapped überschreibt mapped mit finalen DB-Keys DATA(lv_final_id) = commit_mapped-travel[ %cid = 'CID_1' ]-TravelId. WRITE: / |Finale Travel ID: { lv_final_id }|.ENDIF.Fehlerbehandlung mit FAILED & REPORTED
FAILED: Welche Entities fehlgeschlagen?
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( Status ) WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) ( TravelId = '99999999' Status = 'A' ) " Existiert nicht ( TravelId = '00000003' Status = 'A' ) ) FAILED DATA(failed).
COMMIT ENTITIES.
" failed-travel enthält Keys der fehlgeschlagenen EntitiesIF line_exists( failed-travel[ TravelId = '99999999' ] ). WRITE: / 'Travel 99999999 konnte nicht aktualisiert werden'.
" %fail-cause gibt Grund an: DATA(ls_failed) = failed-travel[ TravelId = '99999999' ]. CASE ls_failed-%fail-cause. WHEN if_abap_behv=>cause-not_found. WRITE: / '→ Entity nicht gefunden'. WHEN if_abap_behv=>cause-unauthorized. WRITE: / '→ Keine Berechtigung'. WHEN if_abap_behv=>cause-unspecific. WRITE: / '→ Siehe REPORTED für Details'. ENDCASE.ENDIF.REPORTED: Fehlermeldungen für UI
MODIFY ENTITIES OF zi_travel ENTITY Travel UPDATE FIELDS ( BeginDate EndDate ) WITH VALUE #( ( TravelId = '00000001' BeginDate = '20250615' EndDate = '20250601' ) ) " End vor Begin! REPORTED DATA(reported).
COMMIT ENTITIES.
" reported-travel enthält MessagesLOOP AT reported-travel INTO DATA(ls_report). " %msg ist vom Typ REF TO if_abap_behv_message DATA(lo_msg) = ls_report-%msg.
" Text abrufen: DATA(lv_text) = lo_msg->if_message~get_text( ). WRITE: / lv_text.
" Severity prüfen: DATA(lv_severity) = lo_msg->if_abap_behv_message~m_severity. IF lv_severity = if_abap_behv_message=>severity-error. WRITE: / '→ Fehler!'. ENDIF.
" Betroffene Felder: IF ls_report-%element-EndDate = if_abap_behv=>mk-on. WRITE: / '→ Problem mit Feld EndDate'. ENDIF.ENDLOOP.MAPPED: Neue Keys nach 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).
" Nach CREATE: mapped enthält generierte KeysWRITE: / |Alpha Travel ID: { mapped-travel[ %cid = 'CID_ALPHA' ]-TravelId }|.WRITE: / |Beta Travel ID: { mapped-travel[ %cid = 'CID_BETA' ]-TravelId }|.
" Nach COMMIT: commit_mapped enthält finale Keys (meist identisch, außer bei Draft)Erweiterte EML-Techniken
Transient Fields (nur in Memory)
" Felder, die NICHT in DB persistiert werdenMODIFY 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 steuert, welche Felder wirklich geupdated werdenDynamic Feature Control prüfen
" Features abfragen (welche Actions/Fields sind erlaubt?)READ ENTITIES OF zi_travel ENTITY Travel FIELDS ( TravelId Status ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel)
" Features für diese Instanz abfragen ENTITY Travel EXECUTE get_instance_features FROM VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_features).
" lt_features enthält:" %features-%action-acceptTravel = fc-o-enabled / fc-o-disabled" %features-%update = fc-o-enabled / fc-o-disabled" usw.
IF lt_features[ 1 ]-%features-%action-acceptTravel = if_abap_behv=>fc-o-enabled. WRITE: / 'AcceptTravel Action ist verfügbar'.ELSE. WRITE: / 'AcceptTravel Action ist deaktiviert (z.B. Status schon Accepted)'.ENDIF.Assoziationen mit Bedingungen
" Nur Bookings mit Status 'Confirmed' lesenREAD ENTITIES OF zi_travel ENTITY Travel BY \_Bookings FIELDS ( BookingId BookingDate Status ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_all_bookings).
" Filtern in ABAP (oder besser: in CDS View mit WHERE)DATA(lt_confirmed) = VALUE zi_booking_table( FOR booking IN lt_all_bookings WHERE ( Status = 'C' ) ( booking )).Bulk Operations (Performance)
" Viele Updates in einer Operation (effizienter als 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 aktualisiert|.EML in verschiedenen Kontexten
In ABAP Reports
REPORT z_eml_demo.
START-OF-SELECTION. " EML kann in jedem ABAP-Programm verwendet werden 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. " WICHTIG: IN LOCAL MODE verwenden! 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.
" Ergebnis zurückgeben READ ENTITIES OF zi_travel IN LOCAL MODE ENTITY Travel ALL FIELDS WITH CORRESPONDING #( keys ) RESULT result.ENDMETHOD.In OData-Services (via RAP)
" EML wird automatisch ausgeführt bei OData-Aufrufen!" POST /sap/opu/odata4/sap/zui_travel_o4/Travel" Body: { "AgencyId": "000001", "CustomerId": "000042" }
" → SAP führt intern aus:" MODIFY ENTITIES OF zi_travel" ENTITY Travel CREATE ..." COMMIT ENTITIES.In ABAP Unit Tests
METHOD test_accept_travel. " Arrange: Test-Daten mit 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: EML ausführen MODIFY ENTITIES OF zi_travel ENTITY Travel EXECUTE acceptTravel FROM VALUE #( ( TravelId = '00000001' ) ). COMMIT ENTITIES.
" Assert: Status geprüft 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-Tipps
" ✅ GUT: Bulk-Read mit allen IDs auf einmalREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS WITH VALUE #( FOR id IN lt_ids ( TravelId = id ) ) RESULT DATA(lt_travels)." → 1 DB-Zugriff
" ❌ SCHLECHT: Loop mit einzelnen 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-Zugriffe (langsam!)
" ✅ GUT: Felder explizit angeben (nur was Sie brauchen)READ ENTITIES OF zi_travel ENTITY Travel FIELDS ( TravelId Status ) " Nur 2 Felder WITH ...
" ❌ VERMEIDEN: ALL FIELDS wenn nicht nötigREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS " Liest ALLE Felder + Assoziationen WITH ...
" ✅ GUT: Assoziation nur bei Bedarf lesenREAD ENTITIES OF zi_travel ENTITY Travel FIELDS ( TravelId ) WITH VALUE #( ( TravelId = '00000001' ) ) RESULT DATA(lt_travel)
" Conditional: Nur wenn 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).Wichtige Hinweise / Best Practice
- EML = RAP-Standard: Nutzen Sie IMMER EML für RAP Business Objects (kein direktes
SELECT/UPDATE) - IN LOCAL MODE: Nur in Behavior Implementations verwenden (sonst Berechtigungsprüfung!)
- COMMIT nicht vergessen:
MODIFYschreibt NICHT in DB – erstCOMMIT ENTITIEStut das - Fehlerbehandlung: IMMER
FAILEDundREPORTEDauswerten - %cid für Mapping: Bei
CREATEeindeutige %cid vergeben für späteres Key-Mapping - Bulk statt Loop: Performance-kritisch – nutzen Sie Bulk-Operationen
- Felder explizit:
FIELDS ( ... )ist performanter alsALL FIELDS - Transaktionale Konsistenz: Alle
MODIFY+COMMITbilden eine Transaktion (alles oder nichts) - Actions für Business Logic: Nicht
UPDATEnutzen, wenn eine Action existiert - Test Doubles: Siehe Test Doubles & Mocking für Unit Tests mit EML
- Draft-Handling: Bei Draft-enabled BOs gibt es spezielle Draft-Actions (
Edit,Activate, etc.) - Debuggen: Breakpoint in Behavior Implementation setzen, um EML-Aufruf zu tracen
Weitere Ressourcen
- RAP Basics: /rap-basics/
- RAP Managed vs Unmanaged: /rap-managed-vs-unmanaged/
- ABAP Cloud: /abap-cloud-definition/