EML : Entity Manipulation Language - Le guide complet

Catégorie
ABAP-Statements
Publié
Auteur
Johannes

EML (Entity Manipulation Language) est le langage spécialisé en ABAP pour interagir avec les RAP Business Objects. Au lieu d’accès directs à la base de données (SELECT, UPDATE, DELETE), vous utilisez EML pour des opérations transactionnelles type-safe avec une logique métier complète.

Pourquoi EML ?

Problème avec l’ABAP classique :

" ❌ Les accès directs à la DB contournent la logique métier
UPDATE ztravel SET status = 'A' WHERE travel_id = '00000001'.
COMMIT WORK.
" → Validations, Determinations, Actions NON exécutées !
" → Pas de gestion des erreurs
" → Pas de cohérence transactionnelle

Solution avec EML :

" ✅ EML respecte la logique métier
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE FIELDS ( Status )
WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) )
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
" → Toutes les Validations/Determinations du BDEF sont exécutées
" → Gestion structurée des erreurs via FAILED/REPORTED
" → Cohérence transactionnelle garantie

Structure de base EML

Toutes les opérations EML suivent ce pattern :

<OPERATION> ENTITIES OF <root_entity>
ENTITY <entity_alias>
<OPERATION_DETAILS>
[MAPPED DATA(mapped)] " Nouvelles clés après Create
[FAILED DATA(failed)] " Informations sur les erreurs
[REPORTED DATA(reported)]. " Messages pour l'UI
COMMIT ENTITIES
[RESPONSE OF <root_entity>
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported)].

READ ENTITIES : Lire les données

Syntaxe de base

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).

Lire tous les champs

" ALL FIELDS au lieu d'une liste de champs individuelle
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.

Lire les associations (BY _Association)

" Lire Travel avec ses Bookings
READ ENTITIES OF zi_travel
ENTITY Travel
ALL FIELDS
WITH VALUE #( ( TravelId = '00000001' ) )
RESULT DATA(lt_travel)
" Naviguer via l'association
ENTITY Travel BY \_Bookings
ALL FIELDS
WITH VALUE #( ( TravelId = '00000001' ) )
RESULT DATA(lt_bookings).
" Maintenant nous avons :
" - lt_travel : Le voyage lui-même
" - lt_bookings : Toutes les réservations de ce voyage
WRITE: / |Voyage { lt_travel[ 1 ]-TravelId } a { lines( lt_bookings ) } réservations|.
" Uniquement les relations, pas les données
READ ENTITIES OF zi_travel
ENTITY Travel BY \_Bookings
FROM VALUE #( ( TravelId = '00000001' ) )
LINK DATA(lt_links).
" lt_links contient uniquement les clés :
" source (TravelId) → target (TravelId + BookingId)

IN LOCAL MODE (sans vérification des autorisations)

" Pour les Behavior Implementations ou opérations privilégiées
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
ALL FIELDS
WITH VALUE #( ( TravelId = '00000001' ) )
RESULT DATA(lt_travel).
" → Pas de vérifications d'autorisation
" → Utilisez ceci UNIQUEMENT dans les Behavior Implementations !

MODIFY ENTITIES : Modifier les données

CREATE : Créer de nouvelles entities

MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate Description )
WITH VALUE #(
( %cid = 'CID_1' " Client-ID pour le mapping
AgencyId = '000001"
CustomerId = '000042"
BeginDate = cl_abap_context_info=>get_system_date( )
EndDate = cl_abap_context_info=>get_system_date( ) + 14
Description = 'Voyage d'affaires Munich' )
( %cid = 'CID_2"
AgencyId = '000002"
CustomerId = '000099"
BeginDate = '20250601"
EndDate = '20250615"
Description = 'Vacances Majorque' )
)
MAPPED DATA(mapped) " Contient les TravelIds générés
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
" Récupérer le nouveau TravelId via mapping %cid :
IF sy-subrc = 0.
DATA(lv_new_travel_id) = mapped-travel[ %cid = 'CID_1' ]-TravelId.
WRITE: / |Nouveau voyage créé : { lv_new_travel_id }|.
ENDIF.

CREATE BY : Créer via association

