EML: Entity Manipulation Language - Der komplette Guide

kategorie
ABAP-Statements
Veröffentlicht
autor
Johannes

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äftslogik
UPDATE ztravel SET status = 'A' WHERE travel_id = '00000001'.
COMMIT WORK.
" → Validierungen, Determinations, Actions NICHT ausgeführt!
" → Keine Fehlerbehandlung
" → Keine Transaktionskonsistenz

Lösung mit EML:

" ✅ EML respektiert Geschäftslogik
MODIFY 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 garantiert

EML 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 Feldliste
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.

Assoziationen lesen (BY _Association)

" Travel mit seinen Bookings lesen
READ 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 die Beziehungen, nicht die Daten
READ 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 Operations
READ 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 erstellen
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.
" Neue BookingId:
DATA(lv_booking_id) = mapped-booking[ %cid = 'BOOK_1' ]-BookingId.

UPDATE: Felder aktualisieren

" Status und Beschreibung ändern
MODIFY 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 aktualisiert

DELETE: Entities löschen

" Travel(s) löschen
MODIFY 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ühren
MODIFY 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 Template
MODIFY 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-Struktur
MODIFY 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 Action
DATA(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 schreiben
COMMIT 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 Entities
IF 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 Messages
LOOP 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 Keys
WRITE: / |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 werden
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 steuert, welche Felder wirklich geupdated werden

Dynamic 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' lesen
READ 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 einmal
READ 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 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-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ötig
READ ENTITIES OF zi_travel
ENTITY Travel ALL FIELDS " Liest ALLE Felder + Assoziationen
WITH ...
" ✅ GUT: Assoziation nur bei Bedarf lesen
READ 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: MODIFY schreibt NICHT in DB – erst COMMIT ENTITIES tut das
  • Fehlerbehandlung: IMMER FAILED und REPORTED auswerten
  • %cid für Mapping: Bei CREATE eindeutige %cid vergeben für späteres Key-Mapping
  • Bulk statt Loop: Performance-kritisch – nutzen Sie Bulk-Operationen
  • Felder explizit: FIELDS ( ... ) ist performanter als ALL FIELDS
  • Transaktionale Konsistenz: Alle MODIFY + COMMIT bilden eine Transaktion (alles oder nichts)
  • Actions für Business Logic: Nicht UPDATE nutzen, 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