Unmanaged Save est le scénario hybride dans RAP où le framework gère la phase d’interaction, mais vous prenez en charge la persistance vous-même. Parfait pour l’intégration de code legacy comme les BAPIs et les modules de fonction.
Le problème : Legacy rencontre Modern
Vous avez une logique métier éprouvée dans des BAPIs ou des modules de fonction, mais vous souhaitez créer des applications Fiori modernes avec RAP :
" BAPI existant pour la création de commandeCALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2" EXPORTING order_header_in = ls_header TABLES order_items_in = lt_items return = lt_return.Le dilemme :
- RAP Managed : Le framework fait INSERT/UPDATE/DELETE - mais comment appeler le BAPI ?
- RAP Unmanaged : Contrôle total, mais PAS de support Draft
La solution : Unmanaged Save - le meilleur des deux mondes.
Managed vs. Unmanaged vs. Unmanaged Save
| Aspect | Managed | Unmanaged | Unmanaged Save |
|---|---|---|---|
| Phase d’interaction | Framework | Vous-même | Framework |
| Buffer transient | Géré par le framework | Vous-même | Géré par le framework |
| Phase Save | Framework | Vous-même | Vous-même |
| Support Draft | Oui | Non | Oui |
| Intégration Legacy | Difficile | Oui | Oui |
| Utilisation typique | Greenfield | Entièrement Custom | Intégration Legacy |
Activer Unmanaged Save
Behavior Definition
managed implementation in class zbp_i_salesorder unique;strict ( 2 );with draft;
" Important : Activer Unmanaged Savewith unmanaged save;
define behavior for ZI_SalesOrder alias SalesOrderpersistent table zsalesorderdraft table zd_salesorderlock master total etag LastChangedAtauthorization master ( instance ){ create; update; delete;
field ( readonly ) SalesOrderId; field ( numbering : managed ) SalesOrderId;
validation validateCustomer on save { field CustomerId; } determination calcTotalPrice on modify { field Quantity, UnitPrice; }
action confirmOrder result [1] $self;
draft action Edit; draft action Activate optimized; draft action Discard; draft action Resume;
association _Items { create; with draft; }}
define behavior for ZI_SalesOrderItem alias Itempersistent table zsalesorderitemdraft table zd_salesorderitemlock dependent by _SalesOrderauthorization dependent by _SalesOrder{ update; delete;
field ( readonly ) SalesOrderId, ItemId; field ( numbering : managed ) ItemId;
association _SalesOrder { with draft; }}Mot-clé : with unmanaged save; active le handler Save manuel.
Behavior Implementation : Classe Saver
CLASS lsc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_saver. PROTECTED SECTION. METHODS: " DOIT être implémenté avec unmanaged save save_modified REDEFINITION,
" Optionnel : Nettoyage après COMMIT/ROLLBACK cleanup_finalize REDEFINITION.ENDCLASS.
CLASS lsc_salesorder IMPLEMENTATION.
METHOD save_modified. " Ici vous appelez vos APIs legacy ! " Le framework a géré les données dans le buffer transient, " maintenant vous devez les persister.
" CREATE : Créer de nouvelles entités IF create-salesorder IS NOT INITIAL. LOOP AT create-salesorder INTO DATA(ls_create). me->call_bapi_create( ls_create ). ENDLOOP. ENDIF.
" UPDATE : Modifier des entités existantes IF update-salesorder IS NOT INITIAL. LOOP AT update-salesorder INTO DATA(ls_update). me->call_bapi_update( ls_update ). ENDLOOP. ENDIF.
" DELETE : Supprimer des entités IF delete-salesorder IS NOT INITIAL. LOOP AT delete-salesorder INTO DATA(ls_delete). me->call_bapi_delete( ls_delete ). ENDLOOP. ENDIF.
" De même pour les entités enfants (Items) IF create-item IS NOT INITIAL. me->call_bapi_create_items( create-item ). ENDIF.
IF update-item IS NOT INITIAL. me->call_bapi_update_items( update-item ). ENDIF.
IF delete-item IS NOT INITIAL. me->call_bapi_delete_items( delete-item ). ENDIF. ENDMETHOD.
METHOD cleanup_finalize. " Nettoyer après COMMIT ou ROLLBACK " ex. supprimer des données temporaires, libérer des verrous ENDMETHOD.
ENDCLASS.Intégration BAPI en détail
Create : Créer de nouvelles commandes
CLASS lsc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_saver. PRIVATE SECTION. METHODS call_bapi_create IMPORTING is_order TYPE zsalesorder.ENDCLASS.
CLASS lsc_salesorder IMPLEMENTATION.
METHOD call_bapi_create. DATA: ls_header TYPE bapisdhead, lt_items TYPE TABLE OF bapisditem, lt_return TYPE bapiret2_t, lv_vbeln TYPE vbeln.
" 1. Mapping : Structure RAP -> Structure BAPI ls_header = VALUE #( doc_type = 'TA" sales_org = is_order-SalesOrg distr_chan = is_order-DistrChannel division = is_order-Division sold_to = is_order-CustomerId purch_no = is_order-PurchaseOrderNo doc_date = is_order-OrderDate ).
" 2. Préparer les items (si présents) " Note : Pour Deep Insert, les items sont passés séparément " Ici omis à titre d'exemple
" 3. Appeler le BAPI CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2" EXPORTING order_header_in = ls_header IMPORTING salesdocument = lv_vbeln TABLES return = lt_return.
" 4. Vérification des erreurs LOOP AT lt_return ASSIGNING FIELD-SYMBOL(<return>) WHERE type CA 'EA'. " Reprendre l'erreur dans la structure RAP reported APPEND VALUE #( %tky = is_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = <return>-message ) ) TO reported-salesorder.
" Abandonner la sauvegarde RETURN. ENDLOOP.
" 5. Commit Work (standard BAPI) CALL FUNCTION 'BAPI_TRANSACTION_COMMIT" EXPORTING wait = abap_true.
" 6. Remapper l'ID généré (optionnel, si managed numbering) " Avec managed numbering, le framework a déjà attribué l'ID ENDMETHOD.
ENDCLASS.Update : Modifier des commandes existantes
METHOD call_bapi_update. DATA: ls_header_in TYPE bapisdh1, ls_header_inx TYPE bapisdh1x, lt_return TYPE bapiret2_t.
" 1. Préparer les données d'en-tête ls_header_in-purch_no = is_order-PurchaseOrderNo.
" 2. Évaluer %control : Quels champs ont été modifiés ? ls_header_inx-updateflag = 'U'.
IF is_order-%control-PurchaseOrderNo = if_abap_behv=>mk-on. ls_header_inx-purch_no = abap_true. ENDIF.
IF is_order-%control-OrderDate = if_abap_behv=>mk-on. ls_header_in-doc_date = is_order-OrderDate. ls_header_inx-doc_date = abap_true. ENDIF.
" Autres champs de manière analogue...
" 3. Appeler le BAPI CALL FUNCTION 'BAPI_SALESORDER_CHANGE" EXPORTING salesdocument = CONV #( is_order-SalesOrderId ) order_header_in = ls_header_in order_header_inx = ls_header_inx TABLES return = lt_return.
" 4. Vérification des erreurs (comme pour Create) LOOP AT lt_return ASSIGNING FIELD-SYMBOL(<return>) WHERE type CA 'EA'. APPEND VALUE #( %tky = is_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = <return>-message ) ) TO reported-salesorder. RETURN. ENDLOOP.
" 5. Commit CALL FUNCTION 'BAPI_TRANSACTION_COMMIT" EXPORTING wait = abap_true.ENDMETHOD.Delete : Supprimer des commandes
METHOD call_bapi_delete. DATA: lt_return TYPE bapiret2_t.
" Pour certains objets : Annulation au lieu de suppression " Ici exemple avec BAPI Reject
CALL FUNCTION 'BAPI_SALESORDER_CHANGE" EXPORTING salesdocument = CONV #( is_order-SalesOrderId ) order_header_in = VALUE bapisdh1( ref_doc = 'DELETED' ) order_header_inx = VALUE bapisdh1x( updateflag = 'D' ) TABLES return = lt_return.
" Alternative : Suppression directe (si BAPI existe) " CALL FUNCTION 'BAPI_SALESORDER_DELETE' ...
" Vérification des erreurs LOOP AT lt_return ASSIGNING FIELD-SYMBOL(<return>) WHERE type CA 'EA'. APPEND VALUE #( %tky = is_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = <return>-message ) ) TO reported-salesorder. RETURN. ENDLOOP.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT" EXPORTING wait = abap_true.ENDMETHOD.Verrouillage avec Unmanaged Save
Verrou Framework vs. Verrou Legacy
Avec Unmanaged Save, le framework gère les verrous pendant la phase d’interaction. Dans la phase Save, vous devez éventuellement prendre en compte des verrous legacy supplémentaires.
CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS lock FOR LOCK IMPORTING keys FOR LOCK SalesOrder.ENDCLASS.
CLASS lhc_salesorder IMPLEMENTATION.
METHOD lock. " Le verrou RAP est géré par le framework " En plus : Verrouiller le système legacy (si nécessaire)
LOOP AT keys INTO DATA(ls_key). " ENQUEUE pour les tables legacy CALL FUNCTION 'ENQUEUE_EVVBAKE" EXPORTING mode_vbak = 'E" mandt = sy-mandt vbeln = CONV #( ls_key-SalesOrderId ) EXCEPTIONS foreign_lock = 1 OTHERS = 2.
IF sy-subrc <> 0. APPEND VALUE #( %tky = ls_key-%tky %fail-cause = if_abap_behv=>cause-locked ) TO failed-salesorder.
APPEND VALUE #( %tky = ls_key-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = |Commande { ls_key-SalesOrderId } est verrouillée| ) ) TO reported-salesorder. ENDIF. ENDLOOP. ENDMETHOD.
ENDCLASS.Libération des verrous dans cleanup_finalize
METHOD cleanup_finalize. " Après COMMIT/ROLLBACK : Libérer les verrous
" Libérer tous les verrous legacy détenus CALL FUNCTION 'DEQUEUE_ALL'.
" Ou spécifiquement : " CALL FUNCTION 'DEQUEUE_EVVBAKE' ...ENDMETHOD.Contrôle transactionnel
Transaction RAP vs. Transaction BAPI
┌─────────────────────────────────────────────────────────┐│ Transaction RAP ││ ││ ┌──────────────────┐ ┌──────────────────┐ ││ │ Phase interaction │ │ Phase Save │ ││ │ │ │ │ ││ │ - Modify Entity │ │ save_modified: │ ││ │ - Validations │ │ ┌─────────────┐ │ ││ │ - Determinations │ │ │ BAPI Call 1 │ │ ││ │ - Actions │ │ │ COMMIT WORK │ │ ││ │ │ │ └─────────────┘ │ ││ │ (Transient) │ │ ┌─────────────┐ │ ││ │ │ │ │ BAPI Call 2 │ │ ││ │ │ │ │ COMMIT WORK │ │ ││ │ │ │ └─────────────┘ │ ││ └──────────────────┘ └──────────────────┘ ││ ││ COMMIT ENTITIES (après save_modified) │└─────────────────────────────────────────────────────────┘Règles importantes
- COMMIT WORK dans save_modified : Chaque BAPI a besoin de son COMMIT WORK
- Pas de ROLLBACK WORK : En cas d’erreur, remplir RAP reported, le framework fait le Rollback
- Respecter l’ordre : Parent avant enfants (pour les Deep Operations)
METHOD save_modified. " 1. D'abord les entités parentes IF create-salesorder IS NOT INITIAL. LOOP AT create-salesorder INTO DATA(ls_order). me->call_bapi_create_order( ls_order ). ENDLOOP. ENDIF.
" 2. Puis les entités enfants IF create-item IS NOT INITIAL. LOOP AT create-item INTO DATA(ls_item). me->call_bapi_create_item( ls_item ). ENDLOOP. ENDIF.
" 3. Les updates peuvent être parallèles IF update-salesorder IS NOT INITIAL. " ... ENDIF.
" 4. Les deletes en dernier (enfants avant parent) IF delete-item IS NOT INITIAL. " Supprimer d'abord les items ENDIF.
IF delete-salesorder IS NOT INITIAL. " Puis supprimer la commande ENDIF.ENDMETHOD.Gestion des erreurs
Reprendre les erreurs BAPI dans RAP
METHOD handle_bapi_return. " Méthode utilitaire : BAPI-RETURN -> RAP-reported
LOOP AT it_return ASSIGNING FIELD-SYMBOL(<return>). CASE <return>-type. WHEN 'E' OR 'A'. " Error / Abort APPEND VALUE #( %tky = is_key %msg = new_message( id = <return>-id number = <return>-number severity = if_abap_behv_message=>severity-error v1 = <return>-message_v1 v2 = <return>-message_v2 v3 = <return>-message_v3 v4 = <return>-message_v4 ) ) TO reported-salesorder.
" Flag : Transaction échouée rv_has_error = abap_true.
WHEN 'W'. " Warning APPEND VALUE #( %tky = is_key %msg = new_message( id = <return>-id number = <return>-number severity = if_abap_behv_message=>severity-warning v1 = <return>-message_v1 v2 = <return>-message_v2 v3 = <return>-message_v3 v4 = <return>-message_v4 ) ) TO reported-salesorder.
WHEN 'I' OR 'S'. " Info / Success " Optionnel : Logger les messages de succès ENDCASE. ENDLOOP.ENDMETHOD.Rollback en cas d’erreur
METHOD save_modified. DATA: lv_error_occurred TYPE abap_bool.
" Traiter tous les Creates IF create-salesorder IS NOT INITIAL. LOOP AT create-salesorder INTO DATA(ls_create). me->call_bapi_create( EXPORTING is_order = ls_create IMPORTING ev_error = lv_error_occurred ).
IF lv_error_occurred = abap_true. " En cas d'erreur : Pas d'autres opérations " Le framework fait le ROLLBACK RETURN. ENDIF. ENDLOOP. ENDIF.
" Autres opérations seulement si pas d'erreur...ENDMETHOD.Attribution de numéros avec intégration legacy
Scénario 1 : RAP attribue le numéro (Recommandé)
" Dans BDEF :field ( numbering : managed ) SalesOrderId;
" Dans save_modified : Passer le numéro RAP au BAPIMETHOD call_bapi_create. " RAP a déjà attribué SalesOrderId ls_header-doc_number = is_order-SalesOrderId.
CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2" EXPORTING order_header_in = ls_header " ...ENDMETHOD.Scénario 2 : Legacy attribue le numéro
" Dans BDEF :field ( readonly ) SalesOrderId;" PAS de managed numbering !
" Dans Handler : Implémenter early numberingCLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS get_next_number FOR NUMBERING IMPORTING entities FOR CREATE SalesOrder.ENDCLASS.
CLASS lhc_salesorder IMPLEMENTATION.
METHOD get_next_number. " Obtenir le numéro du système legacy LOOP AT entities ASSIGNING FIELD-SYMBOL(<entity>). " Appeler le numéroteur TRY. <entity>-SalesOrderId = cl_numberrange_runtime=>number_get( nr_range_nr = '01" object = 'SD_VBELN" ).
APPEND VALUE #( %cid = <entity>-%cid SalesOrderId = <entity>-SalesOrderId ) TO mapped-salesorder.
CATCH cx_number_ranges INTO DATA(lx_nr). APPEND VALUE #( %cid = <entity>-%cid %fail-cause = if_abap_behv=>cause-unspecific ) TO failed-salesorder. ENDTRY. ENDLOOP. ENDMETHOD.
ENDCLASS.Exemple complet : Gestion des commandes
CDS View
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Sales Order"
define root view entity ZI_SalesOrder as select from zsalesorder composition [0..*] of ZI_SalesOrderItem as _Items{ key sales_order_id as SalesOrderId, customer_id as CustomerId, sales_org as SalesOrg, distr_channel as DistrChannel, division as Division, order_date as OrderDate, purchase_order_no as PurchaseOrderNo, @Semantics.amount.currencyCode: 'CurrencyCode" total_amount as TotalAmount, @Semantics.currencyCode: true currency_code as CurrencyCode, status as Status, @Semantics.user.createdBy: true created_by as CreatedBy, @Semantics.systemDateTime.createdAt: true created_at as CreatedAt, @Semantics.user.lastChangedBy: true last_changed_by as LastChangedBy, @Semantics.systemDateTime.lastChangedAt: true last_changed_at as LastChangedAt,
_Items}Behavior Definition
managed implementation in class zbp_i_salesorder unique;strict ( 2 );with draft;with unmanaged save;
define behavior for ZI_SalesOrder alias SalesOrderpersistent table zsalesorderdraft table zd_salesorderlock master total etag LastChangedAtauthorization master ( instance ){ create; update; delete;
field ( readonly ) SalesOrderId, CreatedBy, CreatedAt, LastChangedBy, LastChangedAt; field ( numbering : managed ) SalesOrderId; field ( readonly : update ) CustomerId, SalesOrg;
validation validateCustomer on save { field CustomerId; } determination setDefaults on modify { create; } determination calcTotal on modify { field TotalAmount; }
action ( features : instance ) confirmOrder result [1] $self;
draft action Edit; draft action Activate optimized; draft action Discard; draft action Resume; draft determine action Prepare;
association _Items { create; with draft; }
mapping for zsalesorder corresponding { SalesOrderId = sales_order_id; CustomerId = customer_id; SalesOrg = sales_org; DistrChannel = distr_channel; Division = division; OrderDate = order_date; PurchaseOrderNo = purchase_order_no; TotalAmount = total_amount; CurrencyCode = currency_code; Status = status; CreatedBy = created_by; CreatedAt = created_at; LastChangedBy = last_changed_by; LastChangedAt = last_changed_at; }}
define behavior for ZI_SalesOrderItem alias Itempersistent table zsalesorderitemdraft table zd_salesorderitemlock dependent by _SalesOrderauthorization dependent by _SalesOrder{ update; delete;
field ( readonly ) SalesOrderId, ItemId; field ( numbering : managed ) ItemId;
determination calcItemAmount on modify { field Quantity, UnitPrice; }
association _SalesOrder { with draft; }
mapping for zsalesorderitem corresponding { SalesOrderId = sales_order_id; ItemId = item_id; ProductId = product_id; Quantity = quantity; UnitPrice = unit_price; ItemAmount = item_amount; CurrencyCode = currency_code; }}Bonnes pratiques
1. Utiliser des classes Wrapper
Encapsulez les appels BAPI dans des wrappers réutilisables :
" Wrapper au lieu d'appel BAPI directDATA(lo_order_api) = NEW zcl_salesorder_wrapper( ).
TRY. lo_order_api->create_order( EXPORTING is_order = ls_order_data IMPORTING ev_order_id = lv_order_id ). CATCH zcx_order_error INTO DATA(lx_error). " Gestion des erreursENDTRY.2. Toujours évaluer %control
" Passer UNIQUEMENT les champs modifiés au BAPIIF is_entity-%control-Description = if_abap_behv=>mk-on. ls_bapi_data-description = is_entity-Description. ls_bapi_datax-description = abap_true.ENDIF.3. Respecter les limites de transaction
" COMMIT WORK après chaque BAPICALL FUNCTION 'BAPI_..."
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT" EXPORTING wait = abap_true.
" PAS : Accumuler plusieurs BAPIs et commiter à la fin4. Standardiser les messages d’erreur
" Classe d'exception unifiée pour toutes les erreurs legacyCLASS zcx_legacy_error DEFINITION INHERITING FROM cx_static_check. " Avec messages T100 pour le multilinguismeENDCLASS.5. Implémenter le logging
METHOD save_modified. " Business Application Logging pour la traçabilité DATA(lo_log) = cl_bali_log=>create_with_header( header = cl_bali_header_setter=>create( object = 'ZSALES" subobject = 'SAVE" ) ).
" Logger les succès et les erreurs lo_log->add_item( cl_bali_message_setter=>create_from_sy( ) ). lo_log->save( ).ENDMETHOD.Quand utiliser Unmanaged Save ?
| Scénario | Recommandation |
|---|---|
| Nouvelle application sans legacy | Non - Managed (sans unmanaged save) |
| Intégration BAPI requise | Oui - Unmanaged Save |
| Fonctionnalité Draft + Legacy | Oui - Unmanaged Save |
| Transactions complexes | Oui - Unmanaged Save |
| Migration d’application Dynpro | Oui - Unmanaged Save |
| Contrôle total, pas de Draft | Non - Unmanaged (complet) |
Ressources supplémentaires
- Managed vs. Unmanaged : RAP Managed vs. Unmanaged
- Classes Wrapper : Classes Wrapper dans ABAP Cloud
- Fondamentaux EML : Entity Manipulation Language
- RAP Basics : Fondamentaux RAP
- Draft Handling : RAP Draft Handling