Business Object Behavior @ ABAP RESTful Application Programming Model (On-Premise)
September 12, 2022
  1. Design / Prototype
  2. Create Dictionary Objects
  3. Core Data Services (CDS)
  4. Business Service
  5. Business Object Behavior
  6. Create a Fiori App
  7. Source (GitHub) for Data Model/Behaviour/Service
  8. Source (GitHub) for User Interface (Fiori Elements App)

Introduction

business object (BO) is a common term to represent a real-world artifact in enterprise application development such as the Product, the Travel, or the SalesOrder.

When going to implement an application scenario based on business objects, we may distinguish between the external, consumer-related representation of a business object and the internal, provider-related perspective:
– The external perspective hides the intrinsic complexity of business objects.
– The internal perspective exposes the implementation details and the complexity of business objects.

From a formal point of view, a business object is characterized by
– a structure,
– a behavior and
– the corresponding runtime implementation.

Structure

All entities – except the root entity – that represent a node of the business object structure serve as a:

  • Parent entity – if it represents a node in a business object’s structure that is directly connected to another node when moving towards the root.
  • Child entity – if it represents a node in a business object’s structure that is directly connected to another node (parent node) when moving away from the root.
  • Leaf entity – if it represents a node in a business object’s structure without any child nodes. A leaf entity is a CDS entity, which is the target of composition (a child entity node) but does not contain a composition definition.

Behavior Definition

To specify the business object’s behavior, the behavior definition as the corresponding development object is used. A business object behavior definition (behavior definition for short) is an ABAP Repository object that describes the behavior of a business object in the context of the ABAP RESTful application programming model. A behavior definition is defined using the Behavior Definition Language (BDL).

A behavior definition always refers to a CDS data model. As shown in the figure below, a behavior definition relies directly on the CDS root entity. One behavior definition refers exactly to one root entity and one CDS root entity has at most one behavior definition (a 0..1 cardinality), which also handles all included child entities that are included in the composition tree. The implementation of a behavior definition can be done in a single ABAP class (behavior pool) or can be split between an arbitrary set of ABAP classes (behavior pools). The application developer can assign any number of behavior pools to a behavior definition (1..N cardinality).

A behavior specifies the operations and field properties of an individual business object in the ABAP RESTful programming model. It includes a behavior characteristic and a set of operations for each entity of the business object’s composition tree.

Behavior Characteristic

Behavior characteristic is that part of the business object’s behavior that specifies general properties of an entity such as:
ETag
Draft handling
Feature control
Numbering
Authorization Control
Apart from draft capabilities, these characteristics can be defined for each entity separately.

Operations

Each entity of a business object can offer a set of operations. They can cause business data changes that are performed within a transactional life cycle of the business object. As depicted in the diagram above, these modified operations include the standard operations create(), update(), and delete() as well as lock implementations and application-specific operations with dedicated input and output structures which are called actions. Another kind of operation is the read operation: they do not change any business data in the context of a business object behavior. Read operations include read, read by association, and functions (that are similar to actions, however, without causing any side effects).

For more information, see
Create Operation
Update Operation
Delete Operation
Actions
Locking

Source Code @ ZINTLRA_I_PB_HEAD (Behavior Definition)

managed; // implementation in class zbp_intlra_i_pb_head unique;
with draft;

define behavior for ZINTLRA_I_PB_HEAD alias Contact
implementation in class ZBP_INTLRA_I_PB_HEAD unique
persistent table zintlra_pb_d_hdr
draft table zint_pb_d_hdrtmp
lock master total etag PbChangedAt
etag master PbChangedAt

{
  create;
  update;
  delete;
  association _item { create(features : instance); with draft; }

  field(numbering : managed, readonly) PbUuid;
  field(mandatory) PbTagCode,PbLastName,PbEmailId;
  field(readonly) PbOwner, PbId, PbCreatedAt, PbChangedAt;

  action(features : instance) setFavourite    result [1] $self;
  action(features : instance) removeFavourite result [1] $self;

  determination setPhoneBookId on save { field PbId; create; }

  validation validateEmail on save { field PbEmailId; create;update; }

  draft determine action Prepare {
    validation validateEmail;
    validation PhoneNumber~validatePhone;
  }

  mapping for zintlra_pb_d_hdr{

    PbUuid       = pb_uuid;
    PbOwner      = pb_owner;
    PbId         = pb_id;
    PbTagCode    = pb_tag_code;
    PbFirstName  = pb_first_name;
    PbLastName   = pb_last_name;
    PbCreatedAt  = pb_created_at;
    PbChangedAt  = pb_changed_at;
    PbEmailId    = pb_email_id;
    PbFavourite  = pb_favourite;

  }

}

