はじめに
RAPのBehaviorロジックの中から時間のかかる処理を実行するとき、呼び出し元にはすぐに結果を返し、バックグラウンドで非同期に処理を行いたい場合があります。
これまで、RAPから非同期処理を開始する方法としては、Application Job(※1)がありました。これはt-code: SM36で定義するジョブのABAP Cloud版です(※2)。クラス(API)を使用してプログラムからジョブを起動することができます。
Background Processing Framework (bgPF) は昨年登場した新しい方法で、S/4HANA On-Premiseでは2023から使うことができます。bgPFは、従来からある技術のバックグラウンドリモートファンクションコール(bgRFC)を利用しやすくするためのAPIを提供します。技術的な詳細についてはこちらのブログご参照ください。
この記事では、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またはその他のロジックから呼び出すだけです。
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に掲載します。
実装ステップ
- テーブル定義
- CDS View定義
- Behavior Definition定義
- bgPFから呼び出されるクラスの定義
- Behavior Implementationクラス実装
1. テーブル定義
以下のテーブルを定義します。このテーブルには座席の使用率、数量単位(%)、計算ステータスを格納します。
@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で項目を作成)
@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で座席の予約率を計算します。
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)に格納します。
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シナリオで、トランザクションの整合性を担保するために必要な宣言です。
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が無限に呼び出されることになったのではないかと推測しています。
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は更新されません。
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メソッドはエラーを返すことができないため、起こりうるエラーはバリデーションなどであらかじめつぶしておくことが必要です。
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 を選択します。
プロジェクトを右クリックし、"Create Configuration"を選択します。
"Background Processing"にチェックが入っていることを確認して"OK"をクリックします。
Service Bindingのプレビューから処理対象の行を選択して、"Calculate"アクションを実行します。
Trace Resultsタブを見ると、Request Entry Typeが"Remote Function Call"の行ができています。
その行をダブルクリックすると、処理の詳細が表示されます。3行目に"Excecute completed"の行が出ていることから、処理が完了したことがわかります。
※bgPFから呼び出されるクラスの中で、EMLを使って更新を実行しようとしたときは、"Execute completed"の行が現れませんでした
"Go"ボタンを押して一覧を更新すると、予約率とステータスが更新されています。
プロセスを監視するその他の方法については、Help PorlalのMonitoring of the Background Processing Frameworkの章をご参照ください。
おわりに
bgPFを検証してみて、Application Jobと比べて簡単に実装できることを実感しました。今後はRAPから非同期な処理を実行したいという場合は、bgPFが第一の選択肢になると思います。
Appendix
本文で紹介しなかったオブジェクトのソースを掲載します。
ソースコードは以下のリポジトリにも格納しています。
https://github.com/miyasuta/rap-bgpf
Projection View
@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
projection;
strict ( 2 );
define behavior for Zc_Flightoccupancy alias FlightOccupancy
{
use action calculateOccupacyRate;
}
Service Definition
@EndUserText.label: 'Flight Occupancy'
define service ZFlightOccupancy {
expose Zc_Flightoccupancy as FlightOccupancy;
}