Association vs Composition, Virtual Elements, Unmanaged and Additional Save in SAP Restful Application Programming
Association v/s Composition
In SAP Restful application programming, both association and composition are important concepts related to handling relationships between entities in the context of OData services. Let's delve into each concept:
Association:
Composition:
In SAP Restful application programming, we'll often encounter scenarios where we need to decide between association and composition based on the nature of the relationship between entities.
Associations are more loosely coupled and suitable when entities have independent lifecycles, while compositions imply a stronger relationship and lifecycle dependency between entities.
The choice between these two depends on specific business logic and data model requirements.
We can consider Sales Order example to create composite based RAP application.
Here we will create 3 table (Header , Item and Billing table ) to demonstrate the functionality. Where item and billing details are tightly linked/dependent on header information.
Header Table
@EndUserText.label : 'header details'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zheader {
key client : abap.clnt not null;
key hdrkey : sysuuid_x16 not null;
inv_re_no : abap.char(10) not null;
ship_loc : abap.char(25);
sales_area : abap.char(25);
part_no : abap.char(10);
ship_status : abap.char(2);
uom : meins;
@Semantics.quantity.unitOfMeasure : 'zheader.uom'
ship_quan : abap.quan(10,0);
@Semantics.quantity.unitOfMeasure : 'zheader.uom'
reciev_quan : abap.quan(10,0);
upd_by : abap.char(10);
upd_at : timestampl;
locallastchangedat : timestampl;
}
Item Table
@EndUserText.label : 'Item table for CAP BTP'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zitem {
key client : abap.clnt not null;
@AbapCatalog.foreignKey.screenCheck : false
key hdrkey : sysuuid_x16 not null
with foreign key zheader
where client = zitem.client
and hdrkey = zitem.hdrkey;
key itmkey : sysuuid_x16 not null;
inv_re_no : abap.char(10) not null;
item_no : abap.numc(3);
itm_status : abap.char(10);
sales_office : abap.char(10);
uom : meins;
@Semantics.quantity.unitOfMeasure : 'zheader.uom'
delv_qty : abap.quan(10,0);
@Semantics.quantity.unitOfMeasure : 'zheader.uom'
apr_qty : abap.quan(10,0);
}
Billing Information : This table would be storing final amount after discount .
@EndUserText.label : 'billing table'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zbilling {
key client : abap.clnt not null;
@AbapCatalog.foreignKey.screenCheck : false
key hdrkey : sysuuid_x16 not null
with foreign key zheader
where client = zbilling.client
and hdrkey = zbilling.hdrkey;
key itmkey : sysuuid_x16 not null;
inv_re_no : abap.char(10) not null;
item_no : abap.numc(3);
discount : abap.numc(2);
}
Now comes the most important part of designing the Business Object( BO) .
A business object is common term to refer real world artifact in enterprise application development such as product , travel or sales order.
In general, a business object contains several nodes such as Items and Schedule Lines and common transactional operations such as for creating, updating and deleting business data.
Lets start designing the Nodes . We will create 3 different CDS view (each of tables).
While designing CDS view we need to mention parent and child relationship using composition.
Header Interface Root View
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'header info'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
define root view entity zi_header_info as select from zheader composition[0..*] of zi_item_info as _Item
composition[0..*] of zi_billing_info as _billing
{
key hdrkey as Hdrkey,
inv_re_no as InvReNo,
ship_loc as ShipLoc,
sales_area as SalesArea,
part_no as PartNo,
ship_status as ShipStatus,
uom as Uom,
// @Semantics.quantity.unitOfMeasure : 'zheader.uom'
// ship_quan as ShipQuan,
// reciev_quan as RecievQuan,
upd_by as UpdBy,
@Semantics.systemDateTime.lastChangedAt: true
upd_at as upd_at,
@Semantics.systemDateTime.localInstanceLastChangedAt:true
locallastchangedat as locallastchangedat ,
_Item,
_billing
}
Item CDS View
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'item info'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
define view entity zi_item_info as select from zitem association to parent ZI_Header_info as _Header on $projection.Hdrkey = _Header.Hdrkey
{
key hdrkey as Hdrkey,
key itmkey as Itmkey,
inv_re_no as InvReNo,
item_no as ItemNo,
itm_status as ItmStatus,
sales_office as SalesOffice,
uom as Uom,
@Semantics.quantity.unitOfMeasure : 'UOM'
delv_qty as DelvQty,
@Semantics.quantity.unitOfMeasure : 'UOM'
apr_qty as AprQty,
_Header
}
Billing CDS View
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Billing details'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
define view entity zi_billing_info as select from zbilling association to parent zi_header_info as _Header on $projection.Hdrkey = _Header.Hdrkey
{
key zbilling.hdrkey as Hdrkey,
key zbilling.itmkey as Itmkey,
zbilling.inv_re_no as InvReNo,
zbilling.item_no as ItemNo,
zbilling.discount as Discount,
_Header
}
Now Our base model is ready . We will follow RAP practice to create projection view and here we need to define Parent and child relationship explicitly in PROJECTION CDS.
WE DEFINE PARENT CHILD relationship in PROJECTION CDS.
Projection of Header Root View
@EndUserText.label: 'Projection of Header Entity'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
define root view entity zc_header_info as projection on zi_header_info
{
key Hdrkey,
InvReNo,
ShipLoc,
SalesArea,
PartNo,
ShipStatus,
Uom,
UpdBy,
upd_at,
locallastchangedat as locallastchangedat ,
@ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_VIRTUALFL'
@UI: {
lineItem: [ { position: 50, importance: #HIGH } ],
identification: [ { position: 50, label: 'Amount' } ] }
@Search.defaultSearchElement: true
virtual Amount : abap.int4,
/* Associations */
_Item : redirected to composition child zc_item_info ,
_billing : redirected to composition child zc_billing_info
}
Note : We will talk about Virtual Element implementation in details in later part of article.
Projection of Item CDS View
@EndUserText.label: 'Projection of Item Entity'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
define view entity zc_item_info as projection on zi_item_info
{
key Hdrkey,
key Itmkey,
InvReNo,
ItemNo,
ItmStatus,
SalesOffice,
Uom,
DelvQty,
AprQty,
/* Associations */
_Header: redirected to parent zc_header_info
}
Projection of Billing CDS view
@EndUserText.label: 'billing info'
@AccessControl.authorizationCheck: #NOT_REQUIRED
define view entity zc_billing_info as projection on zi_billing_info
{
key Hdrkey,
key Itmkey,
@UI.facet: [ { id: 'Invreno',
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Invreno',
position: 10 }
]
// @UI: {
// lineItem: [ { position: 10, importance: #HIGH } ],
// identification: [ { position: 10, label: 'InvReNo' } ] }
//
@UI: {
lineItem: [ { position: 10, importance: #HIGH } ],
identification: [ { position: 10, label: 'InvReNo' } ] }
InvReNo,
@UI: {
lineItem: [ { position: 20, importance: #HIGH } ],
identification: [ { position: 20, label: 'ItemNo' } ] }
ItemNo,
@UI: {
lineItem: [ { position: 30, importance: #HIGH } ],
identification: [ { position: 30, label: 'Discount' } ] }
Discount,
/* Associations */
_Header : redirected to parent zc_header_info
}
NOW our Business Object is ready. We need to create Behavior definition on top of Interface view and Projection view(ROOT VIEW).
managed implementation in class zbp_i_header_info unique;
strict(2) ;
with draft;
define behavior for zi_header_info //alias <alias_name>
persistent table zheader
lock master total etag upd_at
authorization master ( instance )
etag master locallastchangedat
{
create;
update;
delete;
association _Item { create; }
association _billing { create; }
}
define behavior for zi_item_info //alias <alias_name>
persistent table zitem
lock dependent by _Header
authorization dependent by _Header
{
update;
delete;
association _Header;
}
define behavior for zi_billing_info //alias <alias_name>
persistent table zbilling
lock dependent by _Header
authorization dependent by _Header
{
update;
delete;
association _Header;
}
If We notice on Behavior definition ,Header entity is allowed for CREATE,UPADTE,DELETE and framework as auto added entry for ITEM and BILLING create on the basis of HEADER data through below code.
association _Item { create; }
association _billing { create; }
Above association is mandatory for Item and billing data creation on the basis of header.
Likewise below association defined in ITEM and Billing behavior definition .
association _Header;
NOTE : When we have right click on interface CDS of Header (that marked as root) , framework has auto generated below code for behavior with header ,item and billing information with association defined.
BUT WHY ?? Because it has composition relationship .
NOTE : HEADER is ROOT entity only .(Behavior definition can only be created on ROOT entity).
Now we will create behavior definition on Projection CDS of Header entity.
projection;
strict ( 2 );
use draft;
define behavior for zc_header_info //alias <alias_name>
{
use create;
use update;
use delete;
use association _Item { create; }
use association _billing { create; }
}
define behavior for zc_item_info //alias <alias_name>
{
use update;
use delete;
use association _Header;
}
define behavior for zc_billing_info //alias <alias_name>
{
use update;
use delete;
use association _Header;
}
Note : Metadata annotations plays important role while designing application. I am providing metadata extension as well of CDS entities for reference.
Facet is most important annotation here that design the object page and with the help of parent-child relationship defined on behavior definition we will be able to see CREATE ,UPDATE,DELETE buttons on object page for ITEM data.
Header Metadata Ext
@Metadata.layer: #CORE
annotate view zc_header_info
with
{
@UI.facet: [ { id: 'Invreno',
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Invreno',
position: 10
} ,
{
type: #LINEITEM_REFERENCE,
position: 20,
label: 'Items',
targetElement: '_Item'
},
{
type: #LINEITEM_REFERENCE,
position: 30,
label: 'Billing',
targetElement: '_billing'
}
]
Hdrkey ;
@UI: {
lineItem: [ { position: 10, importance: #HIGH } ],
identification: [ { position: 10, label: 'Inv. Receipt No' } ] }
@Search.defaultSearchElement: true
InvReNo;
@UI: {
lineItem: [ { position: 20, importance: #HIGH } ],
identification: [ { position: 20, label: 'Ship Loc' } ],
selectionField: [{ position: 10 }] }
@Search.defaultSearchElement: true
ShipLoc;
@UI: {
lineItem: [ { position: 30, importance: #HIGH } ],
identification: [ { position: 30, label: ' Sales Area' } ] }
@Search.defaultSearchElement: true
SalesArea;
@UI: {
lineItem: [ { position: 40, importance: #HIGH } ],
identification: [ { position: 40, label: 'Ship Status' } ] }
@Search.defaultSearchElement: true
ShipStatus;
}
Item Metadata Ext
@Metadata.layer: #CUSTOMER
annotate view zc_item_info
with
{
@UI.facet: [ { id: 'Invreno',
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Invreno',
position: 10 }
]
@UI: {
lineItem: [ { position: 10, importance: #HIGH } ],
identification: [ { position: 10, label: 'ITem No' } ] }
ItemNo;
@UI: {
lineItem: [ { position: 30, importance: #HIGH } ],
identification: [ { position: 30, label: 'Status' } ] }
ItmStatus;
@UI: {
lineItem: [ { position: 40, importance: #HIGH } ],
identification: [ { position: 40, label: 'Salesoffice' } ] }
SalesOffice;
@UI: {
lineItem: [ { position: 50, importance: #HIGH } ],
identification: [ { position: 50, label: 'Uom' } ] }
Uom;
@UI: {
lineItem: [ { position: 60, importance: #HIGH } ],
identification: [ { position: 60, label: 'Deliver Quantity' } ] }
DelvQty;
@UI: {
lineItem: [ { position: 70, importance: #HIGH } ],
identification: [ { position: 70, label: 'Approved Quantity' } ] }
AprQty;
}
Note: We have not created separate metadata ext for billing , annotations is written in billing CDS view.
Now we will create service definition and expose projection views of Header , item and Billing.
@EndUserText.label: 'Service binding on projection entity'
define service Zsd_sales_order {
expose zc_header_info;
expose zc_item_info;
expose zc_billing_info;
}
Create Service binding and preview the application.
Recommended by LinkedIn
Select one record and check object page.
We can Create, Update and Delete records . RAP framework would be taking care of operations based on composition relation mentioned in behavior definition and on BO.
Virtual Element
A "virtual element" could refer to an element that is not directly stored in the database but is dynamically generated or calculated based on other data elements.
These virtual elements can be exposed through the service to provide additional functionality or information to consumers without the need for explicit storage.
To demonstrate virtual element , we have added virtual element in projection root view (can only be added in projection).
Below annotation is mandatory to define virtual element.
@ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_VIRTUALFL'
Class ZCL_VIRTUALFL would be used to implement business logic of virtual element which will have below interface.
INTERFACES if_sadl_exit_calc_element_read.
Interface will have below two methods.
GET_CALCULATION_INFO - used to collect data before sending to CALCULATE method for actual business logic implementation.
CALCULATE - Actual business Logic
We have created ZCL_VIRTUALFL and wrote sample code to populate value into virtual element.
Activate class and preview the application.
That's how we can make use of virtual element.
SAVE OPTIONS
In managed scenarios, there are three options for setting up the save phase.
we can use the built-in save sequence of the managed scenario, which is the default for any managed RAP BO.
Based on the default save sequence, we can add additional implementations with the additional save.
As an alternative to the save method of the managed save sequence runtime, we can implement our own save method as an unmanaged save.
Managed SAVE
Unmanaged SAVE
Additional SAVE
Unmanaged Save
In SAP RAP "unmanaged save" refers to a concept related to handling data persistence and transactions in a more manual or explicit manner compared to the default managed save operations provided by RAP framework.
Here are the key points to understand about unmanaged save in SAP RAP:
Managed vs. Unmanaged Save
Use Case
We can make use of unmanaged save to make calculation before saving data to DB.
We will be updating discount field of Billing table on the basis of incoming data on Header and Item table with the help of unmanaged save.
We need to make below changes in behavior definition .
1.Remove persistent table
2. mark unmanaged save
Behavior Definition
managed implementation in class zbp_i_header_info unique;
strict(2) ;
with draft;
define behavior for zi_header_info //alias <alias_name>
//persistent table zheader
with unmanaged save
draft table ztdr_header
lock master total etag upd_at
authorization master ( instance )
etag master locallastchangedat
{
create;
update;
delete;
field ( numbering : managed , readonly ) Hdrkey ;
draft action Edit;
draft action Activate;
draft action Discard;
draft action Resume;
draft determine action prepare;
mapping for zheader { Hdrkey = hdrkey;
InvReNo = inv_re_no;
PartNo = part_no;
SalesArea = sales_area;
ShipLoc = ship_loc;
ShipStatus = ship_status;
upd_at = upd_at;
locallastchangedat = locallastchangedat ;
}
association _Item { create; with draft; }
association _billing { create; }
}
define behavior for zi_item_info //alias <alias_name>
//persistent table zitem
draft table ztdr_item
with unmanaged save
lock dependent by _Header
authorization dependent by _Header
{
update;
delete;
// field ( numbering : managed , readonly ) Itmkey;
field ( readonly) hdrkey;
association _Header;
mapping for zitem { Hdrkey = hdrkey;
InvReNo = inv_re_no;
ItemNo = item_no;
ItmStatus = itm_status;
SalesOffice = sales_office;
Uom = uom;
AprQty = apr_qty;
DelvQty = delv_qty;
}
}
define behavior for zi_billing_info //alias <alias_name>
//persistent table zbilling
draft table ztdr_billing
with unmanaged save
lock dependent by _Header
authorization dependent by _Header
{
update;
delete;
// field ( numbering : managed , readonly ) Itmkey;
field ( readonly) hdrkey;
association _Header;
mapping for zbilling{
Hdrkey = hdrkey;
Itmkey = itmkey;
InvReNo = inv_re_no;
ItemNo = item_no ;
Discount = discount;
}
}
Now we need to create new class extending class cl_abap_behavior_saver (Add in Behavior Imp class).
Method SAVE_MODIFIED would be use to write business logic to commit /rollback changes to DB.
Below are the Parameter of Method SAVE_MODIFIED.
For demonstrate purpose , we have hardcoded 99 as discount value . We can write actual business logic and modify DB tables.
CLASS lhc_zi_header_info DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR zi_header_info RESULT result.
ENDCLASS.
CLASS lhc_zi_header_info IMPLEMENTATION.
METHOD get_instance_authorizations.
ENDMETHOD.
ENDCLASS.
CLASS lsc_zi_header_info DEFINITION INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
*
METHODS save_modified REDEFINITION.
ENDCLASS.
CLASS lsc_zi_header_info IMPLEMENTATION.
METHOD save_modified.
data: lit_header type table of zheader,
lit_item type table of zitem,
lit_billing type table of zbilling,
wa_billing type zbilling.
IF create IS NOT INITIAL.
lit_header = CORRESPONDING #( create-zi_header_info MAPPING FROM ENTITY ).
modify zheader FROM TABLE @lit_header.
lit_item = CORRESPONDING #( create-zi_item_info MAPPING FROM ENTITY ).
modify zitem FROM TABLE @lit_item .
*
lit_billing = CORRESPONDING #( create-zi_billing_info MAPPING FROM ENTITY ).
lit_billing[ 1 ]-discount = 99.
MODIFY zbilling FROM TABLE @lit_billing .
ENDIF.
if update is not INITIAL.
ENDIF.
...
ENDMETHOD.
ENDCLASS.
We can put breakpoint and debug in case of issue.
Additional Save
Additional save refers to implementing custom logic or enhancements that occur in addition to the standard save operations provided by SAP RAP.
In certain situations, an application may need to call an external function during the process of saving data. This should happen after the system has collected the updated information for business objects but before it completes the final save operation.
Additional Save - Triggers after standard save performed by framework
Unmanaged Save - There is no standard save, whole control is on developer hand.
In order to integrate the additional save into the save sequence as a part of the managed runtime, we must first add the corresponding syntax to the behavior definition and then implement the saver handler method as a part of the behavior pool.
Both save options( additional and unmanaged save ) make use of class cl_abap_behavior_saver.
Code implementation of additional save is exactly same as we did to handle unmanaged save.
Sample code of inheriting CL_ABAP_BEHAVIOR_SAVER .
CLASS lcl_saver DEFINITION INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
METHODS save_modified REDEFINITION.
ENDCLASS.
CLASS lcl_saver IMPLEMENTATION.
METHOD save_modified.
IF CREATE-EntityName IS NOT INITIAL.
" Provide table of instance data of all instances that have been created during current transaction
" Use %CONTROL to get information on what entity fields have been set when creating the instance
ENDIF.
IF UPDATE-EntityName IS NOT INITIAL.
" Provide table of instance data of all instances that have been updated during current transaction
" Use %CONTROL to get information on what entity fields have been updated
ENDIF.
IF DELETE-EntityName IS NOT INITIAL.
" Provide table with keys of all instances that have been deleted during current transaction
" NOTE: There is no information on fields when deleting instances
ENDIF.
ENDMETHOD.
...
ENDCLASS.
Thanks for Reading!
Abap on Hana|| RAP || SAP Certified Associate - back-end developer - ABAP Cloud
7moVery helpful 🙌🏼🙌🏼🙌🏼.but I need full application code for that example.
SAP Technical Consultant | S/4 HANA and SAP UI5/Fiori | On Premise and Cloud | SAP Certified Development Specialist | MBA
7moGreat article. Many thanks for your effort 👍
ABAP || Javascript || Life Coach
8moWith a quick look, I got this : Association is between same level entities and Composition is that one entity owns other. Especially in Composition concept e.g. a personal record deleted rest of its dependents should be deleted. Great blog, thanks for the contribution to the community. 🤙
SAP ABAP|RAP|Workflow consultant
8moVery helpful!