define behavior for ZINTLRA_I_PB_ITEM alias PhoneNumber
implementation in class ZBP_INTLRA_I_PB_ITEM unique
persistent table zintlra_pb_d_itm
draft table zint_pb_d_itmtmp
lock dependent by _hdr
etag master PbChangedAt
{
  update;
  delete;
  association _hdr{ with draft; }

  field(numbering : managed, readonly) PbItemUuid;
  field(mandatory) PbCategory, PbTelephone;
  field(readonly) PbUuid, PbId, PbItemId, PbCreatedAt, PbChangedAt;

  action(features : instance) setDefault result [1] $self;
  internal action setNotDefault;

  determination setPhoneBookItemId on save { field PbItemId; create; }
  validation validatePhone on save { field PbTelephone; create;update; }

  mapping for zintlra_pb_d_itm {
    PbItemUuid  = pb_item_uuid;
    PbUuid      = pb_uuid;
    PbId        = pb_id;
    PbItemId    = pb_item_id;
    PbCategory  = pb_category;
    PbTelephone = pb_telephone;
    PbCreatedAt = pb_created_at;
    PbChangedAt = pb_changed_at;
    PbDefault   = pb_default;
  }
}
Before activating above we will have to create and activate the implementation classes and draft tables. You can right click on class name and draft table name and select Quick Fix to implement the same

<< To Top

Behavior Implementation Classes

Source @ ZBP_INTLRA_I_PB_HEAD

CLASS zbp_intlra_i_pb_head DEFINITION PUBLIC ABSTRACT FINAL FOR BEHAVIOR OF zintlra_i_pb_head.
ENDCLASS.

CLASS zbp_intlra_i_pb_head IMPLEMENTATION.
ENDCLASS.
CLASS lhc_Contact DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    CONSTANTS: BEGIN OF lc_favourite,
                 yes TYPE boolean VALUE 'X',
                 no  TYPE boolean VALUE '',
               END of lc_favourite.

    METHODS get_features FOR FEATURES
      IMPORTING keys REQUEST requested_features FOR Contact RESULT result.

    METHODS setFavourite FOR MODIFY
      IMPORTING keys FOR ACTION contact~setfavourite RESULT result.

    METHODS removeFavourite FOR MODIFY
      IMPORTING keys FOR ACTION contact~removefavourite RESULT result.

    METHODS setPhoneBookId FOR DETERMINE ON SAVE
      IMPORTING keys FOR Contact~setPhoneBookId.
    METHODS validateEmail FOR VALIDATE ON SAVE
      IMPORTING keys FOR Contact~validateEmail.

ENDCLASS.

