Skip to Content
Technical Articles
Author's profile photo Ramjee Korada

ABAP Restful Application Programming : Enabling draft feature to the custom implemented Actions

Introduction:

Enabling Draft is most common feature in current projects irrespective of Managed/Unmanaged scenario in fiori applications.

In short, lets see what the draft is.

Draft-enabled applications allow the end user to store changed data in the backend and continue at a later point in time or from a different device, even if the application terminates unexpectedly. This kind of scenario needs to support a stateless communication and requires a replacement for the temporary in-memory version of the business entity that is created or edited. This temporary version is kept on a separate database table and is known as draft data. Drafts are isolated in their own persistence and do not influence existing business logic until activated.                                                          

Problem statement:

Standard RAP framework takes care of creation/modification of draft records for all standard operations (CREATE / UPDATE) but it is developer’s responsibility to implement draft for all custom actions in the applications. In this blog post, we will see how we can implement draft for custom actions.

Challenge:

When we use “MODIFY ENTITIES”, it will update the records of the current instance and commit to the database .We need them to be updated in draft records but not actual records.

Solution:

Before we use “MODIFY ENTITIES”, we need to check if the current instance is active then we need to create Draft instance for it. This can be achieved by executing “EDIT” on active instance. An “EDIT”  action creates a new draft document automatically by copying the corresponding active instance data to the draft table. Immediately EDIT triggers an exclusive lock for the active instance. This lock is maintained until the durable lock phase of the draft ends, which is either when the draft is activated, or when the durable lock expires after a certain time.

This EDIT action has a parameter “preserve_changes” whose default value is false and system overwrites the draft instance if already exists, but we must make sure not to lose the draft information. Hence, we need to fill ‘true” to the parameter “preserve_changes”.

While defining the Action, the key point to make sure to return the entity but not $self. After draft instance is created, we must send the draft instance as output while the active instance is input to the action.

Implementation steps:

( Focus of the blog post is from Step #7  and if you are familiar with basic steps then skip until step #6 )

  1. Create a table with underlying fields
    @EndUserText.label : 'Purchase contract'
    @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
    @AbapCatalog.tableCategory : #TRANSPARENT
    @AbapCatalog.deliveryClass : #A
    @AbapCatalog.dataMaintenance : #RESTRICTED
    define table zrk_t_pur_con {
      key client           : abap.clnt not null;
      key con_uuid         : sysuuid_x16 not null;
      object_id            : zrk_pur_con_id;
      description          : zrk_description;
      buyer                : zrk_buyer_id;
      supplier             : zrk_sup_no;
      sup_con_id           : zrk_sup_con_id;
      comp_code            : zrk_company_code;
      stat_code            : zrk_stat_code;
      fiscl_year           : zrk_fiscal_year;
      valid_from           : zrk_valid_from;
      valid_to             : zrk_valid_to;
      created_by           : abp_creation_user;
      created_at           : abp_creation_tstmpl;
      last_changed_by      : abp_locinst_lastchange_user;
      last_changed_at      : abp_lastchange_tstmpl;
      locl_last_changed_at : abp_locinst_lastchange_tstmpl;
    
    }​
  2. Create an interface view for data modeling
    @AccessControl.authorizationCheck: #CHECK
    @EndUserText.label: 'ZRK_I_PUR_CON_UD'
    define root view entity ZRK_I_PUR_CON_UD as select from zrk_t_pur_con
    
    {
        key con_uuid as ConUuid,
        object_id as ObjectId,
        description as Description,
        buyer as Buyer,
        supplier as Supplier,
        sup_con_id as SupConId,
        comp_code as CompCode,
        stat_code as StatCode,
        fiscl_year as FisclYear,
        valid_from as ValidFrom,
        valid_to as ValidTo,
        created_by as CreatedBy,
        created_at as CreatedAt,
        last_changed_by as LastChangedBy,
        last_changed_at as LastChangedAt,
        locl_last_changed_at as LoclLastChangedAt
    }
    ​
  3. Create a projection view to expose in the UI service
    @AccessControl.authorizationCheck: #CHECK
    @EndUserText.label: 'Project for unmanaged draft'
    @Metadata.allowExtensions: true
    define root view entity ZRK_C_PUR_CON_UD 
    provider contract transactional_query
    as projection on ZRK_I_PUR_CON_UD
    {
        key ConUuid,
        ObjectId,
        Description,
        Buyer,
        Supplier,
        SupConId,
        CompCode,
        StatCode,
        FisclYear,
        ValidFrom,
        ValidTo,
        CreatedBy,
        CreatedAt,
        LastChangedBy,
        LastChangedAt,
        LoclLastChangedAt
        
    }
    ​
  4. Enrich UI with metadata extension
    @Metadata.layer: #CORE
    @UI: {
      headerInfo: {
        typeName: 'Purchase Contract',
        typeNamePlural: 'Purchase Contracts',
        description: {
        type: #STANDARD,
        value: 'Description'
            },
            title: {
    //    type: #STANDARD,
        value: 'ObjectId'
            }
            
        }
    }
    annotate entity ZRK_C_PUR_CON_UD
        with 
    {
     
      @UI.facet: [ {
              id: 'Header',
              type: #HEADERINFO_REFERENCE,
              label: 'Header',
              purpose: #HEADER,
              position: 10,
              targetQualifier: 'Header'
      },
      {     id: 'General',
            type: #IDENTIFICATION_REFERENCE,
            purpose: #STANDARD,
            label: 'General',
            position: 20 ,
            targetQualifier: 'General'},
    
      {     id: 'Validities',
            type: #IDENTIFICATION_REFERENCE,
            label: 'Validities',
            position: 30 ,
            targetQualifier: 'Validities'}
    
       ]
    
      @UI.hidden: true
      @UI.lineItem: [{
          position: 10 ,
          type: #FOR_ACTION,
          label: 'Forward',
          dataAction: 'Forward'
      }]
        @UI.identification: [{
          position: 10 ,
          type: #FOR_ACTION,
          label: 'Forward',
          dataAction: 'Forward'
      }]
      ConUuid;
    
      @UI:{ lineItem: [{ position: 10 }] , identification: [{ position: 10 , qualifier: 'General'}]}
      @UI.selectionField: [{ position: 10 }]
      ObjectId;
      
      @UI.selectionField: [{ position: 20 }]
      @UI:{ lineItem: [{ position: 20 }] , identification: [{ position: 20 , qualifier: 'General'}]}
      Description;
      
      @UI.selectionField: [{ position: 30 }]
      @UI:{ lineItem: [{ position: 30 }] , identification: [{ position: 30 , qualifier: 'General'}]}
      @Consumption.valueHelpDefinition: [{ 
          entity: {
              name: 'ZRK_I_BUYER',
              element: 'BuyerId'
          }
       }]  
      Buyer;
      
      @UI.selectionField: [{ position: 40 }]
      @UI:{ lineItem: [{ position: 40 }] , identification: [{ position: 40 ,qualifier: 'General'}]}
      @Consumption.valueHelpDefinition: [{ entity: {
          name: 'ZRK_I_SUPPLIER',
          element: 'SupNo'
      } ,
            useForValidation: true
      }]  
      Supplier;
      
      @UI:{ lineItem: [{ position: 50 }] , identification: [{ position: 50 ,qualifier: 'General'}]}
      @Consumption.valueHelpDefinition: [{ entity: {
          name: 'ZRK_I_SUP_CON',
          element: 'SupConId'
      } ,
      additionalBinding: [{
          localElement: 'Supplier',
          localConstant: '',
          element: 'SupNo',
          usage: #FILTER_AND_RESULT 
      }] , 
            useForValidation: true
      }]   
      SupConId;
      
      @UI.selectionField: [{ position: 50 }]
      @UI:{ lineItem: [{ position: 50 }] , identification: [{ position: 55 ,qualifier: 'General'}]}
      @Consumption.valueHelpDefinition: [{ entity: {
          name: 'ZRK_I_COMP_CODE',
          element: 'CompCode'
      } ,
            useForValidation: true
      }]   
      CompCode;  
      
      @UI:{ lineItem: [{ position: 60 }] , identification: [{ position: 60 ,qualifier: 'Header'}]}
      StatCode;
      
      @UI:{ lineItem: [{ position: 70 }] , identification: [{ position: 70 , qualifier: 'Validities' }]}
      ValidFrom;
      
      @UI:{ lineItem: [{ position: 80 }] , identification: [{ position: 80 , qualifier: 'Validities' }]}
      ValidTo;
      
      @UI:{ lineItem: [{ position: 80 }] , identification: [{ position: 90 , qualifier: 'Validities' }]}
      @Consumption.valueHelpDefinition: [{ 
          entity: {
              name: 'ZRK_I_FISCAL_YEAR',
              element: 'fiscal_year'
          }
       }]   
      FisclYear;
      
      @UI:{ lineItem: [{ position: 90 }] , identification: [{ position: 100, qualifier: 'General' , label: 'Created By' }]}
      CreatedBy;
      
      @UI.hidden: true
      CreatedAt;
      @UI.hidden: true
      LastChangedBy;
      @UI.hidden: true
      LastChangedAt;
      @UI.hidden: true
      LoclLastChangedAt;
    
    }​
  5. Create a behavior definition “with Draft ”
    unmanaged implementation in class zbp_rk_i_pur_con_ud unique;
    with draft;
    
    define behavior for ZRK_I_PUR_CON_UD alias PurCon
    //late numbering
    draft table zrk_dt_pur_con_u
    lock master total etag LoclLastChangedAt
    authorization master ( instance )
    etag master LoclLastChangedAt
    {
    
      field ( numbering : managed ) ConUuid;
      field ( readonly ) ObjectId , CreatedBy;
      create;
      update;
      delete;
    
      //draft action Edit;
    
      determination set_pc_num on modify { create; }
    
    }​
  6. Then create implementation class and apply your logic for basic operations.
  7. Define custom action “Forward”
    Input parameter : Buyer to select from F4 help ( for more details on input for actions, please refer blog )
    Result parameter : As explained above, we have to return the entity but not $self.

      action Forward parameter ZRK_I_FWD_BUYER result [1] ZRK_I_PUR_CON_UD ;​
  8. Its time to implement “Action” and refer to below snippet
    • Get the user input to be updated into local variable
    • Prepare the draft instance for all active instances from list of records that user selected for Action by executing EDIT
    • Copy “keys” into local table and modify the property “%is_draft” to “if_abap_behv=>mk-on” so that further processing happens on draft instances but not active instances anymore.
    • Then READ the entities with latest instances and MODIFY the entities to reflect the changes on draft instances
    • Pass the result back to UI with draft instance information.
        METHOD Forward.
      
      */.. Get new buyer information
          READ TABLE keys ASSIGNING FIELD-SYMBOL(<fs_key>) INDEX 1.
          IF sy-subrc EQ 0.
            DATA(lv_new_buyer) = <fs_key>-%param-Buyer.
          ENDIF.
      
      */..Create a draft instance for all active instance
      */.. There could be multiple records mixed with draft/active when multi-select is enabled.
      
          MODIFY ENTITIES OF zrk_i_pur_con_ud IN LOCAL MODE
          ENTITY PurCon
          EXECUTE edit FROM
          VALUE #( FOR <fs_active_key> IN keys WHERE ( %is_draft = if_abap_behv=>mk-off )
                                                  ( %key = <fs_active_key>-%key
                                                    %param-preserve_changes = 'X'
                                                  ) )
                REPORTED DATA(edit_reported)
                FAILED DATA(edit_failed)
                MAPPED DATA(edit_mapped).
      
          DATA(lt_temp_keys) = keys.
          LOOP AT lt_temp_keys ASSIGNING FIELD-SYMBOL(<fs_temp_keys>).
              <fs_temp_keys>-%is_draft = if_abap_behv=>mk-on.
          ENDLOOP.
      
      */.. Read the existing Data
          READ ENTITIES OF zrk_i_pur_con_ud IN LOCAL MODE
          ENTITY PurCon
          FIELDS ( Buyer )
          WITH CORRESPONDING #( lt_temp_keys )
          RESULT DATA(lt_buyer).
      
      */.. Then modify the draft instance but not active instance
          MODIFY ENTITIES OF zrk_i_pur_con_ud IN LOCAL MODE
          ENTITY PurCon
          UPDATE FIELDS ( Buyer )
          WITH VALUE #( FOR <fs_rec_draft> IN lt_buyer ( %tky = <fs_rec_draft>-%tky
                                                   %is_draft = '01'
                                                   Buyer = lv_new_buyer ) )
                                         REPORTED edit_reported
                                         FAILED edit_failed
                                         MAPPED DATA(lt_updated).
      
      */.. Read the data to send back to UI. / Optional - This is to check if the values are updated ?
          READ ENTITIES OF zrk_i_pur_con_ud IN LOCAL MODE
          ENTITY PurCon
          ALL FIELDS
          WITH CORRESPONDING #( lt_temp_keys )
          RESULT DATA(lt_buyer_updated).
      
      */.. Pass the data to UI.
          result = CORRESPONDING #( lt_buyer_updated ).
      
        ENDMETHOD.​
  9. Project the behavior definition, Define the “Service Definition” and generate “Service Binding”.
    projection;
    use draft;
    
    
    define behavior for ZRK_C_PUR_CON_UD alias PurCon
    {
    use create;
    use update;
    use delete;
    
    use action Forward ;
    
    }​
  10. Preview the application to test.
    Scenario #1 : Take an example of active instance ( PC1 ). As we see, Draft is created and buyer details are updated after “Action” is triggered.Active%20instance%20before%20ActionDraft%20after%20Action
    Scenario #2 :Take an example of draft instance ( PC2 ) , the existing Draft itself is updated with buyer details after “Action” triggered

 

