【ABAP】Background Processing Framework (bgPF) でRAPから非同期処理を起動する

## はじめに
RAPのBehaviorロジックの中から時間のかかる処理を実行するとき、呼び出し元にはすぐに結果を返し、バックグラウンドで非同期に処理を行いたい場合があります。
これまで、RAPから非同期処理を開始する方法としては、[Application Job](https://help.sap.com/docs/SAP_S4HANA_CLOUD/6aa39f1ac05441e5a23f484f31e477e7/0837d1ea0b0b4d3892f66e8533b654cb.html?locale=en-US)（※1）がありました。これはt-code: SM36で定義するジョブのABAP Cloud版です（※2）。クラス（API）を使用してプログラムからジョブを起動することができます。

Background Processing Framework (bgPF) は昨年登場した新しい方法で、S/4HANA On-Premiseでは2023から使うことができます。bgPFは、従来からある技術のバックグラウンドリモートファンクションコール（bgRFC）を利用しやすくするためのAPIを提供します。技術的な詳細については[こちら](https://blogs.sap.com/2023/12/14/introducing-the-background-processing-framework/)のブログご参照ください。

この記事では、bgPFの概要とRAPへの組み込み方について紹介します。

※1 RAPでApplication Jobを使用した非同期処理の起動方法については、以下のリポジトリを参照
https://github.com/SAP-samples/abap-platform-application-jobs
※2 S/4HANA On-Premiseでも使うことができる

## bgPFについて
### bgPFは何がうれしいか
Application Jobには、以下のような難点がありました。
- ジョブカタログエントリ、ジョブテンプレートなどのオブジェクトを事前に作成しておく必要がある
- ジョブが受け取れるパラメータが、単一項目またはSELECT-OPTION型に限定されている

bgPFの場合、非同期に呼ばれる処理をクラスとして作成しておくだけでよく、Application Jobと比べて簡単に非同期処理を実装できます。また、渡せるパラメータの種類に制限はなく、テーブル型を渡すことも可能です。

### bgPFで非同期処理を開始するために必要なもの
bgPFで非同期処理を開始するために必要なものはいたってシンプルで、事前に非同期処理を行うクラスを作成しておき、それをRAPまたはその他のロジックから呼び出すだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/071a270a-93ff-ab94-c78c-e66831e3099d.png)

### bgPFのシナリオ
bgPFを利用するシナリオには2種類あり、一つがRAPの中で使用する**Transactional Controlled**シナリオ、もう一つがRAP以外から使用する**Transactional Uncontrolled**シナリオです。
Transactional Controlledは、RAPのトランザクションコントロールを使用してトランザクションの整合性を担保したいときに使用します。このために、非同期処理を行うクラスの中でトランザクションのフェーズ（modifyフェーズまたはsaveフェーズ）を宣言するという特徴があります。
Transactional Uncontrolledは、RAPを使用しない場合、または厳格なトランザクションの制御が不要な場合に使用します。この場合、非同期処理を行うクラスの中でのフェーズの宣言は不要です。

この記事では、RAPから呼び出すためTransactional Controlledシナリオを使用します。Transactional Uncontrolledでは利用するAPIが変わりますが、基本的な流れは同じです。

## bgPFでRAPから非同期処理を起動する
ここからは、実際にRAPのBehaviorロジックの中からどのようにbgPFを呼び出すかについて、実装で確認します。環境はBTPトライアル環境のABAP Environmentを使用しています。

### シナリオ
/DMO/I_FlightというCDS Viewは、航空便の最大座席数（MaximumSeats）と予約済み座席数（OccupiedSeats）を持っています。このビューと同じキーを持つカスタムテーブルを作成し、そこに座席の予約率を計算した結果を格納します。予約率の計算を「時間がかかる処理」と仮定し、bgPFを使って実行します。オブジェクトの構成は以下のようになります。

図中の番号は以下の実装ステップと対応します。番号のないオブジェクトについては、bgPFの実装と直接関係ないためAppendixに掲載します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c5dc8e81-95c5-9cec-7e22-30bd71f200b7.png)