CLASS lhc_Contact IMPLEMENTATION.

  METHOD get_features.

    " Fill the response table
    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY Contact
    ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(Contacts).

    result = VALUE #( FOR contact IN Contacts
                        LET
                            is_set_as_fav     = COND #( WHEN contact-PbFavourite = lc_favourite-yes
                                                        THEN if_abap_behv=>fc-o-disabled
                                                        ELSE if_abap_behv=>fc-o-enabled  )
                            is_not_set_as_fav = COND #( WHEN contact-PbFavourite = lc_favourite-no
                                                        THEN if_abap_behv=>fc-o-disabled
                                                        ELSE if_abap_behv=>fc-o-enabled  )
                        IN
                            ( %tky                   = contact-%tky
                              %action-setFavourite    = is_set_as_fav
                              %action-removeFavourite = is_not_set_as_fav ) ).

  ENDMETHOD.

  METHOD setFavourite.

    " Set as Favourite
    MODIFY ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
      ENTITY Contact
         UPDATE
           FIELDS ( PbFavourite )
           WITH VALUE #( FOR key IN keys
                           ( %tky      = key-%tky
                             PbFavourite = lc_favourite-yes ) )
    FAILED failed
    REPORTED reported.

    " Fill the response table
    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY Contact
    ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(Contacts).

    result = VALUE #( FOR contact IN Contacts
                        ( %tky   = contact-%tky
                          %param = contact ) ).
  ENDMETHOD.

  METHOD removeFavourite.

    " Set as Not a Favourite
    MODIFY ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
      ENTITY Contact
         UPDATE
           FIELDS ( PbFavourite )
           WITH VALUE #( FOR key IN keys
                           ( %tky      = key-%tky
                             PbFavourite = lc_favourite-no ) )
    FAILED failed
    REPORTED reported.

    " Fill the response table
    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY Contact
    ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(Contacts).

    result = VALUE #( FOR contact IN Contacts
                        ( %tky   = contact-%tky
                          %param = contact ) ).
  ENDMETHOD.

  METHOD setPhoneBookId.
    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY Contact
    ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(Contacts).

    DELETE Contacts WHERE PbId IS NOT INITIAL.

    CHECK Contacts IS NOT INITIAL.

    LOOP AT Contacts ASSIGNING FIELD-SYMBOL(<lfs_contact>).
        " Get Numbers
        TRY.
            cl_numberrange_runtime=>number_get(
              EXPORTING
                nr_range_nr       = '01'
                object            = 'ZPBID'
              IMPORTING
                number            = DATA(lv_number)
            ).
            <lfs_contact>-PbId = lv_number.
          CATCH cx_number_ranges INTO DATA(lx_number_ranges).
        ENDTRY.
    ENDLOOP.

    MODIFY ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY Contact
    UPDATE
    FROM VALUE #( FOR contact IN Contacts INDEX INTO i (
    %tky = contact-%tky
    PbId = contact-PbID
    %control-PbId = if_abap_behv=>mk-on ) )
    REPORTED DATA(update_reported).

    reported = CORRESPONDING #( DEEP update_reported ).
  ENDMETHOD.

  METHOD validateEmail.
    DATA: go_regex   TYPE REF TO cl_abap_regex,
          go_matcher TYPE REF TO cl_abap_matcher,
          go_match   TYPE c LENGTH 1.

      CREATE OBJECT go_regex
        EXPORTING
          pattern     = '\w+(\.\w+)*@(\w+\.)+(\w{2,4})'
          ignore_case = abap_true.

      " Read E-Mail ID's
      READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
      ENTITY Contact
      FIELDS ( PbEmailId ) WITH CORRESPONDING #( keys )
      RESULT DATA(Contacts).

      LOOP AT Contacts INTO DATA(contact).
         " Validate E-Mail
         go_matcher = go_regex->create_matcher( text = contact-PbEmailId ).

         APPEND VALUE #( %tky = contact-%tky %state_area = 'VALIDATE_EMAIL' ) TO reported-contact.
         IF go_matcher->match( ) IS INITIAL.
           " Invalid
           APPEND VALUE #( %tky = contact-%tky ) TO failed-contact.
           APPEND VALUE #( %tky = contact-%tky
                           %state_area = 'VALIDATE_EMAIL'
                           %msg = NEW zcm_intlra_pb(
                                    severity = if_abap_behv_message=>severity-error
                                    textid   = zcm_intlra_pb=>invalid_email
                                    email    = contact-PbEmailId
                                  )
                          %element-PbEmailId = if_abap_behv=>mk-on )
                     TO reported-contact.
         ENDIF.
      ENDLOOP.
  ENDMETHOD.

ENDCLASS.

Source @ ZBP_INTLRA_I_PB_ITEM

CLASS zbp_intlra_i_pb_item DEFINITION PUBLIC ABSTRACT FINAL FOR BEHAVIOR OF zintlra_i_pb_head.
ENDCLASS.

CLASS zbp_intlra_i_pb_item IMPLEMENTATION.
ENDCLASS.
CLASS lhc_phonenumber DEFINITION INHERITING FROM cl_abap_behavior_handler.

  PRIVATE SECTION.
    CONSTANTS: BEGIN OF lc_default,
                 yes TYPE boolean VALUE 'X',
                 no  TYPE boolean VALUE ' ',
               END of lc_default.
    METHODS get_features FOR FEATURES
      IMPORTING keys REQUEST requested_features FOR PhoneNumber RESULT result.

    METHODS setDefault FOR MODIFY
      IMPORTING keys FOR ACTION PhoneNumber~setDefault RESULT result.
    METHODS setPhoneBookItemId FOR DETERMINE ON SAVE
      IMPORTING keys FOR PhoneNumber~setPhoneBookItemId.
    METHODS setNotDefault FOR MODIFY
      IMPORTING keys FOR ACTION PhoneNumber~setNotDefault.
    METHODS validatePhone FOR VALIDATE ON SAVE
      IMPORTING keys FOR PhoneNumber~validatePhone.

