RAP Unmanaged Save - Intégration avec du code legacy

Catégorie
RAP
Publié
Auteur
Johannes

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 commande
CALL 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

AspectManagedUnmanagedUnmanaged Save
Phase d’interactionFrameworkVous-mêmeFramework
Buffer transientGéré par le frameworkVous-mêmeGéré par le framework
Phase SaveFrameworkVous-mêmeVous-même
Support DraftOuiNonOui
Intégration LegacyDifficileOuiOui
Utilisation typiqueGreenfieldEntièrement CustomIntégration Legacy

Activer Unmanaged Save

Behavior Definition

managed implementation in class zbp_i_salesorder unique;
strict ( 2 );
with draft;
" Important : Activer Unmanaged Save
with unmanaged save;
define behavior for ZI_SalesOrder alias SalesOrder
persistent table zsalesorder
draft table zd_salesorder
lock master total etag LastChangedAt
authorization 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 Item
persistent table zsalesorderitem
draft table zd_salesorderitem
lock dependent by _SalesOrder
authorization 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

  1. COMMIT WORK dans save_modified : Chaque BAPI a besoin de son COMMIT WORK
  2. Pas de ROLLBACK WORK : En cas d’erreur, remplir RAP reported, le framework fait le Rollback
  3. 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 BAPI
METHOD 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 numbering
CLASS 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 SalesOrder
persistent table zsalesorder
draft table zd_salesorder
lock master total etag LastChangedAt
authorization 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 Item
persistent table zsalesorderitem
draft table zd_salesorderitem
lock dependent by _SalesOrder
authorization 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 direct
DATA(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 erreurs
ENDTRY.

2. Toujours évaluer %control

" Passer UNIQUEMENT les champs modifiés au BAPI
IF 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 BAPI
CALL FUNCTION 'BAPI_..."
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT"
EXPORTING wait = abap_true.
" PAS : Accumuler plusieurs BAPIs et commiter à la fin

4. Standardiser les messages d’erreur

" Classe d'exception unifiée pour toutes les erreurs legacy
CLASS zcx_legacy_error DEFINITION
INHERITING FROM cx_static_check.
" Avec messages T100 pour le multilinguisme
ENDCLASS.

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énarioRecommandation
Nouvelle application sans legacyNon - Managed (sans unmanaged save)
Intégration BAPI requiseOui - Unmanaged Save
Fonctionnalité Draft + LegacyOui - Unmanaged Save
Transactions complexesOui - Unmanaged Save
Migration d’application DynproOui - Unmanaged Save
Contrôle total, pas de DraftNon - Unmanaged (complet)

Ressources supplémentaires