### 実装ステップ
1. テーブル定義
1. CDS View定義
1. Behavior Definition定義
1. bgPFから呼び出されるクラスの定義
1. Behavior Implementationクラス実装

#### 1. テーブル定義
以下のテーブルを定義します。このテーブルには座席の使用率、数量単位（%）、計算ステータスを格納します。
```abap:zflightoccupancy
@EndUserText.label : 'Flight Occupancy'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zflightoccupancy {

  key client        : abap.clnt not null;
  key carrier_id    : /dmo/carrier_id not null;
  key connection_id : /dmo/connection_id not null;
  key flight_date   : /dmo/flight_date not null;
  @Semantics.quantity.unitOfMeasure : 'zflightoccupancy.occupancy_unit'
  occupancy_rate    : abap.quan(5,2);
  occupancy_unit    : abap.unit(3);
  calc_status       : zde_calc_status;

}
```

#### 2. CDS View定義
ステップ1で作成したテーブルと/DMO/I_Flightを結合したビューを作成します。計算ステータスに色をつけて表示するため、Criticalityを計算項目として定義しています。（Projection Viewではcase文が使えないため、ベースのViewで項目を作成）

```abap:ZI_FlightOccupancy
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Flight Occupancy'
define root view entity ZI_FlightOccupancy as select from /DMO/I_Flight as flight
left outer many to one join zflightoccupancy as occupancy
on flight.AirlineID = occupancy.carrier_id
and flight.ConnectionID = occupancy.connection_id
and flight.FlightDate = occupancy.flight_date
{
    key flight.AirlineID,
    key flight.ConnectionID,
    key flight.FlightDate,
    flight.Price,
    flight.CurrencyCode,
    flight.PlaneType,
    flight.MaximumSeats,
    flight.OccupiedSeats,
    occupancy.calc_status as CalcStatus,
    @EndUserText.label: 'Calculation Status'
    case occupancy.calc_status
      when '1' then 'Processing'
      when '2' then 'Done' 
      when '3' then 'Error' 
      else 'None'
      end as CalcStatusDisp,    
    case occupancy.calc_status
      when '1' then 2 //processing: warning
      when '2' then 3 //done: positive
      when '3' then 1 //error: critical
      else 0
      end as Criticality,
    @Semantics.quantity.unitOfMeasure: 'OccupancyUnit'
    @EndUserText.label: 'Occupancy Rate'
    occupancy.occupancy_rate as OccupancyRate,  
    occupancy.occupancy_unit as OccupancyUnit,    
    /* Associations */
    flight._Airline,
    flight._Connection,
    flight._Currency
}

```

#### 3. Behavior Definition定義
今回のシナリオではCRUD処理によってテーブルを更新するのではなく、アクションを介して更新するため**unmanaged save**を使用します。create, delete処理は使用しないためコメントアウトします。アクションcalculateOccupacyRateで座席の予約率を計算します。

```abap:ZI_FlightOccupancy
managed implementation in class zbp_i_flightoccupancy unique;
strict ( 2 );

define behavior for ZI_FlightOccupancy alias FlightOccupacy
with unmanaged save
lock master
authorization master ( instance )
{
  //  create;
  update;
  //  delete;

  action calculateOccupacyRate result [1] $self;
}
```

#### 4. bgPFから呼び出されるクラスの定義
予約率の計算、テーブル更新を行うクラスを定義します。

クラスの定義部分は以下のようになります。重要な点は、インターフェース`if_bgmc_op_single`を使用することです。ADTでクラスを定義するときに、使用するインターフェースとしてif_bgmc_op_singleを指定すると、残りのインターフェースは自動的に入ってきます。
このクラスでは、コンストラクタに計算対象となる航空便のキー（carrier_id, connection_id, flight_date）を受け取り、インスタンス変数（m_flight_keys）に格納します。