ENDCLASS.

CLASS lhc_phonenumber IMPLEMENTATION.
  METHOD get_features.

    " Fill the response table
    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY PhoneNumber
    ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(PhoneNumbers).
    result = VALUE #( FOR Number IN PhoneNumbers
                                                  LET
                            is_set_as_default = COND #( WHEN Number-PbDefault = lc_default-yes
                                                        THEN if_abap_behv=>fc-o-disabled
                                                        ELSE if_abap_behv=>fc-o-enabled  )
                        IN
                            ( %tky                 = Number-%tky
                              %action-setDefault = is_set_as_default ) ).

  ENDMETHOD.

  METHOD setDefault.
    DATA: lt_no_default TYPE TABLE FOR UPDATE zintlra_i_pb_item.

    " Get Parent Keys
    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY PhoneNumber BY \_hdr
    FIELDS ( PbUuid ) WITH CORRESPONDING #( keys )
    RESULT DATA(ParentKeys).

    READ TABLE keys INTO DATA(key1) INDEX 1.
    CLEAR lt_no_default.
    LOOP AT ParentKeys INTO DATA(parentkey).
        " Get Parent Keys
        READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
        ENTITY Contact BY \_item
        FIELDS ( PbItemUuid ) WITH VALUE #( ( %tky = parentkey-%tky ) )
        RESULT DATA(Phonekeys).

        LOOP AT Phonekeys INTO DATA(pk).
            IF pk-%tky NE key1-%tky.
                APPEND INITIAL LINE TO lt_no_default ASSIGNING FIELD-SYMBOL(<lfs_nodef>).
                <lfs_nodef> = CORRESPONDING #( pk ).
            ENDIF.
        ENDLOOP.
    ENDLOOP.

    " Set the Default status
    MODIFY ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
      ENTITY PhoneNumber
         UPDATE
           FIELDS ( PbDefault )
           WITH VALUE #( FOR key IN keys
                           ( %tky      = key-%tky
                             PbDefault = lc_default-yes ) )
    FAILED failed
    REPORTED DATA(reported1).

    " Set the  NOT Default status
    IF NOT lt_no_default IS INITIAL.
        MODIFY ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
          ENTITY PhoneNumber
          EXECUTE setNotDefault
          FROM CORRESPONDING #( lt_no_default ).
    ENDIF.

    " Fill the response table
    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY PhoneNumber
    ALL FIELDS WITH CORRESPONDING #( phonekeys )
    RESULT DATA(PhoneNumbers).

    result = VALUE #( FOR Number IN PhoneNumbers
                          ( %tky   = Number-%tky
                            %param = Number ) ).
  ENDMETHOD.

  METHOD setPhoneBookItemId.

    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY PhoneNumber
    ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(PhoneNumbers).

    DELETE PhoneNumbers WHERE PbItemId IS NOT INITIAL.

    CHECK PhoneNumbers IS NOT INITIAL.

    READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY PhoneNumber BY \_hdr
    FIELDS ( PbUuid PbId ) WITH CORRESPONDING #( keys )
    RESULT DATA(Contacts).

    READ TABLE Contacts INTO DATA(Contact) INDEX 1.

    " Get Current Max Item#
    SELECT max( pb_item_id ) FROM zintlra_pb_d_itm
     WHERE pb_uuid = @Contact-PbUuid
      INTO @DATA(lv_current_item).

    LOOP AT PhoneNumbers ASSIGNING FIELD-SYMBOL(<lfs_number>).
        lv_current_item += 10.
        <lfs_number>-PbId = Contact-PbId.
        <lfs_number>-PbItemId = lv_current_item.
    ENDLOOP.

    MODIFY ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
    ENTITY PhoneNumber
    UPDATE
    FROM VALUE #( FOR Number IN PhoneNumbers INDEX INTO i (
    %tky = Number-%tky
    PbId     = Number-PbId
    PbItemId = Number-PbItemId
    %control-PbItemId = if_abap_behv=>mk-on
    %control-PbId = if_abap_behv=>mk-on ) )
    REPORTED DATA(update_reported).

    reported = CORRESPONDING #( DEEP update_reported ).
  ENDMETHOD.

  METHOD setNotDefault.
    " Set the NOT Default status
    MODIFY ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
      ENTITY PhoneNumber
         UPDATE
           FIELDS ( PbDefault )
           WITH VALUE #( FOR key IN keys
                           ( %tky      = key-%tky
                             PbDefault = lc_default-no ) )
    FAILED failed
    REPORTED DATA(reported1).
  ENDMETHOD.

