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étierUPDATE 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 transactionnelleSolution avec EML :
" ✅ EML respecte la logique métierMODIFY 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 garantieStructure 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 individuelleREAD 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 BookingsREAD 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|.Lire uniquement les liens (ASSOCIATION LINKS)
" Uniquement les relations, pas les donnéesREAD 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éesREAD 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 existantMODIFY 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 DescriptionMODIFY 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 à jourDELETE : 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 TravelMODIFY 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èleMODIFY 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ètresMODIFY 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'actionDATA(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 iciCOMMIT 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 MessagesLOOP 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éesWRITE: / |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 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 contrôle quels champs sont réellement mis à jourVé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 foisREAD 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 individuelsLOOP 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écessaireREAD ENTITIES OF zi_travel ENTITY Travel ALL FIELDS " Lit TOUS les champs + Associations WITH ...
" ✅ BON : Lire l'association uniquement si nécessaireREAD 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/UPDATEdirect) - IN LOCAL MODE : Uniquement dans les Behavior Implementations (sinon vérification des autorisations !)
- Ne pas oublier COMMIT :
MODIFYn’écrit PAS en DB – seulementCOMMIT ENTITIESle fait - Gestion des erreurs : TOUJOURS évaluer
FAILEDetREPORTED - %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 queALL FIELDS - Cohérence transactionnelle : Tous les
MODIFY+COMMITforment une transaction (tout ou rien) - Actions pour Business Logic : Ne pas utiliser
UPDATEsi 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
- RAP Basics : /rap-basics/
- RAP Managed vs Unmanaged : /rap-managed-vs-unmanaged/
- ABAP Cloud : /abap-cloud-definition/