Conclusion:

we have seen and understood how to create draft instances from active instances and update the details on draft but not actual instance. So that end user can review it and decide to Save / Discard them in object page.

Assigned Tags

      11 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Andre Fischer
      Andre Fischer

      Hi Ramjee,

      thank you very much for your blog post.

      It helped me with an issue I had with a draft enabled application where I use an instanance based action to create additional child items.

      Only after having performed the following chang

      //action ( features : instance ) addChild2 parameter /DMO/I_RAP_GEN_PARAM_ADD_CHILD result [1] $self;
      action ( features : instance ) addChild2 parameter /DMO/I_RAP_GEN_PARAM_ADD_CHILD result [1] /DMO/I_RAPGENERATORBONODE;

      I am able to refresh my object page without getting the following error.

      Unable to load the data.Error:

      Number of specified key properties does not match number of key properties of type 'com.sap.gateway.srvd.dmo.rapgeneratorbo.v0001.RAPGeneratorBOType';

      expected number of key properties '2'

      Kind regards,

      Andre

       

       

       

      Author's profile photo Ramjee Korada
      Ramjee Korada
      Blog Post Author

      Hi Andre,

      Thanks for your feedback and Glad that your issue is also resolved.

      I was facing the same error and had to debug further to conclude solution. Most of the tutorials talks about $SELFf and we did not realize that key got changed.

      So I mentioned this step as key point to remember.

      Always, you are the first one to give best answers for all our questions.

       

      Best wishes,

      Ramjee Korada.

      Author's profile photo Mahesh Palavalli
      Mahesh Palavalli

      Nice one Ramjee!!

       

      Author's profile photo Mohit Sharma
      Mohit Sharma

      Good Job Ramjee!!

      Author's profile photo Stefan Keuker
      Stefan Keuker

      Hi Ramjee,

      I run into a lock with the 'MODIFY execute EDIT' as the entity from which a draft should be created is already locked by the RAP Framework when the action is triggered from the UI.

      How did you get around that? Thanks for commenting,

      Stefan.

      Author's profile photo Ramjee Korada
      Ramjee Korada

      Hi Stefan,

      Since it is custom action, I don't expect the framework to lock the entity.

      What is your input in your test case? is it active entity ? or draft entity ?

      May be you can explain more ( some screen shots of the logic ) about the problem to understand .

      Best wishes,

      Ramjee Korada

       

       

      better.

      Author's profile photo Stefan Keuker
      Stefan Keuker

      Hi Ramjee,

      I'm using a managed scenario and in a managed scenario according to this help article about the action runtime the framework sets the entity lock when the action is executed. Then according to this help article for the edit action the edit action triggers an exclusive lock on the active entity instance.

      So in the managed scenario both the action triggered lock and the edit triggered lock try to lock the same entity. Which is rejected.

      I see you are using an unmanaged implementation. Did you implement the 'FOR LOCK' method?

      Thanks for your response.

      Stefan.

      Author's profile photo Daniel Lang
      Daniel Lang

      Hey 🙂
      If someone else is running into that problem on a managed instance, I want to share my solution:

      It is possible do disable the default locking mechanism when an action is executed like that:

      action (lock : none) yourActionName result [1] $self;

      Now within this action it is possible to call the Edit action.
      BTW. it is highly recommended to supply a content id (%cid parameter).

      Kind regards,
      Daniel Lang

      Author's profile photo Stefan Keuker
      Stefan Keuker

      Hi Daniel,

      good idea., guess then you can do the enqueue in the action implementation yourself. I ended up helping myself to use an unmanaged lock (lock master unmanaged) in the managed implementation. In the implementation I do enqueue but ignore existing locks held by the current application user.

      Stefan.

      Author's profile photo Alexandra Köthe
      Alexandra Köthe

      Hi Ramjee,

      thanks a lot for this post.

      We have created an action based on this concept which works well. Now, we would like to do somehow the opposite. We would like to change our entity, save this change, and then create the draft instance and display.

      We have an action "REWORK_DATA" which is only available in the display mode and when the field STATE of our root entity has the value "fixed".

      This action changes the Field "STATE" to "Rework" using modify Entities, afterwards we use execute EDIT. The result parameter of the action returns the initial key (not draft) with the new key (draft) as %param.

      Our problem now is that the displayed data of our Child entity depend on this field STATE in a quite complicated way. If  the state has the value "Fixed" we read from one database, if the state has the value "Rework" we mix up data from two databases.

      The saved data are as we expect, but the draft instance has the wrong values for the child entity. The values have not been refreshed after the change of our State field. They are shown as they have been in the state Fixed.

      Is there a way to assure that the Draft instance has they correct values?

      Thanks a lot in advance!!

       

      Best regards,

      Alexandra

      Author's profile photo Ramjee Korada
      Ramjee Korada

      Hi Alex,

      It seems complicated and confusing. The only difference in draft and active instance is "IsActiveEntity".

      Active : (ConUuid=5aa49b80-5db7-1edd-92f5-7bcd575b16df,IsActiveEntity=true)
      Draft : (ConUuid=5aa49b80-5db7-1edd-92f5-7bcd575b16df,IsActiveEntity=false)

      Is the issue of refreshing the item table automatically after an action on Header . Is it possible to connect over call ?

      You can send me an invite -

      koradaramjee@gmail.com or

      https://www.linkedin.com/in/ramjee-korada-5a3a644b/

       

      Best wishes,

      Ramjee Korada