Side Effects, quel est ce sortilège ?

Plongeons dans le cœur du modèle RAP d’ABAP, où ces mystérieux side effects jouent un rôle essentiel. Conçus pour optimiser l’expérience utilisateur dans les applications Fiori en mode Draft, ils permettent de synchroniser intelligemment l’interface graphique sans surcharger le backend.

1. Mais au fait, c’est quoi un « side effect » en RAP ?

Les side effects ont pour vocation de permettre a l’UI d’être rafraîchit dans le cas d’un RAP BO construit en mode DRAFT. En effet :

  • Dans le cas d’objet RAP en mode Draft, le « stateless communication pattern » est utilisé. C’est à dire que l’UI ne vient pas recharger tous les champs de l’objet une fois un des champs modifié.
  • Le gros avantage est un fonctionnement plus efficace des requêtes, car sinon il y aurait trop de requêtes, lenteurs, surcharges réseau, s’il fallait tout recharger à chaque fois.
  • Par exemple, si un des champs de l’objet RAP est modifié par le user, une requête PATCH va être effectuée vers le BO RAP, et si la requête est un succès, il y aura un retour 204 No content ( Classique pour une requête PATCH ). Cependant, imaginons que la modification de ce champs déclenche également une détermination sur un autre champs. comme il n’y a pas de READ effectue juste après, l’UI n’est pas mise a jour avec la nouvelle valeur.
  • La solution est donc le Side effect. Le side effect va permettre, dans notre cas d’exemple de préciser que lorsque ce champs en question est modifié, alors il faut aussi recharger l’autre champs.
  • Lorsque side effet est créé, les metadatas sont modifiées pour indiquer a l’UI qu’un side effect existe est qu’il faudra recalculer certains champs lors de certaines modifications.

Pour essayer de résumer la documentation, il est possible de créer des side effect pour différents cas :

  • Field : Quand un champ spécifique est modifié, cela déclenche le side effect ( exemple expliqué ci-dessus )
    side effects { field MyField affects Targets }
  • Action : Quand une action utilisateur est exécutée (ex: Calculate, CheckAvailability), cela déclenche le side effect et donc l’UI sera rechargée dans ce cas la.
    side effects { action MyAction affects Targets }
  • determine action : Quand une détermination RAP est déclenchée, cela peut également provoquer un side effect.
    side effects { determine action MyDetermineAction executed on Sources affects Targets }
  • self : Quand l’entité elle-même (n’importe quel champ) est modifiée, cela peut déclencher des rechargements d’autres entités, mais pas elle-même.
    side effects { $self affects Targets }
  • event : Quand un événement est levé côté backend, cela peut déclencher un side effect. Nous y reviendrons plus tard via un exemple concret.
    side effects { event MyEvent affects Targets }

C’est différents cas vont permettre de recharger différentes Targets , en effet, dans un side effects block, un target désigne ce que l’on souhaite recharger côté UI lorsque le side effect est déclenché.

  • field : Les champs qu’on souhaite recharger
  • permissions : Permet de recalculer les autorisations
  • $self : Recharge l’intégralité de l’entité elle-même (pas juste un champ).
  • entity : Recharge une entité associée complète via une association CDS
  • messages : Recharge les messages d’erreur / warning / info

Maintenant que les objectifs des side effects sont clairs, passons a nos cas d’exemples.

2. Cas d’étude

Tout le code est disponible sur notre Gitlab.

Pour notre cas, nous allons créer un objet RAP en se basant sur cette table :

@EndUserText.label : 'test'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zcrt_test {

  key client         : abap.clnt not null;
  key id             : sysuuid_x16 not null;
  text               : abap.char(50);
  text2              : abap.char(50);
  text3              : abap.char(50);
  status             : zstatus_test;
  criticality        : abap.int1;
  local_last_changed : abp_locinst_lastchange_tstmpl;
  createdby          : abp_creation_user;
  createdat          : abp_creation_tstmpl;
  changeby           : abp_lastchange_user;
  changeat           : abp_lastchange_tstmpl;

}

