Skip to Content

Those familiar with the new ABAP Programming Model for SAP Fiori might have noticed that in the official documentation, the scenarios covered for coding transactional applications are limited to ones that read and write to custom tables. As we all know, most real life scenarios require applications that read from and write back to standard delivered tables. And as we also know, you always update standard tables using either the standard delivered BAPI’s or handler classes.

I was curious to know if there was a way to leverage the new programming model to create an application that will read from standard tables (tables with non-guid keys in particular) and will save the data back to the database using the aforementioned methods, and it turns out there is (or this blog wouldn’t exist šŸ™‚ ). Now, I don’t know if this is documented somewhere, but at least I couldn’t find it anywhere.

So here are the basic steps to get this scenario working using purchase order tables.

** Disclaimer **

This example is just to explain how to leverage the framework for this scenario, it is missing all the validations and extra annotations you will need for a production ready application. There is enough documentation for that, so it is not covered in this post.

Defining the CDS Data Model for the Draft Business Object

I suggest first creating the views and the associations without any annotations first. Once the CDS views are active, then add the @ObjectModel annotations that will create the Business Object.Ā For this example, there will be a view for the PO header and one for the PO items.

PO Header Model View
dead@AbapCatalog.sqlViewName: 'Z_I_POHEAD_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Header Model View'

@ObjectModel: { compositionRoot: true,
                modelCategory: #BUSINESS_OBJECT,
                transactionalProcessingEnabled: true,
                createEnabled: true,
                updateEnabled: true,
                deleteEnabled: true,
                semanticKey: ['ebeln'],
                draftEnabled: true,
                writeDraftPersistence: 'ZPO_HEADER_D' }