```abap:zcl_flight_operation_contr
CLASS zcl_flight_operation_contr DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    TYPES: BEGIN OF ty_flight_keys,
             carrier_id    TYPE /dmo/carrier_id,
             connection_id TYPE /dmo/connection_id,
             flight_date   TYPE /dmo/flight_date,
           END OF ty_flight_keys.

    INTERFACES if_serializable_object .
    INTERFACES if_bgmc_operation .
    INTERFACES if_bgmc_op_single .

    METHODS constructor
      IMPORTING i_flight_keys TYPE ty_flight_keys.

  PROTECTED SECTION.
  PRIVATE SECTION.
    DATA m_flight_keys TYPE ty_flight_keys.
ENDCLASS.
```

クラスの実装部分は以下のようになります。
メソッド`if_bgmc_op_single~execute`で、非同期に実行される処理を実装します。ポイントは、更新処理を行う前に`cl_abap_tx=>save( )`を実行することです（この一文がないとMODIFY処理が実行されない）。
cl_abap_tx=>save( )は、以降の処理が更新を伴うものであることを宣言します。これはTransactional Controlledシナリオで、トランザクションの整合性を担保するために必要な宣言です。

```abap
CLASS zcl_flight_operation_contr IMPLEMENTATION.
  METHOD constructor.
    m_flight_keys = i_flight_keys.
  ENDMETHOD.


  METHOD if_bgmc_op_single~execute.
    "時間のかかる処理を再現
    WAIT UP TO 10 SECONDS.
    
    cl_abap_tx=>save( ).

    SELECT SINGLE MaximumSeats,
                  OccupiedSeats
    FROM /DMO/I_Flight
    WHERE AirlineID = @m_flight_keys-carrier_id
      AND ConnectionID = @m_flight_keys-connection_id
      AND FlightDate = @m_flight_keys-flight_date
    INTO @DATA(flight).

    IF sy-subrc <> 0 OR flight-MaximumSeats = 0.
      MODIFY zflightoccupancy FROM @( VALUE #( carrier_id = m_flight_keys-carrier_id
                                               connection_id = m_flight_keys-connection_id
                                               flight_date = m_flight_keys-flight_date
                                               occupancy_rate = 0
                                               occupancy_unit = '%'
                                               calc_status = '3' ) ).
      RETURN.
    ENDIF.

    MODIFY zflightoccupancy FROM @( VALUE #( carrier_id = m_flight_keys-carrier_id
                                             connection_id = m_flight_keys-connection_id
                                             flight_date = m_flight_keys-flight_date
                                             occupancy_rate = flight-OccupiedSeats / flight-MaximumSeats * 100
                                             occupancy_unit = '%'
                                             calc_status = '2' ) ).

  ENDMETHOD.
ENDCLASS.
```
※当初、EMLを使って以下のように更新をしようとしたのですが、処理が終わらずテーブルも更新されないという結果になりました。RAPを使った更新をかけようとしたことで、bgPFが無限に呼び出されることになったのではないかと推測しています。
```abap
    modify ENTITIES OF ZI_FlightOccupancy
      ENTITY FlightOccupacy
      UPDATE FIELDS ( OccupancyRate CalcStatus )
      with value #( ( AirlineID = m_flight_keys-carrier_id
                      ConnectionID = m_flight_keys-connection_id
                      FlightDate = m_flight_keys-flight_date
                      OccupancyRate = flight-OccupiedSeats / flight-OccupiedSeats * 100
                      CalcStatus = '2'  ) ).
 ```          

 #### 5. Behavior Implementationクラス実装
Behavior Implementationクラスでは2つの処理を実装します。一つ目はアクションの実行、二つ目は保存処理です。unmanaged saveを使用しているため、保存処理はsave_modifiedというメソッドの中で行います。