Cette table a pour objectif de stocker 3 champs textes ainsi qu’un statut dont nous baserons la valeur en fonction des différents champs texte => via une détermination dans l’objet RAP.

Puis nous utilisons le ABAP RAP Generator Objects pour générer un objet RAP Draft-Enabled.

Les règles que nous allons mettre en place :

  • Lorsque le champs text est rempli, il doit être impossible de remplir le champs text2
  • Lorsque le champs text est rempli, sa valeur doit être répliquée sur le champs text3
  • Lorsque le champs text2 est rempli, il doit être impossible de remplir le champs text
  • Enfin, a la sauvegarde de l’objet, nous souhaitons qu’un traitement via bgPF soit effectué pour déterminer le statut. Une fois ce traitement asynchrone effectué, il faut donc que l’UI soit mise.

Voici via cette vidéo, le résultat attendu.

PARTIE 1 : Gestion des champs text, text2 et text3

  • Pour gérer le fait que les champs text et text2 ne soit pas remplis en même temps, il suffit d’implémenter la méthode get_instance_features et de désactiver le champs qui ne doit pas être remplis :
METHOD get_instance_features.

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
    ENTITY zrcrttest
    ALL FIELDS
    WITH CORRESPONDING #( keys )
    RESULT DATA(result_read).

    LOOP AT keys INTO DATA(key).
      DATA(instance) = result_read[ id = key-id ].
      IF instance-Text2 IS NOT INITIAL.
        APPEND VALUE #(
        %is_draft = key-%is_draft
        id = key-Id
        %field-text = if_abap_behv=>fc-f-read_only
        ) TO result.
      ENDIF.
      IF instance-Text IS NOT INITIAL.
        APPEND VALUE #(
        %is_draft = key-%is_draft
        id = key-Id
        %field-text2 = if_abap_behv=>fc-f-read_only
        ) TO result.
      ENDIF.
    ENDLOOP.


  ENDMETHOD.

Ici, si le champs text est rempli, alors le champs text2 passe en read only via la constante if_abap_behv=>fc-f-read_only attribuée a %field-text2 du parametre RESULT.

Idem dans l’autre cas.

  • Ensuite, pour que le text3 soit identique au champs text, nous créons une détermination qui sera exécutée à la modification du champs text
  METHOD text3.

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  ALL FIELDS
  WITH CORRESPONDING #( keys )
  RESULT DATA(results_read).

    MODIFY ENTITIES OF zr_crt_test IN LOCAL MODE
ENTITY zrcrttest
UPDATE FIELDS ( text3 )
    WITH VALUE #(
        FOR update IN results_read
        ( %tky   = update-%tky
          text3 = update-text
          %control-text3 = if_abap_behv=>mk-on
) ).
  ENDMETHOD.
  • Au final, le behavior definition ressemble à ceci pour le moment
managed implementation in class ZBP_R_CRT_TEST unique;
strict ( 2 );
with draft;
define behavior for ZR_CRT_TEST alias ZrCrtTest
persistent table zcrt_test

draft table ZCRT_TEST_D
etag master LocalLastChanged
authorization master ( global )
lock master total etag ChangeAt
{

  field ( readonly, numbering:managed )
  Id;

  field ( readonly )
  LocalLastChanged,
  CreatedBy,
  CreatedAt,
  ChangeBy,
  ChangeAt,
  Text3,
  Status;

  field ( features : instance )
  Text,
  Text2;

  create;
  update;
  delete;
  determination text3 on modify { field text; }

    draft action Activate optimized;
    draft action Discard;
    draft action Edit;
    draft action Resume;
    draft determine action Prepare;

  mapping for zcrt_test corresponding
    {
      Id               = id;
      Text             = text;
      Text2            = text2;
      Text3            = text3;
      LocalLastChanged = local_last_changed;
      CreatedBy        = createdby;
      CreatedAt        = createdat;
      ChangeBy         = changeby;
      ChangeAt         = changeat;
      Status           = status;
      Criticality      = criticality;
    }

}