METHOD validatePhone.
      DATA lv_phone TYPE string.
      " Read Phone Numbers
      READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
      ENTITY PhoneNumber
      ALL FIELDS WITH CORRESPONDING #( keys )
      RESULT DATA(PhoneNumbers).

      READ ENTITIES OF zintlra_i_pb_head IN LOCAL MODE
      ENTITY PhoneNumber BY \_hdr
      FROM CORRESPONDING #( PhoneNumbers )
      LINK DATA(ContactPhoneNumberLinks).

      LOOP AT PhoneNumbers INTO DATA(phonenumber).
         " Validate Phone
         APPEND VALUE #( %tky = phonenumber-%tky %state_area = 'VALIDATE_PHONE' ) TO reported-phonenumber.
         CLEAR lv_phone.
         lv_phone = phonenumber-PbTelephone.
         REPLACE ALL OCCURRENCES OF ` ` IN lv_phone  WITH ''.
         IF lv_phone CN '+-1234567890'.
           " Invalid
           APPEND VALUE #( %tky = phonenumber-%tky ) TO failed-phonenumber.
           APPEND VALUE #( %tky = phonenumber-%tky
                           %state_area = 'VALIDATE_PHONE'
                           %msg = NEW zcm_intlra_pb(
                                    severity = if_abap_behv_message=>severity-error
                                    textid   = zcm_intlra_pb=>invalid_phone
                                    phone    = phonenumber-PbTelephone
                                  )
                          %element-PbTelephone = if_abap_behv=>mk-on
                          %path = VALUE #( contact-%tky    = ContactPhoneNumberLinks[ source-%tky = phonenumber-%tky ]-target-%tky )
                         )
                     TO reported-phonenumber.
         ENDIF.
      ENDLOOP.
  ENDMETHOD.

ENDCLASS.

<< To Top

Behavior Projection

Source Code @ ZINTLRA_C_PB_HEAD (Behavior Projection)

projection;
use draft;

define behavior for ZINTLRA_C_PB_HEAD alias Contact
use etag
{
  use create;
  use update;
  use delete;

  use action setFavourite;
  use action removeFavourite;
  use association _item { create; with draft; }
}

define behavior for ZINTLRA_C_PB_ITEM alias PhoneNumber
use etag
{
  use update;
  use delete;
  use action setDefault ;
  use association _hdr{ with draft; }
}

<< To Top

BO Runtime

The business object runtime mainly consists of two parts:
The first part is the interaction phase, in which a consumer calls the business object operations to change data and read instances with or without the transactional changes. The business object runtime keeps the changes in its internal transactional buffer which represents the state of the instance data. This transactional buffer is always required for a business object. After all, changes were performed, the data can be persisted. This is realized with the save sequence.

For more information about the BO runtime within one LUW, see The RAP Transactional Model for the SAP LUW.

BO Runtime

BO Runtime

For each operation, the transactional runtime is described in detail in the respective runtime diagrams. See Operations.

The save sequence has the same structure for each operation. For more information, see Save Sequence Runtime.

Instantiation of Handler and Saver Classes

In general, the instantiation of handler and saver classes is tightly coupled to an ABAP session. Instances live as long as the ABAP session and they can exist across LUW borders. However, their life cycle depends on the way their methods are called. Nested method invocations always provoke the re-instantiation of the local handler and saver classes. For example, this is the case if a handler method executes an EML to modify the request in local mode to call another method of the same handler class, or if other BOs are involved in the invocation chain.

<< To Top

The blog refers to SAP HELP (Source for RAP Information)