define view Z_I_POHEADER
  as select from ekko
  association [1..*] to Z_I_POITEM as _items on $projection.ebeln = _items.ebeln
{
       @ObjectModel.readOnly: true
  key  ekko.ebeln,
       ekko.bukrs,
       ekko.bstyp,
       ekko.bsart,
       ekko.loekz,
       ekko.aedat,
       ekko.ernam,
       @ObjectModel.readOnly: true
       ekko.lastchangedatetime,
       ekko.lifnr,
       ekko.zterm,
       ekko.ekorg,
       ekko.ekgrp,
       ekko.waers,
       ekko.bedat,
       ekko.knumv,
       ekko.kalsm,
       ekko.stafo,
       ekko.lifre,
       ekko.lands,
       ekko.memory,
       ekko.procstat,
       ekko.reason_code,
       ekko.memorytype,
       @ObjectModel: {
           association: {
               type: [#TO_COMPOSITION_CHILD]
           }
       }
       _items
}
PO Item Model View
@AbapCatalog.sqlViewName: 'Z_I_POITEM_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Item Model View'

@ObjectModel: { updateEnabled: true,
                createEnabled: true,
                deleteEnabled: true,
                semanticKey: ['ebeln', 'ebelp'],
                writeDraftPersistence: 'ZPO_ITEM_D'}

define view Z_I_POITEM
  as select from ekpo
  association [1..1] to Z_I_POHEADER as _header on $projection.ebeln = _header.ebeln
{
       @ObjectModel.readOnly: true
  key  ekpo.ebeln,
       @ObjectModel.readOnly: true
  key  ekpo.ebelp,
       ekpo.loekz,
       ekpo.statu,
       ekpo.aedat,
       ekpo.txz01,
       ekpo.matnr,
       ekpo.ematn,
       ekpo.bukrs,
       ekpo.werks,
       ekpo.lgort,
       ekpo.matkl,
       ekpo.infnr,
       ekpo.menge,
       ekpo.meins,
       ekpo.bprme,
       ekpo.bpumz,
       ekpo.bpumn,
       ekpo.umrez,
       ekpo.umren,
       ekpo.netpr,
       ekpo.peinh,
       ekpo.netwr,
       ekpo.brtwr,
       ekpo.prdat,
       ekpo.bstyp,
       ekpo.effwr,
       ekpo.xoblr,
       ekpo.adrn2,
       @ObjectModel: {
       association: {
               type: [#TO_COMPOSITION_PARENT,#TO_COMPOSITION_ROOT]
           }
       }
       _header
}

Defining the CDS Consumption Views

This step is the same as explained in SAP’s documentation as you can check here. From these views you will be generating the OData service so I suggest using this layer to give user friendly names to the properties.

PO Header Consumption View
@AbapCatalog.sqlViewName: 'Z_C_POHEAD_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Header Consumption View'

@OData.publish: true

@Metadata.allowExtensions: true

@VDM.viewType: #CONSUMPTION

@ObjectModel: { compositionRoot: true,
                transactionalProcessingDelegated: true,
                createEnabled: true,
                updateEnabled: true,
                deleteEnabled: true,
                semanticKey: ['OrderNumber'],
                draftEnabled: true }

define view Z_C_POHEADER
  as select from Z_I_POHEADER
  association [1..*] to Z_C_POITEM as _items on $projection.OrderNumber = _items.OrderNumber
{

  key Z_I_POHEADER.ebeln              as OrderNumber,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.bukrs              as CompanyCode,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.bsart              as OrderType,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.aedat              as CreatedAt,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.ernam              as CreatedBy,
      @ObjectModel.readOnly: true
      Z_I_POHEADER.lastchangedatetime as ChangedAt,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.lifnr              as Vendor,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.ekorg              as PurchasingOrg,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.ekgrp              as PurchasingGrp,
      @ObjectModel.mandatory: true
      Z_I_POHEADER.waers              as CurrencyKey,
      @ObjectModel.association: {
        type: [#TO_COMPOSITION_CHILD]
      }
      _items
}
PO Item Consumption View
@AbapCatalog.sqlViewName: 'Z_C_POITEM_V'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Item Consumption View'

@Metadata.allowExtensions: true

@VDM.viewType: #CONSUMPTION

@ObjectModel: { updateEnabled: true,
                createEnabled: true,
                deleteEnabled: true,
                semanticKey: ['OrderNumber', 'OrderItem']}

define view Z_C_POITEM
  as select from Z_I_POITEM
  association [1..1] to Z_C_POHEADER as _header on $projection.OrderNumber = _header.OrderNumber
{
      @ObjectModel.readOnly: true
  key Z_I_POITEM.ebeln as OrderNumber,
      @ObjectModel.readOnly: true
  key Z_I_POITEM.ebelp as OrderItem,
      Z_I_POITEM.loekz,
      Z_I_POITEM.statu,
      Z_I_POITEM.aedat,
      Z_I_POITEM.txz01,
      Z_I_POITEM.matnr,
      Z_I_POITEM.ematn,
      Z_I_POITEM.bukrs,
      Z_I_POITEM.werks,
      Z_I_POITEM.lgort,
      Z_I_POITEM.matkl,
      Z_I_POITEM.infnr,
      Z_I_POITEM.menge,
      Z_I_POITEM.meins,
      Z_I_POITEM.bprme,
      Z_I_POITEM.bpumz,
      Z_I_POITEM.bpumn,
      Z_I_POITEM.umrez,
      Z_I_POITEM.umren,
      Z_I_POITEM.netpr,
      Z_I_POITEM.peinh,
      Z_I_POITEM.netwr,
      Z_I_POITEM.brtwr,
      Z_I_POITEM.prdat,
      Z_I_POITEM.bstyp,
      Z_I_POITEM.effwr,
      Z_I_POITEM.xoblr,
      Z_I_POITEM.adrn2,
      @ObjectModel.association: {
            type: [#TO_COMPOSITION_PARENT, #TO_COMPOSITION_ROOT]
      }
      _header
}

Implement SAVE and UPDATE logic in Business Object

Now here is where the magic happens šŸ™‚ . If you open the BO that was generated by the CDS view in transaction BOBX and you change the view to “Extended” (on the top menu Goto-> Standard <-> Extended, you can see that when you generate a Business Object from tables with non-guid keys it uses the classĀ /BOBF/CL_DAC_UNION to access data. In the case where the tables you used have guid keys, then it will not need to use the legacy data access and I will point the differences on how to save the data later, but the concept is the same.

A transactional application with Draft capabilities is great because it allows a user to create or edit an existing document without committing the changes without losing the data. And what is even greater is that all the life-cycle of the draft object is taken care of by the framework. So when you create new draft the framework will create the corresponding entries in the draft tables you specified with the annotation “writeDraftPersistence”.

So going back to the Business Object, if you check your root note you will see that it will have a Draft class that was generated when you activated the model view. The names start with “ZCL_DR_*” and inherit from the class /bobf/cl_lib_dr_classic_app. Like I mentioned before the framework will take care of creating the draft objects for you as well as copying the data from an existing object to a draft. The latter scenario happens when you are editing an object that already exists in the database. For example, in our simple purchase scenario, if you edit a purchase order that already exists, the methodĀ /bobf/if_frw_draft~create_draft_for_active_entity from that draft class will be called. The classĀ /bobf/cl_lib_dr_classic_app already contains an implementation of that method you don’t need to do anything for that part to work.

The real question is, once I’m ready to save the changes I’ve done to an existing document or to a newly created document, how do I do it? Since all of the changes (creates and updates) for this type of applications are done to the Draft entity, then regardless on whether you are creating or updating a draft, the framework will treat it the same. Basically, a function import will be exposed in your service that allows you to activate a Draft. This function import is mapped to a BOPF action which ends up calling a method from your draft class. The method isĀ /bobf/if_frw_draft~copy_draft_to_active_entity, and here is where you will code your logic to save the data back to the database using whichever standard deliver method SAP offers for the object you are manipulating.

It is very important to understand how this part works. In this method you have two exporting tables,Ā et_failed_draft_key andĀ et_key_link.

You useĀ et_failed_draft_keyĀ  to notify the framework that the activation of draft failed, andĀ et_key_link to notify it that the activation was successful. AlsoĀ et_key_link allows you to link the key of the active entity and the draft entity. This is crucial for the process because the framework needs this link between the keys in order to delete the draft entity for the active object.

In that method you will need to do the following:

  1. Read the data from the Draft entity your are trying to “activate” to determine whether is an UPDATE to an existing entity or is a new object you need to CREATE. You can do something like this to determine that.
 "Read node data
    io_read->retrieve(
      EXPORTING
        iv_node       = is_ctx-node_key
        it_key        = it_draft_key
      IMPORTING
        et_data       = <ft_data>
    ).

    IF lines( <ft_data> ) NE 1.
      RAISE EXCEPTION TYPE /bobf/cx_frw_fatal.
    ENDIF.

    ""Check if Draft has an active entity
    READ TABLE <ft_data> ASSIGNING FIELD-SYMBOL(<fs_data>) INDEX 1.
    ASSIGN COMPONENT if_draft_constants=>co_db_fieldname-has_active_entity OF STRUCTURE <fs_data> TO FIELD-SYMBOL(<fs_has_active>).
   
    IF <fs_has_active> EQ abap_true.
      "Update - Call BAPI to UPDATE
    ELSE.
      "Create - Call BAPI to CREATE
    ENDIF.
  1. Get the Draft data from the parent object and from the children that allow modifications. Use the io_read object to retrieve the data.
  2. Use the Draft data to populate the interface of whichever BAPI or handler class you are using.
  3. Get the key from the newly created object. You only need the key of the root object, not from any of the children. The framework will take care of deleting all the entries from the associated tables of the Draft object.
  4. Map the key form the active object to the draft object.
  • For then the key of the active object is a guid, all you need to do is populate the table et_key_link like so:
INSERT VALUE #( active = <fs_guid> draft = it_draft_key[ 1 ]-key ) INTO TABLE et_key_link.
  • For then the key of the active object is not a guid, like in the case of the purchase order you need to map the key structure to a guid generated using a magical class called /bobf/cl_dac_legacy_mapping like so:
    DATA: lr_active_key TYPE REF TO data.

    "Get BOPF configuration
    DATA(lo_conf) = /bobf/cl_frw_factory=>get_configuration( iv_bo_key = is_ctx-bo_key ).
    lo_conf->get_node( EXPORTING iv_node_key = is_ctx-node_key
                       IMPORTING es_node     = DATA(ls_node_conf) ).

    "Get key structure for active object
    lo_conf->get_altkey( 
       EXPORTING 
        iv_node_key    = is_ctx-node_key
        iv_altkey_name = /bobf/if_conf_cds_link_c=>gc_alternative_key_name-draft-active_entity_key
       IMPORTING 
        es_altkey      = DATA(ls_altkey_active_key) ).

    CREATE DATA lr_active_key TYPE (ls_altkey_active_key-data_type).
    ASSIGN lr_active_key->* TO FIELD-SYMBOL(<ls_active_entity_key>).
    
    "Map your key to structure like with whichever method you want, I use move-corresponding
    MOVE-CORRESPONDING <fs_key> TO <ls_active_entity_key>.
    
    "Map the active key structure to a GUID key
    DATA(lo_dac_map) = /bobf/cl_dac_legacy_mapping=>get_instance( iv_bo_key = is_ctx-bo_key 
                                                                  iv_node_key = is_ctx-node_key ).
    lo_dac_map->map_key_to_bopf( EXPORTING ir_database_key = lr_active_key
                                 IMPORTING es_bopf_key     = DATA(ls_key) ).
    
    INSERT VALUE #( active = ls_key-key draft = it_draft_key[ 1 ]-key ) INTO TABLE et_key_link.

 

And done!!

As I mentioned before in this blog I’m not covering locking, deleting standard objects, validations, etc. For that there is some good documentation here. For locking and deleting objects, when your Business Object gets generated there will be two actions called LOCK_<name_of_bo> and DELETE_<name_of_bo> with Z* implementation classes also generated by the framework. You can put your code there.

I hope this information has been useful šŸ˜€

 

 

To report this post you need to login first.

5 Comments

You must be Logged on to comment or reply to a post.

  1. Sitakant Tripathy

     

    Hi Diego,

    Nice one, could you please clarify something for me.

    Disclaimer:Ā not hands on in usingĀ BOPF yet..

    BOPF does standardize the developer effort to a large extent and developers would generally be reduced to building the business logic in terms of validations and actions with even the database updates dynamically controlled and tied up with entity node contents.

    With your hack in method Ā /bobf/if_frw_draft~copy_draft_to_active_entity, how do you break the logical flow in framework for it to not do the built in database updates at the end of the save event.

    so in your example the nodes representing EKKO and EKPO would still be created and would still be updated as part of the framework and looks likeĀ duplicate DBĀ updates considering the BAPI calls.

    Regards,

    Sitakant.

     

    (0) 
    1. Diego Borja Post author

      HiĀ Sitakant,

      The BOPF framework will only do updates on the Draft tables because you are not setting any

      tables with the “writeActivePersistence” annotation. For this reason the BOPF will never try to update EKKO and EKPO in the example I put. TheĀ /bobf/if_frw_draft~copy_draft_to_active_entity implementation is not a hack. When you only use the annotation “writeDraftPersistence”, the pattern used to persist active data is through an “Activate” action.

      I hope this clarifies your question.

      Cheers,

      Diego

      (1) 

Leave a Reply