" Créer Booking pour un Travel existant
MODIFY ENTITIES OF zi_travel
ENTITY Travel CREATE BY \_Bookings
FIELDS ( BookingDate CustomerId CarrierId FlightPrice )
WITH VALUE #(
( TravelId = '00000001' " Clé parent
%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.
" Nouveau BookingId :
DATA(lv_booking_id) = mapped-booking[ %cid = 'BOOK_1' ]-BookingId.

UPDATE : Mettre à jour les champs

" Modifier Status et Description
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE FIELDS ( Status Description )
WITH VALUE #(
( TravelId = '00000001"
Status = 'A' " Accepté
Description = 'Approuvé le ' && sy-datum )
( TravelId = '00000002"
Status = 'X' " Rejeté
Description = 'Rejeté' )
)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
" Gestion des erreurs :
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: / 'Erreur:', lv_text.
ENDLOOP.
ENDIF.

UPDATE SET FIELDS (tous les champs non-initial)

" UPDATE sans FIELDS explicite → tous les champs sont écrasés !
DATA(ls_update) = VALUE zi_travel(
TravelId = '00000001"
Status = 'A"
Description = 'Nouvelle description"
" BeginDate, EndDate etc. restent inchangés (non spécifiés)
).
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE SET FIELDS WITH VALUE #( ( CORRESPONDING #( ls_update ) ) )
FAILED DATA(failed).
" → Seulement Status et Description sont mis à jour

DELETE : Supprimer des entities

" Supprimer Travel(s)
MODIFY ENTITIES OF zi_travel
ENTITY Travel
DELETE FROM VALUE #(
( TravelId = '00000042' )
( TravelId = '00000043' )
)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
" Vérifier si réussi :
IF line_exists( failed-travel[ TravelId = '00000042' ] ).
WRITE: / 'Suppression échouée pour 00000042'.
ELSE.
WRITE: / 'Supprimé avec succès : 00000042'.
ENDIF.

EXECUTE : Exécuter des actions

Action d’instance

" Exécuter l'action 'acceptTravel' pour un Travel
MODIFY ENTITIES OF zi_travel
ENTITY Travel
EXECUTE acceptTravel FROM VALUE #(
( TravelId = '00000001' )
( TravelId = '00000002' )
)
RESULT DATA(result) " Si l'action renvoie un résultat
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
" result contient les données Travel mises à jour (si l'action a `result [1] $self`)
LOOP AT result INTO DATA(ls_result).
WRITE: / |Travel { ls_result-TravelId } Status : { ls_result-Status }|.
ENDLOOP.

Action statique

" Action statique (sans instance)
MODIFY ENTITIES OF zi_travel
ENTITY Travel
EXECUTE createDefaultTravel
RESULT DATA(result)
MAPPED DATA(mapped).
COMMIT ENTITIES.
" Nouveau Travel créé :
DATA(lv_new_id) = mapped-travel[ 1 ]-TravelId.

Factory Action

" Factory Action : Crée une nouvelle instance basée sur un modèle
MODIFY ENTITIES OF zi_travel
ENTITY Travel
EXECUTE copyTravel FROM VALUE #(
( TravelId = '00000001' " Modèle
%param = VALUE #( Description = 'Copie du voyage 1' )
)
)
MAPPED DATA(mapped).
COMMIT ENTITIES.
" Nouveau Travel copié :
DATA(lv_copy_id) = mapped-travel[ 1 ]-TravelId.

Action avec paramètres

" Action avec structure de paramètres
MODIFY ENTITIES OF zi_travel
ENTITY Travel
EXECUTE setDiscount FROM VALUE #(
( TravelId = '00000001"
%param-Percentage = 10 " 10% de réduction
%param-Reason = 'Remise fidélité"
)
)
RESULT DATA(result).
COMMIT ENTITIES.
" result-%param contient les valeurs de retour de l'action
DATA(lv_new_price) = result[ 1 ]-%param-NewTotalPrice.

COMMIT ENTITIES : Finaliser la transaction

Commit simple

MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE FIELDS ( Status )
WITH VALUE #( ( TravelId = '00000001' Status = 'A' ) ).
" Écrire les modifications dans la DB uniquement ici
COMMIT ENTITIES.
IF sy-subrc = 0.
WRITE: / 'Commité avec succès'.
ENDIF.

Commit avec gestion des erreurs

MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate )
WITH VALUE #( ( %cid = 'CID_1' AgencyId = '999999' ) ).
" → AgencyId n'existe pas → La validation échoue
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
IF commit_failed-travel IS NOT INITIAL.
WRITE: / 'Commit échoué !'.
LOOP AT commit_reported-travel INTO DATA(ls_msg).
WRITE: / ls_msg-%msg->if_message~get_text( ).
ENDLOOP.
" La transaction a été automatiquement annulée !
ENDIF.

COMMIT avec RESPONSE et 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) " Clés finales après Commit
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
IF sy-subrc = 0.
" commit_mapped écrase mapped avec les clés DB finales
DATA(lv_final_id) = commit_mapped-travel[ %cid = 'CID_1' ]-TravelId.
WRITE: / |ID Travel finale : { lv_final_id }|.
ENDIF.

Gestion des erreurs avec FAILED & REPORTED

FAILED : Quelles entities ont échoué ?

MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE FIELDS ( Status )
WITH VALUE #(
( TravelId = '00000001' Status = 'A' )
( TravelId = '99999999' Status = 'A' ) " N'existe pas
( TravelId = '00000003' Status = 'A' )
)
FAILED DATA(failed).
COMMIT ENTITIES.
" failed-travel contient les clés des entities qui ont échoué
IF line_exists( failed-travel[ TravelId = '99999999' ] ).
WRITE: / 'Travel 99999999 n'a pas pu être mis à jour'.
" %fail-cause indique la raison :
DATA(ls_failed) = failed-travel[ TravelId = '99999999' ].
CASE ls_failed-%fail-cause.
WHEN if_abap_behv=>cause-not_found.
WRITE: / 'Entity non trouvée'.
WHEN if_abap_behv=>cause-unauthorized.
WRITE: / '→ Pas d'autorisation'.
WHEN if_abap_behv=>cause-unspecific.
WRITE: / '→ Voir REPORTED pour les détails'.
ENDCASE.
ENDIF.

REPORTED : Messages d’erreur pour l’UI

MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE FIELDS ( BeginDate EndDate )
WITH VALUE #( ( TravelId = '00000001"
BeginDate = '20250615"
EndDate = '20250601' ) ) " End avant Begin !
REPORTED DATA(reported).
COMMIT ENTITIES.
" reported-travel contient les Messages
LOOP AT reported-travel INTO DATA(ls_report).
" %msg est du type REF TO if_abap_behv_message
DATA(lo_msg) = ls_report-%msg.
" Récupérer le texte :
DATA(lv_text) = lo_msg->if_message~get_text( ).
WRITE: / lv_text.
" Vérifier la sévérité :
DATA(lv_severity) = lo_msg->if_abap_behv_message~m_severity.
IF lv_severity = if_abap_behv_message=>severity-error.
WRITE: / '→ Erreur !'.
ENDIF.
" Champs affectés :
IF ls_report-%element-EndDate = if_abap_behv=>mk-on.
WRITE: / '→ Problème avec le champ EndDate'.
ENDIF.
ENDLOOP.

MAPPED : Nouvelles clés après 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).
" Après CREATE : mapped contient les clés générées
WRITE: / |Alpha Travel ID : { mapped-travel[ %cid = 'CID_ALPHA' ]-TravelId }|.
WRITE: / |Beta Travel ID : { mapped-travel[ %cid = 'CID_BETA' ]-TravelId }|.
" Après COMMIT : commit_mapped contient les clés finales (généralement identiques, sauf en Draft)

Techniques EML avancées

Champs transitoires (uniquement en mémoire)

" Champs qui ne sont PAS persistés en 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 contrôle quels champs sont réellement mis à jour

Vérifier Dynamic Feature Control

" Interroger les features (quelles Actions/Fields sont autorisés ?)
READ ENTITIES OF zi_travel
ENTITY Travel
FIELDS ( TravelId Status )
WITH VALUE #( ( TravelId = '00000001' ) )
RESULT DATA(lt_travel)
" Interroger les features pour cette instance
ENTITY Travel
EXECUTE get_instance_features
FROM VALUE #( ( TravelId = '00000001' ) )
RESULT DATA(lt_features).
" lt_features contient :
" %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: / 'Action AcceptTravel est disponible'.
ELSE.
WRITE: / 'Action AcceptTravel est désactivée (par ex. Status déjà Accepté)'.
ENDIF.

Associations avec conditions