Cependant, nous voyons qu’il y a un problème :

En effet, dans la vidéo, il est possible de voir que les champs ne sont pas mis a jours.

Quand nous observons les appels http, nous voyons que lors de la modification des champs texte, un PATCH est envoyé avec succès

avec comme réponse 204 – No Content ( Normal en cas de succès avec un PATCH )

Le problème ici est donc que l’application Fiori n’a aucun moyen de savoir que les caractéristiques des champs text2 et text3 ont également été modifiées.

  • Maintenant, en ajoutant les side effects au behavior definition : cela fonctionne ( Cf. première vidéo )

Voici désormais le behavior definition :

managed implementation in class ZBP_R_CRT_TEST unique;
strict ( 2 );
with draft;
define behavior for ZR_CRT_TEST alias ZrCrtTest
persistent table zcrt_test

draft table ZCRT_TEST_D
etag master LocalLastChanged
authorization master ( global )
lock master total etag ChangeAt
{

  field ( readonly, numbering:managed )
  Id;

  field ( readonly )
  LocalLastChanged,
  CreatedBy,
  CreatedAt,
  ChangeBy,
  ChangeAt,
  Text3,
  Status;

  side effects
  {
    field text2 affects $self;
    field text affects $self;
  }

  field ( features : instance )
  Text,
  Text2;

  create;
  update;
  delete;
  determination text3 on modify { field text; }

    draft action Activate optimized;
    draft action Discard;
    draft action Edit;
    draft action Resume;
    draft determine action Prepare;

  mapping for zcrt_test corresponding
    {
      Id               = id;
      Text             = text;
      Text2            = text2;
      Text3            = text3;
      LocalLastChanged = local_last_changed;
      CreatedBy        = createdby;
      CreatedAt        = createdat;
      ChangeBy         = changeby;
      ChangeAt         = changeat;
      Status           = status;
      Criticality      = criticality;
    }

}

Ici en ajoutant field text2 affects $self; et field text affects $self; j’indique a l’UI qu’il faut recharger l’entité complète lorsque les champs text ou text2 sont modifiés. Cela permet de recharger les feature controls ( et donc griser les champs souhaités ) ainsi que d’updater le champs text3, puisque tous les champs sont rechargés.

Le PATCH est toujours envoyé, mais cette fois-ci, les feature controls et les champs sont rechargés via un GET effectué juste après car, via les metadatas, l’application Fiori sait que lors de cette modification, il faut re-charger l’ensemble de l’entité.

PARTIE 2 : Update de l’UI lors du calcul du champs status en arrière plan via bgPF

Notre objectif ici est de démontrer qu’il est possible d’updater l’UI, même une fois la sauvegarde de l’objet effectuée et terminée, dans le cas où des modifications en arrière plan de manière asynchrones seraient effectuée. Pour cela, nous allons utiliser les Event-Driven Side Effects.

Ces event-driven Side Effect permettent de mettre a jour de l’UI lors de processus asynchrones.