アクションの実装は以下のようになります。アクションでは、CalcStatus（計算ステータス）に処理中を表すステータスを設定して結果を返します。この時点ではまだDBは更新されません。

 ```abap
 CLASS lhc_FlightOccupacy DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.

    METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
      IMPORTING keys REQUEST requested_authorizations FOR FlightOccupacy RESULT result.
    METHODS calculateoccupacyrate FOR MODIFY
      IMPORTING keys FOR ACTION flightoccupacy~calculateoccupacyrate RESULT result.



ENDCLASS.

CLASS lhc_FlightOccupacy IMPLEMENTATION.

  METHOD get_instance_authorizations.
  ENDMETHOD.



  METHOD calculateOccupacyRate.
    "set status
    MODIFY ENTITIES OF ZI_FlightOccupancy IN LOCAL MODE
      ENTITY FlightOccupacy
      UPDATE FIELDS ( calcStatus )
      WITH VALUE #( FOR status IN keys ( %tky = status-%tky
                                         CalcStatus = 1 ) ).

    "return result
    READ ENTITIES OF ZI_FlightOccupancy IN LOCAL MODE
      ENTITY FlightOccupacy
      ALL FIELDS WITH CORRESPONDING #( keys )
      RESULT DATA(occupacy).

    result = VALUE #( FOR data IN occupacy ( %tky = data-%tky
                                             %param = data ) ).
  ENDMETHOD.

ENDCLASS.
```

保存処理の実装は以下のようになります。ここでbgPFが登場します。
はじめに、アクションで更新したステータスと予約率の初期値をDBに保存します。次に、bgPFを使用して計算処理を実行します。`lo_process->save_for_execution( )`によって処理が起動されます。
なお、bgPFの実行で例外が発生する場合がありますが、ここではキャッチするだけで特に何もしていません。save_modifiedメソッドはエラーを返すことができないため、起こりうるエラーはバリデーションなどであらかじめつぶしておくことが必要です。

```abap
CLASS lsc_ZI_FLIGHTOCCUPANCY IMPLEMENTATION.

  METHOD save_modified.
    DATA lo_operation TYPE REF TO if_bgmc_op_single.
    DATA lo_process TYPE REF TO if_bgmc_process_single_op.
    DATA lo_process_monitor TYPE REF TO if_bgmc_process_monitor.

    LOOP AT update-flightoccupacy INTO DATA(occupacy).
      "persist status
      MODIFY zflightoccupancy FROM @( VALUE #( carrier_id = occupacy-AirlineID
                    connection_id = occupacy-ConnectionID
                    flight_date = occupacy-FlightDate
                    calc_status = occupacy-CalcStatus
                    occupancy_rate = 0
                    occupancy_unit = '%' ) ).

      "trigger bgPF
      lo_operation = NEW zcl_flight_operation_contr( i_flight_keys = VALUE #( carrier_id = occupacy-AirlineID
                                                                              connection_id = occupacy-ConnectionID                                                                      
                                                                              flight_date = occupacy-FlightDate ) ).
      TRY.
          lo_process = cl_bgmc_process_factory=>get_default( )->create( ).
          lo_process->set_name( 'Calc Flight Occupancy Test'
                   )->set_operation( lo_operation ).
          lo_process_monitor = lo_process->save_for_execution( ).
        CATCH cx_bgmc INTO DATA(lx_bgmc).
      ENDTRY.

    ENDLOOP.


  ENDMETHOD.

  METHOD cleanup_finalize.
  ENDMETHOD.

ENDCLASS.
```

### 処理の実行と監視
bgPFで起動した処理は、ADTのABAP Cross-Tracerというツールによって監視することができます。このために、あらかじめトレースを有効化しておく必要があります。

Window > Show view > ABAP Cross Trace を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/53866501-2d63-514a-08e0-383772058a99.png)