" Lire uniquement les Bookings avec Status 'Confirmed"
READ ENTITIES OF zi_travel
ENTITY Travel BY \_Bookings
FIELDS ( BookingId BookingDate Status )
WITH VALUE #( ( TravelId = '00000001' ) )
RESULT DATA(lt_all_bookings).
" Filtrer en ABAP (ou mieux : dans la CDS View avec WHERE)
DATA(lt_confirmed) = VALUE zi_booking_table(
FOR booking IN lt_all_bookings WHERE ( Status = 'C' ) ( booking )
).

Opérations en masse (Performance)

" Beaucoup d'updates en une opération (plus efficace qu'une boucle)
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 mis à jour|.

EML dans différents contextes

Dans les rapports ABAP

REPORT z_eml_demo.
START-OF-SELECTION.
" EML peut être utilisé dans n'importe quel programme ABAP
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.

Dans les Behavior Implementations

" Dans zbp_i_travel (Behavior Pool)
METHOD acceptTravel.
" IMPORTANT : Utiliser 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.
" Retourner le résultat
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Travel
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT result.
ENDMETHOD.

Dans les services OData (via RAP)

" EML est exécuté automatiquement lors des appels OData !
" POST /sap/opu/odata4/sap/zui_travel_o4/Travel
" Body : { "AgencyId": "000001", "CustomerId": "000042" }
" → SAP exécute en interne :
" MODIFY ENTITIES OF zi_travel
" ENTITY Travel CREATE ...
" COMMIT ENTITIES.

Dans les tests unitaires ABAP

METHOD test_accept_travel.
" Arrange : Données de test avec 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 : Exécuter EML
MODIFY ENTITIES OF zi_travel
ENTITY Travel
EXECUTE acceptTravel FROM VALUE #( ( TravelId = '00000001' ) ).
COMMIT ENTITIES.
" Assert : Vérifier le 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.

Conseils de performance

" ✅ BON : Bulk-Read avec tous les IDs en une fois
READ ENTITIES OF zi_travel
ENTITY Travel ALL FIELDS
WITH VALUE #( FOR id IN lt_ids ( TravelId = id ) )
RESULT DATA(lt_travels).
" → 1 accès DB
" ❌ MAUVAIS : Loop avec des Reads individuels
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 accès DB (lent !)
" ✅ BON : Spécifier explicitement les champs (uniquement ce dont vous avez besoin)
READ ENTITIES OF zi_travel
ENTITY Travel FIELDS ( TravelId Status ) " Seulement 2 champs
WITH ...
" ❌ À ÉVITER : ALL FIELDS si non nécessaire
READ ENTITIES OF zi_travel
ENTITY Travel ALL FIELDS " Lit TOUS les champs + Associations
WITH ...
" ✅ BON : Lire l'association uniquement si nécessaire
READ ENTITIES OF zi_travel
ENTITY Travel FIELDS ( TravelId )
WITH VALUE #( ( TravelId = '00000001' ) )
RESULT DATA(lt_travel)
" Conditionnel : Seulement si 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).

Remarques importantes / Bonnes pratiques

  • EML = Standard RAP : Utilisez TOUJOURS EML pour les RAP Business Objects (pas de SELECT/UPDATE direct)
  • IN LOCAL MODE : Uniquement dans les Behavior Implementations (sinon vérification des autorisations !)
  • Ne pas oublier COMMIT : MODIFY n’écrit PAS en DB – seulement COMMIT ENTITIES le fait
  • Gestion des erreurs : TOUJOURS évaluer FAILED et REPORTED
  • %cid pour Mapping : Lors de CREATE, attribuer un %cid unique pour le mapping ultérieur des clés
  • Masse au lieu de Loop : Performance critique – utiliser les opérations en masse
  • Champs explicites : FIELDS ( ... ) est plus performant que ALL FIELDS
  • Cohérence transactionnelle : Tous les MODIFY + COMMIT forment une transaction (tout ou rien)
  • Actions pour Business Logic : Ne pas utiliser UPDATE si une Action existe
  • Test Doubles : Voir Test Doubles & Mocking pour les tests unitaires avec EML
  • Gestion Draft : Pour les BOs Draft-enabled, il existe des Actions Draft spéciales (Edit, Activate, etc.)
  • Débogage : Définir un breakpoint dans Behavior Implementation pour tracer l’appel EML

Ressources complémentaires