Concrètement, ici, lors de la sauvegarde de l’objet, nous allons ajouter une modification asynchrone via bgPF. Ce dernier viendra modifier la valeur du champs status en fonction des valeurs de textes ( Si au moins un champs texte est rempli, on passe le statut en non « Validated », sinon en « No Validation ». Pendant l’ensemble du temps de traitement, le champs sera laissé en « Waiting for validation » pour montrer au user qu’un calcul est en cours ). Dans le cas de cet exemple, et pour montrer que cela est utile dans le cas des traitements long, nous allons ajouter un WAIT de 4 secondes ( Comme c’est en bgPF, la modification de manière synchrone est terminée et le user peut reprendre la main même si le traitement asynchrone est encore en cours ).

  • Nous créons tout d’abord une action qui sera appelée par le bgPF via EML : action calculateStatus result [1] $self;
  METHOD calculateStatus.
    DATA : status_update TYPE TABLE FOR UPDATE zr_crt_test.

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  ALL FIELDS
  WITH CORRESPONDING #( keys )
  RESULT DATA(results_read).

    MODIFY ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  UPDATE FIELDS ( status )
      WITH VALUE #(
          FOR update IN results_read
          ( %tky   = update-%tky
            status = COND #( WHEN update-Text IS INITIAL AND update-Text2 IS INITIAL THEN 'N'
            ELSE 'V' )
            %control-Status = if_abap_behv=>mk-on
  ) ).

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  ALL FIELDS
  WITH CORRESPONDING #( keys )
  RESULT results_read.

    result = VALUE #( FOR result_read IN results_read ( %tky = result_read-%tky
                                              %param    = result_read ) ).

  ENDMETHOD.
  • Puis nous créons la classe pour le bgPF :
CLASS zbgpf_UI DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    INTERFACES if_bgmc_op_single_tx_uncontr.

    METHODS constructor
      IMPORTING i_rap_bo_entity_key TYPE sysuuid_x16.

  PROTECTED SECTION.
  PRIVATE SECTION.
    DATA uuid TYPE sysuuid_x16.
ENDCLASS.



CLASS zbgpf_UI IMPLEMENTATION.

  METHOD constructor.
    uuid = i_rap_bo_entity_key.
  ENDMETHOD.

  METHOD if_bgmc_op_single_tx_uncontr~execute.

    WAIT UP TO 4 SECONDS.

    DATA : status_update TYPE TABLE FOR UPDATE zr_crt_test.

    READ ENTITIES OF zr_crt_test
             ENTITY zrcrttest
             ALL FIELDS
             WITH VALUE #( ( %is_draft = if_abap_behv=>mk-off
                             %key-Id = uuid
                            )  )
             RESULT DATA(entities)
             FAILED DATA(failed).

    IF entities IS NOT INITIAL.

      LOOP AT entities INTO DATA(entity).

        MODIFY ENTITIES OF zr_crt_test
        ENTITY zrcrttest
        EXECUTE calculateStatus FROM VALUE #( (  %key-Id = uuid ) ).

      ENDLOOP.
    ENDIF.

    COMMIT ENTITIES.
  ENDMETHOD.

ENDCLASS.
  • Enfin, dans le behavior definition, nous ajoutons with additional save et, dans la méthode save_modified, nous venons créer le bgPF en utilisant la classe zbgpf_UI qui viendra modifier le champs status.
  • Nous effectuons cette action : RAISE ENTITY EVENT zr_crt_test~statusUpdated afin de venir informer via un event que le champs status a été modifié et l’UI sait qu’il faut recharger
managed implementation in class ZBP_R_CRT_TEST unique;
strict ( 2 );
with draft;
define behavior for ZR_CRT_TEST alias ZrCrtTest
persistent table zcrt_test
with additional save