プロジェクトを右クリックし、"**Create Configuration**"を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d5b11ee9-33a3-9d6d-41ac-1c92d13d86d5.png)

"**Background Processing**"にチェックが入っていることを確認して"OK"をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2580a081-f19b-d9ae-73e1-689b29200600.png)

Service Bindingのプレビューから処理対象の行を選択して、"Calculate"アクションを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/75162b15-2712-2a4e-53c1-6a10b183b2de.png)

ステータスが"Processing"になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/bd5711f9-1948-86ce-ef57-1085eb5c5df7.png)

Trace Resultsタブを見ると、Request Entry Typeが"Remote Function Call"の行ができています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b1db2aa9-a69d-d03a-19ff-fbcd3ef2cd21.png)

その行をダブルクリックすると、処理の詳細が表示されます。3行目に"Excecute completed"の行が出ていることから、処理が完了したことがわかります。
※bgPFから呼び出されるクラスの中で、EMLを使って更新を実行しようとしたときは、"Execute completed"の行が現れませんでした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/00414e1a-a890-e18d-9e95-6f9bdf975ec7.png)

"Go"ボタンを押して一覧を更新すると、予約率とステータスが更新されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5d0fda8a-d0d1-c3fe-b37f-cae7cd473df3.png)

プロセスを監視するその他の方法については、[Help Porlal](https://help.sap.com/docs/abap-cloud/abap-concepts/background-processing-framework)のMonitoring of the Background Processing Frameworkの章をご参照ください。

## おわりに
bgPFを検証してみて、Application Jobと比べて簡単に実装できることを実感しました。今後はRAPから非同期な処理を実行したいという場合は、bgPFが第一の選択肢になると思います。

## Appendix
本文で紹介しなかったオブジェクトのソースを掲載します。
ソースコードは以下のリポジトリにも格納しています。
https://github.com/miyasuta/rap-bgpf

### Projection View
```abap:Zc_Flightoccupancy 
@EndUserText.label: 'Flight Occupancy'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
define root view entity Zc_Flightoccupancy 
provider contract transactional_query
as projection on ZI_FlightOccupancy
{
    key AirlineID,
    key ConnectionID,
    key FlightDate,
    Price,
    CurrencyCode,
    PlaneType,
    MaximumSeats,
    OccupiedSeats,
    Criticality,
    CalcStatus,
    CalcStatusDisp,    
    OccupancyRate,
    OccupancyUnit,
    /* Associations */
//    _Airline, Service Bindingの登録時にエラーになったためコメントアウト
    _Connection,
    _Currency
}
```

### Behavior Projection
```abap:Zc_Flightoccupancy
projection;
strict ( 2 );

define behavior for Zc_Flightoccupancy alias FlightOccupancy
{

  use action calculateOccupacyRate;
}
```

### Service Definition
```abap:ZFLIGHTOCCUPANCY
@EndUserText.label: 'Flight Occupancy'
define service ZFlightOccupancy {
  expose Zc_Flightoccupancy as FlightOccupancy;
}
```

### Service Binging
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/433c5f3e-71be-c580-04f9-560cee79937b.png)


## 参考
- [Background Processing Framework | SAP Help Portal](https://help.sap.com/docs/abap-cloud/abap-concepts/background-processing-framework)
- [Introducing the Background Processing Framework | SAP Blog](https://blogs.sap.com/2023/12/14/introducing-the-background-processing-framework/)
- [Asynchronous code execution using ABAP Cloud | SAP Blog](https://blogs.sap.com/2023/10/11/asynchronous-code-execution-using-abap-cloud/)
- [What’s New in the ABAP RESTful Application Programming Model (RAP)? | Youtube](https://www.youtube.com/watch?v=yGWn8EKVpN0) 
- [ABAP Platform for SAP S/4HANA 2023](https://blogs.sap.com/2023/11/27/abap-platform-for-sap-s-4hana-2023/)