ABAP Cloud - Migration (example)
In this article we will take an example of migrating a report from TIER-3 to ABAP Cloud.
Table of contents
In the last few articles we mainly looked at the theory of ABAP Cloud, how the 3-TIER model is structured and how software components contribute to the structure. This article is about practice.
Introduction
In today's example we are using a simple report that we want to migrate to the ABAP Cloud. Reports can be migrated in two directions because they perform two tasks in classic ABAP:
- Online execution with result
- Batch execution for background processing
For the example shown and the test data, we use a Cloud Appliance Library from SAP in version S/4 HANA 2022. If you would like to recreate our example, you will find the corresponding test data there.
Use Case
Therefore, we must first determine what the intended purpose is. In our example, we use the report for background processing and usually schedule this as a job at night. The report looks like this, with only a rudimentary implementation and output available:
REPORT zfi_book_finance_document.
START-OF-SELECTION.
DATA(go_document) = NEW zcl_book_fi_document_app( ).
DATA(gs_document) = go_document->book( ).
IF gs_document IS INITIAL.
cl_demo_output=>display( go_document->mo_log->get_messages_flat( ) ).
ELSE.
cl_demo_output=>display( go_document->get_document( gs_document ) ).
ENDIF.
This calls a global class for processing that contains our actual logic. There is a method to create the FI documents and a method that reads the document based on the data and returns the information.
CLASS zcl_book_fi_document_app DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ts_document,
belnr TYPE bkpf-belnr,
bukrs TYPE bkpf-bukrs,
gjahr TYPE bkpf-gjahr,
END OF ts_document.
TYPES ts_header TYPE bkpf.
DATA mo_log TYPE REF TO zif_application_log READ-ONLY.
METHODS constructor.
METHODS book
RETURNING VALUE(rs_result) TYPE ts_document.
METHODS get_document
IMPORTING is_document TYPE ts_document
RETURNING VALUE(rs_result) TYPE ts_header.
ENDCLASS.
CLASS zcl_book_fi_document_app IMPLEMENTATION.
METHOD constructor.
mo_log = zcl_application_log=>create( ).
ENDMETHOD.
METHOD book.
DATA ls_header TYPE bapiache09.
DATA lt_accountgl TYPE STANDARD TABLE OF bapiacgl09.
DATA lt_currencyamount TYPE STANDARD TABLE OF bapiaccr09.
DATA ld_key TYPE bapiache09-obj_key.
DATA lt_return TYPE zif_application_log=>tt_message.
ls_header = VALUE #( comp_code = '1710'
doc_type = 'SA'
doc_date = '20240101'
pstng_date = sy-datum
ref_doc_no_long = |Test: { sy-uname }|
username = sy-uname ).
lt_accountgl = VALUE #( ( itemno_acc = 1 gl_account = '0011002080' )
( itemno_acc = 2 gl_account = '0070200000' costcenter = '0017101101' ) ).
lt_currencyamount = VALUE #( currency = 'EUR'
( itemno_acc = 1 amt_doccur = '65.99' )
( itemno_acc = 2 amt_doccur = '-65.99' ) ).
CALL FUNCTION 'BAPI_ACC_DOCUMENT_POST'
EXPORTING
documentheader = ls_header
IMPORTING
obj_key = ld_key
TABLES
accountgl = lt_accountgl
currencyamount = lt_currencyamount
return = lt_return.
mo_log->add_msg_bapi( it_bapiret = lt_return ).
IF line_exists( lt_return[ type = 'E' ] ) OR line_exists( lt_return[ type = 'W' ] ).
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ELSE.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'.
rs_result = ld_key.
ENDIF.
ENDMETHOD.
METHOD get_document.
SELECT SINGLE FROM bkpf
FIELDS *
WHERE bukrs = @is_document-bukrs
AND belnr = @is_document-belnr
AND gjahr = @is_document-gjahr
INTO @rs_result.
ENDMETHOD.
ENDCLASS.
We use the function module BAPI_ACC_DOCUMENT_POST to generate the documents and the table BKPF to read the information.
Basic structure
In the first step we build the basic structure of the three TIERs. At the end the structure should look like our sketch.
Build TIER-1
In the first step, we build the basic structure as we described in the last article. We had already created the software component, which uses “ABAP for Cloud”. To do this, we now create the appropriate structure package with the same name.
In the second step, we assign the software component and the corresponding transport layer. Since we are traveling on the CAL system here and are not transporting, we leave the layer empty.
The TIER-1 package now looks like this, this is a structure package with the software component ZFI_T1_BNK and the language version "ABAP for Cloud Development", which it receives from the settings of the software component.
At the end of the first TIER, we create a development package where we will later map our development.
Build TIER-2
In the second TIER, for simplicity, we create a development package with the name of the software component and label it as TIER-2. Here you can also follow the pattern from TIER-1 and create the appropriate structure package first, but it would be a little more work. This way you can derive the assignment to the appropriate software component.
Since we created the package under the package ZCA_TIER2, the appropriate software component is pre-assigned. However, the component uses the language version “Standard ABAP” because we want to create our non-released wrappers here.
Logic migration
In the first step we create a new class that adopts the logic of the old class. We create an executable class here so that we can quickly see the first results and take over the report logic.
We adopt the logic from the report into the MAIN method. The class must be replaced by the new class. We also need to change the output slightly so that it now uses the OUT object of the MAIN method.
DATA(go_document) = NEW zcl_finance_document_new( ).
DATA(gs_document) = go_document->book( ).
IF gs_document IS INITIAL.
out->write( go_document->mo_log->get_messages_flat( ) ).
ELSE.
out->write( go_document->get_document( gs_document ) ).
ENDIF.
However, now that we have adopted all the logic from the ZCL_BOOK_FI_DOCUMENT_APP class, we cannot activate it yet and receive numerous error messages.
Neither the BAPI nor the table are released for use in ABAP Cloud. The compiler won't let us activate the class and we have to deal with the errors first.
Wrapper
Now how can we read the BKPF data? If there is no released API from SAP, we can build a wrapper.
Core Data Service
To do this, we create a core data service for the table. It is important here that we create the wrapper in TIER-2.
We create a simple structure with a few fields from the BKPF. If we were to create a real wrapper, we would also convert the field names to the English long names so that we bring the object closer to the standard.
@EndUserText.label: 'FI Header (Wrapper)'
define view entity ZFI_I_FinanceDocumentHeader
as select from bkpf
{
key bukrs,
key gjahr,
key belnr,
blart,
bldat,
budat,
cpudt,
cputm,
wwert,
usnam,
tcode,
bvorg,
xblnr,
dbblg,
stblg,
stjah,
bktxt,
waers,
kursf
}
After we have created and activated the CDS, we still have to release it so that we can use it in TIER-1. In Eclipse you will find the appropriate button in the “Properties” view in the “API State” tab.
We want to create a C1 Contract and release the object for "Use in Cloud Development" to be able to use it in our ABAP Cloud application. The status should be “Released”.
We can then validate the “API State” again and now see the new contract for the object.
Replacing the table
Now we can start replacing the BKPF calls with the new Core Data Service in the ZCL_FINANCE_DOCUMENT_NEW class.
TYPES: BEGIN OF ts_document,
belnr TYPE ZFI_I_FinanceDocumentHeader-belnr,
bukrs TYPE ZFI_I_FinanceDocumentHeader-bukrs,
gjahr TYPE ZFI_I_FinanceDocumentHeader-gjahr,
END OF ts_document.
TYPES ts_header TYPE ZFI_I_FinanceDocumentHeader.
Finally, we can now adjust the SELECT to get a result in the class again and thus get rid of the error messages.
SELECT SINGLE FROM ZFI_I_FinanceDocumentHeader
FIELDS *
WHERE bukrs = @is_document-bukrs
AND belnr = @is_document-belnr
AND gjahr = @is_document-gjahr
INTO @rs_result.
BAPI
In the last step we want to get rid of the BAPI. Here we could also build a wrapper, but first we should check whether there is already a successor object.
Successor
Using the Cloudification Browser or the Cloudification Repository Viewer, we can check whether there is already a defined successor for the BAPI_ACC_DOCUMENT_POST function module and we will find it:
This is the RAP object I_JOURNALENTRYTP, which replaces the BAPI.
Documentation
How does the RAP object actually work? SAP provides documentation here via Knowledge Transfer Document (KTD). If we look at the behavior for our RAP object I_JOURNALENTRYTP, we will see a link to navigation in the upper part.
By clicking on "Open Documentation" we load the KTD, here we can view the documentation for the object. The object is divided into different areas, we are particularly interested in the area of the “POST” and “REVERSE” campaigns, for booking and cancellation.
In this case, the documentation gives us a nice overview of the structure of the call and some examples of how to implement the object in the code.
RAP Object
Let's implement the RAP object to replace the BAPI in our class. The POST is an action, which means that before calling it we first have to fill the structure with all the data, which we then pass on to the RAP object.
DATA lt_new TYPE TABLE FOR ACTION IMPORT i_journalentrytpJournalEntry~Post.
INSERT VALUE #(
%cid = to_upper( cl_uuid_factory=>create_system_uuid( )->create_uuid_x16( ) )
%param = VALUE #(
companycode = '1710'
createdbyuser = sy-uname
accountingdocumenttype = 'SA'
documentdate = '20240101'
postingdate = cl_abap_context_info=>get_system_date( )
DocumentReferenceID = |RAP: { sy-uname }|
_glitems = VALUE #(
( glaccountlineitem = '1' glaccount = '0011002080' _currencyamount = VALUE #( ( journalentryitemamount = '-100.55' currency = 'EUR' ) ) )
( glaccountlineitem = '2' glaccount = '0070200000' _currencyamount = VALUE #( ( journalentryitemamount = '100.55' currency = 'EUR' ) ) ) ) ) )
INTO TABLE lt_new.
We define an internal table with the data type of the action and then fill the required structure. We fill the %CID with a unique ID for our process, and we then enter the contents of the booking via %PARAM. This means that all data is in the internal table. Now we can call the action on the RAP object.
MODIFY ENTITIES OF i_journalentrytp
ENTITY journalentry
EXECUTE post FROM lt_new
FAILED DATA(ls_failed_deep)
REPORTED DATA(ls_reported_deep).
If we receive an error message, the FAILED structure is filled and we can transfer the error messages to our log via REPORTED.
LOOP AT ls_reported_deep-journalentry INTO DATA(ls_entry).
mo_log->add_msg_text( ls_entry-%msg->if_message~get_text( ) ).
ENDLOOP.
If everything has gone through the check cleanly, then we can have the document created in the database. To do this, we have to call a COMMIT ENTIRIES and get the document number created again via REPORTED.
COMMIT ENTITIES BEGIN
RESPONSE OF i_journalentrytp
REPORTED DATA(lt_commit_reported).
DATA(ls_commit) = lt_commit_reported-journalentry[ 1 ].
rs_result-belnr = ls_commit-AccountingDocument.
rs_result-bukrs = ls_commit-CompanyCode.
rs_result-gjahr = ls_commit-FiscalYear.
COMMIT ENTITIES END.
Structure
Now that all objects have been activated, the architecture should look like this. The class now exists in TIER-1 and we have a wrapper on TIER-2 that makes the table available for TIER-1 to read.
Final class
To compare, the final class with all changes and extensions. The class could now be activated and can be used in TIER-1.
CLASS zcl_finance_document_new DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
TYPES: BEGIN OF ts_document,
belnr TYPE ZFI_I_FinanceDocumentHeader-belnr,
bukrs TYPE ZFI_I_FinanceDocumentHeader-bukrs,
gjahr TYPE ZFI_I_FinanceDocumentHeader-gjahr,
END OF ts_document.
TYPES ts_header TYPE ZFI_I_FinanceDocumentHeader.
DATA mo_log TYPE REF TO zif_application_log READ-ONLY.
METHODS constructor.
METHODS book
RETURNING VALUE(rs_result) TYPE ts_document.
METHODS get_document
IMPORTING is_document TYPE ts_document
RETURNING VALUE(rs_result) TYPE ts_header.
ENDCLASS.
CLASS zcl_finance_document_new IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA(go_document) = NEW zcl_finance_document_new( ).
DATA(gs_document) = go_document->book( ).
IF gs_document IS INITIAL.
out->write( go_document->mo_log->get_messages_flat( ) ).
ELSE.
out->write( go_document->get_document( gs_document ) ).
ENDIF.
ENDMETHOD.
METHOD constructor.
mo_log = zcl_application_log=>create( ).
ENDMETHOD.
METHOD book.
DATA lt_new TYPE TABLE FOR ACTION IMPORT i_journalentrytpJournalEntry~Post.
INSERT VALUE #(
%cid = to_upper( cl_uuid_factory=>create_system_uuid( )->create_uuid_x16( ) )
%param = VALUE #(
companycode = '1710'
createdbyuser = sy-uname
accountingdocumenttype = 'SA'
documentdate = '20240101'
postingdate = cl_abap_context_info=>get_system_date( )
DocumentReferenceID = |RAP: { sy-uname }|
_glitems = VALUE #(
( glaccountlineitem = '1' glaccount = '0011002080' _currencyamount = VALUE #( ( journalentryitemamount = '-100.55' currency = 'EUR' ) ) )
( glaccountlineitem = '2' glaccount = '0070200000' _currencyamount = VALUE #( ( journalentryitemamount = '100.55' currency = 'EUR' ) ) ) ) ) )
INTO TABLE lt_new.
MODIFY ENTITIES OF i_journalentrytp
ENTITY journalentry
EXECUTE post FROM lt_new
FAILED DATA(ls_failed_deep)
REPORTED DATA(ls_reported_deep).
IF ls_failed_deep IS INITIAL.
COMMIT ENTITIES BEGIN
RESPONSE OF i_journalentrytp
REPORTED DATA(lt_commit_reported).
DATA(ls_commit) = lt_commit_reported-journalentry[ 1 ].
rs_result-belnr = ls_commit-AccountingDocument.
rs_result-bukrs = ls_commit-CompanyCode.
rs_result-gjahr = ls_commit-FiscalYear.
COMMIT ENTITIES END.
ELSE.
LOOP AT ls_reported_deep-journalentry INTO DATA(ls_entry).
mo_log->add_msg_text( ls_entry-%msg->if_message~get_text( ) ).
ENDLOOP.
ENDIF.
ENDMETHOD.
METHOD get_document.
SELECT SINGLE FROM ZFI_I_FinanceDocumentHeader
FIELDS *
WHERE bukrs = @is_document-bukrs
AND belnr = @is_document-belnr
AND gjahr = @is_document-gjahr
INTO @rs_result.
ENDMETHOD.
ENDCLASS.
Application Log
In our example we are already using an ABAP Cloud Application Log with the new classes. That's why we don't even re-describe the logic in this example. If you build a TIER-1 re-use component with the new classes, then you can also use it normally in your TIER-3 applications.
Hint: Currently TIER-3 components and applications can use any TIER-1 object, there is no review of the C1 contract.
Application Job
In the next step we want to create a job because our old report was used as a job. For this we use the new concept of application jobs.
Creation
In the first step, we create a new subpackage to have all job-relevant objects in one package.
As a second step, we need the actual job class, which is started when the job is executed. This class requires the two interfaces IF_APJ_DT_EXEC_OBJECT and IF_APJ_RT_EXEC_OBJECT.
Now we can implement the actual logic in the EXECUTE method. We call up our document class, book the FI document, output an additional message to the log and then save the message to the job. We don't need GET_PARAMETERS in our example because we don't expect any input from outside. The complete job class now looks like this:
CLASS zcl_finance_job DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_apj_dt_exec_object.
INTERFACES if_apj_rt_exec_object.
ENDCLASS.
CLASS zcl_finance_job IMPLEMENTATION.
METHOD if_apj_dt_exec_object~get_parameters.
ENDMETHOD.
METHOD if_apj_rt_exec_object~execute.
DATA(go_document) = NEW zcl_finance_document_new( ).
DATA(gs_document) = go_document->book( ).
go_document->mo_log->add_msg_text(
|BELNR: { gs_document-belnr }, BUKRS: { gs_document-bukrs }, GJAHR: { gs_document-gjahr }| ).
DATA(lo_header) = cl_bali_header_setter=>create(
object = 'ZFI_LOG'
subobject = 'DOC'
external_id = cl_abap_context_info=>get_system_date( ) && cl_abap_context_info=>get_system_time( ) ).
go_document->mo_log->save( id_save_with_job = abap_true
io_header = lo_header ).
ENDMETHOD.
ENDCLASS.
Job
In the next step we have to create the catalog and the template for the job. If you are looking for a job, you will find the two objects in the system.
First we need the catalog and assign the class there.
In the next step we generate the template and use the catalog from the previous step as a reference.
Test
Let's test the new job and go to the "Application Jobs" app, where we schedule the job once and look at the result in the log.
In the second step, we look at the generated document via the Core Data Service in Eclipse (Data Preview).
The test has been successful so far, the components in ABAP Cloud work and give us the desired result.
Structure
The new structure looks like this with the new objects, although we have now left TIER-3 empty because all objects have been successfully migrated to TIER-1 and 2.
Move wrapper
In the last step we want to get rid of the TIER-2 wrapper. Here we assume that a released object has been delivered. In this case, there is a corresponding shared Core Data Service for the table that we can now use. Such releases are currently done by upgrading the system, but in the future this may also be done by notification if releases are only available every two years.
Successor
The successor to the BKPF can currently be determined in two ways. Firstly, you can open the table in the ABAP Development Tools and navigate to the "Properties" view. Here you will find the information under "API State" if it is stored in the system is.
If you still have an older release and there is no information about a successor, you can get the information via the Cloudification Repository. For this there is the viewer from SAP and a viewer from us. In both we get the successor for the table.
Exchange
Since there is now a successor to our wrapper, we have two options in this case:
- Replace wrapper - We replace our wrapper at all call points and then delete the wrapper because it is no longer needed.
- Adjust Wrapper - We adjust our wrapper and use the shared object instead of directly the table like before.
Here you should differentiate from case to case. The more often the wrapper is used, the more worthwhile it is to use variant 2 and only change the contents of the wrapper once. In this example we also use the second variant and only swap the content. When changing the shared object, we get a warning, but we just have to confirm it.
Now we change the data source in the Core Data Service and now have to map the English long names to the German short names. In this case, the mapping beforehand would have made a lot more sense.
define view entity ZFI_I_FinanceDocumentHeader
as select from I_JournalEntry
{
key CompanyCode as bukrs,
key AccountingDocument as belnr,
key FiscalYear as gjahr,
AccountingDocumentType as blart,
DocumentDate as bldat,
PostingDate as budat,
FiscalPeriod as monat,
AccountingDocumentCreationDate as cpudt,
CreationTime as cputm,
LastManualChangeDate as aedat,
LastAutomaticChangeDate as upddt,
ExchangeRateDate as wwert,
AccountingDocCreatedByUser as usnam,
TransactionCode as tcode,
IntercompanyTransaction as bvorg,
DocumentReferenceID as xblnr,
RecurringAccountingDocument as dbblg,
ReverseDocument as stblg,
ReverseDocumentFiscalYear as stjah,
AccountingDocumentHeaderText as bktxt,
TransactionCurrency as waers,
ExchangeRate as kursf
}
Finally, just activate the object.
Move
Since our wrapper is now ready for ABAP Cloud, we can move it to a corresponding TIER-1 package. However, before we can move it, we need to change the language version of the object. In the "Properties" view in the "General" tab we can adjust the language version using the "Edit" button and then activate the object again.
By right-clicking on the object in the "Project Explorer" you can select the "Change Package Assignment..." function in the menu.
In the subsequent dialog, just enter the target package and carry out the action. If the object is ABAP Cloud Ready, it will be moved accordingly and we are done.
Structure
TIER-2 is now also empty and our application was completely implemented with ABAP Cloud. It won't always be that easy; the second TIER can be more or less large, depending on which module is used. In some cases it can also happen that objects from TIER-2 never come to TIER-1 and always remain there.
Summary
What we have done to migrate the coding, here is a brief summary.
- Creating the structure for the TIER-1 components
- Creation of a wrapper and release for TIER-1
- Migration of coding and replacement of objects that have not been released
- Creation of the job
- Migration of the wrapper (possible after release)
Conclusion
The example is kept relatively simple in order to illustrate the various steps as simply as possible. They are intended to give you an insight into working with ABAP Cloud and give you a practical insight into the topic.