draft table ZCRT_TEST_D
etag master LocalLastChanged
authorization master ( global )
lock master total etag ChangeAt
{

  field ( readonly, numbering:managed )
  Id;

  field ( readonly )
  LocalLastChanged,
  CreatedBy,
  CreatedAt,
  ChangeBy,
  ChangeAt,
  Text3,
  Status;

  event statusUpdated for side effects;

  side effects
  {
    field text2 affects $self;
    field text affects $self;
    event statusUpdated affects field ( Status, Criticality );
  }

  field ( features : instance )
  Text,
  Text2;

  create;
  update;
  delete;
  determination text3 on modify { field text; }
  determination criticality on modify { field Status; }
  determination status on modify { create; }
  determination statusOnSave on save { create; }
  action calculateStatusBGPF result [1] $self;
  action calculateStatus result [1] $self;
  validation validation on save { create; update; }

    draft action Activate optimized;
    draft action Discard;
    draft action Edit;
    draft action Resume;
    draft determine action Prepare
    {
        validation validation;
        determination statusOnSave;
    }

  mapping for zcrt_test corresponding
    {
      Id               = id;
      Text             = text;
      Text2            = text2;
      Text3            = text3;
      LocalLastChanged = local_last_changed;
      CreatedBy        = createdby;
      CreatedAt        = createdat;
      ChangeBy         = changeby;
      ChangeAt         = changeat;
      Status           = status;
      Criticality      = criticality;
    }

}
  METHOD save_modified.
    DATA : events_to_be_raised TYPE TABLE FOR EVENT zr_crt_test~statusUpdated.

    IF update-zrcrttest IS NOT INITIAL.
      LOOP AT update-zrcrttest ASSIGNING FIELD-SYMBOL(<update_zrcrttest>).

        "check if a process via bgpf shall be started
        IF     <update_zrcrttest>-Status          = 'W'
           AND <update_zrcrttest>-%control-Status = if_abap_behv=>mk-on.

          TRY.
              DATA(bgpf_status) = NEW zbgpf_UI( i_rap_bo_entity_key = <update_zrcrttest>-%key-id ).
              DATA background_process TYPE REF TO if_bgmc_process_single_op.
              background_process = cl_bgmc_process_factory=>get_default(  )->create(  ).
              background_process->set_operation_tx_uncontrolled( bgpf_status ).
              background_process->save_for_execution(  ).
            CATCH cx_bgmc INTO DATA(exception).
          ENDTRY.
          DATA(bgpf_update) = abap_true.
        ENDIF.

        IF   ( <update_zrcrttest>-Status = 'V' OR <update_zrcrttest>-Status = 'N' )
           AND <update_zrcrttest>-%control-Status = if_abap_behv=>mk-on.

          CLEAR events_to_be_raised.
          APPEND INITIAL LINE TO events_to_be_raised.
          events_to_be_raised[ 1 ] = CORRESPONDING #( <update_zrcrttest> ).

          RAISE ENTITY EVENT zr_crt_test~statusUpdated FROM events_to_be_raised.
        ENDIF.
      ENDLOOP.
    ENDIF.

    IF create-zrcrttest IS NOT INITIAL.
      LOOP AT create-zrcrttest ASSIGNING FIELD-SYMBOL(<create_zrcrttest>).

        "check if a process via bgpf shall be started
        IF     <create_zrcrttest>-Status          = 'W'
           AND <create_zrcrttest>-%control-Status = if_abap_behv=>mk-on.

          TRY.
              bgpf_status = NEW zbgpf_UI( i_rap_bo_entity_key = <create_zrcrttest>-%key-id ).
              background_process = cl_bgmc_process_factory=>get_default(  )->create(  ).
              background_process->set_operation_tx_uncontrolled( bgpf_status ).
              background_process->save_for_execution(  ).
            CATCH cx_bgmc INTO exception.
          ENDTRY.

        ENDIF.

      ENDLOOP.
    ENDIF.

  ENDMETHOD.

Comme le side effect est bien present dans le behavior definition : event statusUpdated affects field ( Status, Criticality ); l’UI Fiori vient créer un listener sur l’event statusUpdated, et quand il est émis suite à la modification effectuée via bgPF, l’UI effectue un nouveau GET vers le RAP BO pour récupérer les informations mises à jour ( ici, Status et Criticality ).

Conclusion

Vous savez desormais mettre à jour l’UI de vos Fiori app en utilisant le Draft. Egalement, vous avez pu voir que les traitements en arriere plan ne vous empeche pas de mettre à jour votre UI une fois ceux-ci terminer.

Enfin, il est important de noter que ce comportement de event-driven side effect, permet egalement de mettre a jour l’UI de tous les users qui seraient connectés sur le meme objet. En effet, comme un listener est crée par l’application Fiori sur cet event, tout UI en train d’afficher l’objet modifié sera notifiée et sera mise a jour !