23c Oracle Saga Frameworkを試してみた その1：設定編

# はじめに
Oracle Saga Frameworkは、Oracle Databaseでマイクロサービス・アプリケーション用のsagaを実装および管理するフレームワークです。Oracle Databaseをプラットフォームとして、Sagaベースのアプリケーションを構築することができます。
私自身マイクロサービスはまだまだ勉強中の身で試行錯誤ですが、[ドキュメント](https://docs.oracle.com/cd/F82042_01/adfns/developing-applications-saga.html#GUID-DE723847-0907-48E8-87C8-9A5D2DB63A8D)
を参考に中間層なしでできるPL/SQLを使ってどんなものか試してみました。環境はOracle Database 23c Developer Releaseを使っています。

# 1. 試してみる構成について
Sagaパターンの例はこのようなものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134317/1a638c80-f9d0-3347-5768-f85cd3a201af.png)

::: note info
イメージは[Oracle社blog](https://blogs.oracle.com/oracle4engineer/post/simplify-microservice-transactions-with-oracle-database-sagas-ja) から拝借しましたが、このblogにはOracle DatabaseにおけるSagaの基本的な考え方から説明されています。
:::

旅行代理店サービスから、航空券サービス、ホテルサービス、レンタカーサービスに予約を行います。航空券、ホテル、レンタカーすべてが確保ができた場合のみ予約成立です。各サービス間はイベント航空券が取れても、ホテルが取れなければキャンセルとなるので、その場合は航空券の取り消しの処理のためのトランザクション（補償トランザクション）が必要となります。この記事では単純化して、旅行代理店サービスと航空券予約サービスとホテル予約サービスの部分を想定して試してみます。イメージは以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134317/a9f3164c-a5b0-26f3-cbab-b2c60eb495da.png)
# 2 Pluggable Databaseの作成
1つのCDB(Container Database)にそれぞれのサービス用途でPDB(Pluggable Database)を作成し、アプリケーションはPL/SQLで実装します。
旅行代理店サービスはTravelAgencyPDB、航空券予約サービスはAirlinePDB、ホテル予約サービスはHotelPDB、メッセージ伝播のためのブローカーもわかりやすいように個別でBrokerPDBというPDBを作成しました。Net経由で接続するための接続文字列もPDBと同じ名前で設定します。

```sql
connect / as sysdba
CREATE PLUGGABLE DATABASE TravelAgencyPDB ADMIN USER pdbadmin IDENTIFIED BY  "WElcome12345##" FILE_NAME_CONVERT=('/opt/oracle/oradata/FREE/pdbseed','/opt/oracle/oradata/FREE/TRAVELAGENCYPDB');
CREATE PLUGGABLE DATABASE AirlinePDB ADMIN USER pdbadmin IDENTIFIED BY "WElcome12345##" FILE_NAME_CONVERT=('/opt/oracle/oradata/FREE/pdbseed','/opt/oracle/oradata/FREE/AIRLINEPDB');
CREATE PLUGGABLE DATABASE HotelPDB ADMIN USER pdbadmin  IDENTIFIED BY "WElcome12345##" FILE_NAME_CONVERT=('/opt/oracle/oradata/FREE/pdbseed','/opt/oracle/oradata/FREE/HOTELPDB');
CREATE PLUGGABLE DATABASE BrokerPDB ADMIN USER pdbadmin  IDENTIFIED BY "WElcome12345##" FILE_NAME_CONVERT=('/opt/oracle/oradata/FREE/pdbseed','/opt/oracle/oradata/FREE/BROKERPDB');
```



# 3.各PDBにユーザーとPDB間のデータベース・リンクを作成
今回はadminというユーザーを作成し、そのユーザーでトランザクションもメッセージ伝播も実行させることにします。本来はユーザにあった権限、ロールを付与しないといけませんが、検証の便宜上CONNECT、RESOURCE、DBAロール、UNLIMITED TABLESPACE権限を付与しています。
SAGA_ADM_ROLEとSAGA_PARTICIPATION_ROLEは、それぞれOracle Saga Framworkの管理インタフェースとクライアント・インタフェースを実行するために必要なロールです。
また伝播のためにブローカーとサービスのPDB間でデータベース・リンクを作成する必要があります。Developer ReleaseのOracle Saga Frameworkではプライベート・データベースリンクはサポートされていないのでパブリックで作成します。

```sql
connect / as sysdba
-- @BrokerPDB
ALTER SESSION SET CONTAINER=BrokerPDB;
CREATE USER admin IDENTIFIED BY "WElcome12345##";
GRANT CONNECT,RESOURCE,DBA TO admin;
GRANT UNLIMITED TABLESPACE TO admin;
GRANT SAGA_ADM_ROLE TO admin;
GRANT EXECUTE ON SYS.DBMS_SAGA TO admin;

CREATE PUBLIC DATABASE LINK LinkToTravelAgency CONNECT TO admin IDENTIFIED BY 
 "WElcome12345##" USING 'TravelAgencyPDB';
CREATE PUBLIC DATABASE LINK LinkToAirline CONNECT TO admin IDENTIFIED BY "WElcome12345##" USING 'AirlinePDB';
CREATE PUBLIC DATABASE LINK LinkToHotel CONNECT TO admin IDENTIFIED BY "WElcome12345##" USING 'HotelPDB';

--@TravelAgencyPDB
ALTER SESSION SET CONTAINER=TravelAgencyPDB;
CREATE USER admin IDENTIFIED BY "WElcome12345##";
GRANT CONNECT,RESOURCE,DBA TO admin;
GRANT UNLIMITED TABLESPACE TO admin;
GRANT SAGA_ADM_ROLE TO admin;
GRANT SAGA_PARTICIPANT_ROLE TO admin ;
GRANT EXECUTE ON SYS.DBMS_SAGA TO admin;

CREATE PUBLIC DATABASE LINK LinkToBroker CONNECT TO admin IDENTIFIED BY  "WElcome12345##" USING 'BrokerPDB';

--@AirlinePDB
ALTER SESSION SET CONTAINER=AirlinePDB;
CREATE USER admin IDENTIFIED BY "WElcome12345##";
GRANT CONNECT,RESOURCE,DBA TO admin;
GRANT UNLIMITED TABLESPACE TO admin;
GRANT SAGA_ADM_ROLE TO admin;
GRANT SAGA_PARTICIPANT_ROLE TO admin ;
GRANT EXECUTE ON SYS.DBMS_SAGA TO admin;

CREATE PUBLIC DATABASE LINK LinkToBroker CONNECT TO admin IDENTIFIED BY  "WElcome12345##" USING 'BrokerPDB';

--@HotelPDB
ALTER SESSION SET CONTAINER=HotelPDB;
CREATE USER admin IDENTIFIED BY "WElcome12345##";
GRANT CONNECT,RESOURCE,DBA TO admin;
GRANT UNLIMITED TABLESPACE TO admin;
GRANT SAGA_ADM_ROLE TO admin;
GRANT SAGA_PARTICIPANT_ROLE TO admin ;
GRANT EXECUTE ON SYS.DBMS_SAGA TO admin;

CREATE PUBLIC DATABASE LINK LinkToBroker CONNECT TO admin IDENTIFIED BY  "WElcome12345##" USING 'BrokerPDB';
```

# 4.表の準備
航空券の予約管理のための表をAirlinePDBに、ホテルの予約管理のための表をHotelPDBに作成します。
座席や在庫のようなデータは、複数セッションからの更新による競合を起こすケースがありますが、Oracle Database 23cの新機能であるロックフリー予約を利用すると、ロックを取得せずジャーナル表で更新を管理することで競合を回避することができます。
また、Oracle Saga Frameworkでこの機能を利用すると、トランザクションが成功しなかった場合の補償トランザクションの実行も自動的に行わせることができます。そのため、今回座席数の列（seats）と部屋数(cnt)の列はロックフリー予約の列として作成します。

```sql
-- @AirlinePDB
connect admin/WElcome12345##@AirlinePDB
-- Add a table with reservable column which maintains flight information and insert data.
CREATE TABLE flights(flight VARCHAR2(10) PRIMARY KEY, seats NUMBER RESERVABLE CONSTRAINT flights_const CHECK (seats >= 0));

INSERT INTO flights VALUES('United',10);
INSERT INTO flights VALUES('Delta',5);
COMMIT;

-- @AirlinePDB
connect admin/WElcome12345##@HotelPDB
-- Add a table with reservable column which maintains room information and insert data.
CREATE TABLE rooms(roomtype VARCHAR2(20) PRIMARY KEY, cnt NUMBER RESERVABLE CONSTRAINT rooms_const CHECK (cnt >= 0));

INSERT INTO rooms VALUES('twin',1);
INSERT INTO rooms VALUES('single',3);
COMMIT;
```

それぞれのサービスの実行状況を確認するための表も作成しておきます。
``` sql
--- @TravelAgencyPDB
connect admin/WElcome12345##@TravelAgencyPDB
CREATE TABLE travelagencytest(sender VARCHAR2(100),message VARCHAR2(100));

--- @AirlinePDB
connect admin/WElcome12345##@AirlinePDB
CREATE TABLE flighttest(sender VARCHAR2(100),message VARCHAR2(100));

--- @HotelPDB
connect admin/WElcome12345##@HotelPDB
CREATE TABLE roomtest(sender VARCHAR2(100),message VARCHAR2(100));
```

# 5.アプリケーションの準備
各サービスで実行するアプリケーションを定義します。
Oracle Database Saga FrameworkのPL/SQLでは、Sagaのための事前定義済みのメソッドを使ってコールバック・パッケージを実装する必要があります。

## 旅行代理店サービスのアプリケーションを作成＠TravelAgencyPDB
事前定義済みのメソッドであるresponseプロシージャは、OPコードRESPONSEが指定されたsagaメッセージを受信したときに起動されます。アプリケーションの内容は航空券サービスで航空券を取れたか取れなかったかでトランザクションをコミットまたはロールバックするようにファイナライズしています。 ファイナライズはSagaの管理パッケージであるDBMS_SAGAパッケージのCOMMIT_SAGA、ROLLBACK_SAGAプロシージャで実行します。

```sql
--- @TravelAgencyPDB
connect admin/WElcome12345##@TravelAgencyPDB

-- create callback package for participant
CREATE OR REPLACE PACKAGE dbms_ta_cbk AS
PROCEDURE response(saga_id in RAW, saga_sender IN VARCHAR2, payload IN JSON DEFAULT NULL);
END dbms_ta_cbk;
/

CREATE OR REPLACE PACKAGE BODY dbms_ta_cbk AS
PROCEDURE response (saga_id in RAW, saga_sender IN VARCHAR2, payload IN JSON DEFAULT NULL) AS
flight_result VARCHAR2(30);
room_result VARCHAR2(30);
BEGIN
/* debug */
  INSERT INTO travelagencytest VALUES(saga_sender,JSON_SERIALIZE(payload));
  flight_result := JSON_VALUE(payload, '$.flightresult');
  room_result := JSON_VALUE(payload, '$.roomresult');

/* 航空券予約が成功した場合はホテルの予約へ、両方取れた場合はコミット、それ以外はロールバック */
  IF flight_result = 'success' AND room_result IS NULL THEN
    DBMS_SAGA.SEND_REQUEST(saga_id,'Hotel',payload);
  ELSIF room_result = 'success' AND room_result = 'success' THEN
    DBMS_SAGA.COMMIT_SAGA('TRAVELAGENCY', saga_id);
  ELSE
    DBMS_SAGA.ROLLBACK_SAGA('TRAVELAGENCY', saga_id);
  END IF;
END;
END dbms_ta_cbk;
/
```

## 航空券予約サービスのアプリケーションを作成＠AirlinePDB
AirlinePDBで実行されるアプリケーションを定義します。requestファンクションはOPコードREQUESTが指定されたSagaメッセージを受信したときに起動されます。
アプリケーションの内容は、受信した航空会社の座席数があればsuccess、なければfailureを返すというものです。

```sql
-- @AirlinePDB
connect admin/WElcome12345##@AirlinePDB;

-- create callback package for participant
CREATE OR REPLACE PACKAGE dbms_airline_cbk AS
FUNCTION request(saga_id in RAW, saga_sender IN VARCHAR2, payload IN JSON DEFAULT NULL) RETURN JSON;
END dbms_airline_cbk;
/

CREATE OR REPLACE PACKAGE BODY dbms_airline_cbk AS
FUNCTION request(saga_id in RAW, saga_sender IN VARCHAR2, payload IN JSON DEFAULT NULL) RETURN JSON AS
  response JSON;
  tickets NUMBER;
  p_val VARCHAR2(100);
BEGIN
  BEGIN
/* debug */
    INSERT INTO flighttest VALUES(saga_sender,JSON_SERIALIZE(payload));

/* 受信した情報と一致する航空会社の座席数を確認し取得できれば更新 */  
    SELECT seats INTO tickets FROM flights WHERE flight = JSON_VALUE(payload, '$.flight') for update;
    IF tickets > 0 THEN
      UPDATE flights SET seats = seats - 1 WHERE flight = JSON_VALUE(payload, '$.flight');
      p_val := REPLACE(REPLACE(JSON_SERIALIZE(payload),'{'),'}');
      response := JSON('{"flightresult":"success",'||p_val||'}');
    ELSE
      response := JSON('{"flightresult":"no-ticket"}');
    END IF; 

  EXCEPTION
      WHEN OTHERS THEN
        response := JSON('{"flightresult":"failure"}');
  END;
    RETURN response;
  END;
END dbms_airline_cbk;
/
```

## ホテル予約サービスのアプリケーションを作成＠HotelPDB
HotelPDBで実行されるアプリケーションを定義します。requestファンクションはOPコードREQUESTが指定されたSagaメッセージを受信したときに起動されます。
アプリケーションの内容は、受信したタイプの部屋の残数があればsuccess、なければfailureを返すというものです。

```sql
-- @HotelPDB
connect admin/WElcome12345##@HotelPDB;

-- create callback package for participant
CREATE OR REPLACE PACKAGE dbms_hotel_cbk AS
FUNCTION request(saga_id in RAW, saga_sender IN VARCHAR2, payload IN JSON DEFAULT NULL) RETURN JSON;
END dbms_hotel_cbk;
/
CREATE OR REPLACE PACKAGE BODY dbms_hotel_cbk AS
FUNCTION request(saga_id in RAW, saga_sender IN VARCHAR2, payload IN JSON DEFAULT NULL) RETURN JSON AS
  response JSON;
  room_cnt NUMBER;
  p_val VARCHAR2(100);
BEGIN
  BEGIN
/* debug */
    INSERT INTO roomtest VALUES(saga_sender,JSON_SERIALIZE(payload));
/* 受信した情報と一致する部屋の数を確認し取得できれば更新 */  
    SELECT cnt INTO room_cnt FROM rooms WHERE roomtype = JSON_VALUE(payload, '$.room') for update;
    IF room_cnt > 0 THEN
      UPDATE rooms SET cnt = cnt - 1 WHERE roomtype = JSON_VALUE(payload, '$.room');
      p_val := REPLACE(REPLACE(JSON_SERIALIZE(payload),'{'),'}');
      response := JSON('{"roomresult":"success",'||p_val||'}');

    ELSE
      response := JSON('{"roomresult":"no-room"}');
    END IF; 

  EXCEPTION
      WHEN OTHERS THEN
        response := JSON('{"roomresult":"failure"}');
  END;
    RETURN response;
  END;
END dbms_hotel_cbk;
/
```

# 6.Oracle Saga Frameworkのセットアップ

Oracle Saga Frameworkのセットアップを行います。DBMS_SAGAパッケージを使って以下を追加していきます。

+ **ブローカー** : 参加者間のメッセージ伝播の役割。参加者、コーディネータと紐づけられる
+ **参加者** : Sagaに参加するアプリケーション。ここでは旅行代理店サービス、航空券予約サービス、ホテル予約サービス
+ **イニシエータ** : Sagaを開始するアプリケーション。コーディネータと紐づけられる。ここでは旅行代理店サービス。
+ **コーディネータ** : トランザクション管理する役割

## ブローカの追加＠BrokerPDB
BrokerPDBでadminユーザーでブローカーを追加します。
ブローカは、複数のSaga参加者とそのコーディネータの間のメッセージ伝播の役割を担います。わかりやすいようにアプリケーションサービスとは別のPDBにしていますが、一緒のPDBでも構いません。裏ではOracle Databaseのアドバンスド・キューイングが使用されています。

```SQL
--@BrokerPDB
connect admin/WElcome12345##@BrokerPDB;
--add broker
BEGIN
DBMS_SAGA_ADM.ADD_BROKER(broker_name=>'TravelBroker', broker_schema=>'admin');
END;
/
```

## コーディネータと参加者の追加＠TravelAgencyPDB
TravelAgencyPDBでBrokerPDBで追加したブローカーと紐づけてコーディネータと参加者を追加します。
参加者はトランザクションを開始するイニシエータとなるため、コーディネータと関連付ける必要があります。また実行するコールバックパッケージを指定します。

::: note warn
現在のリリースではイニシエータはローカル（同じPDBおよび同じスキーマ）であるコーディネータにのみ関連付けることができます。
:::

```sql
-- @TravelAgencyPDB
connect admin/WElcome12345##@TravelAgencyPDB

-- Add the saga coordinator(local to the initiator)

BEGIN
 DBMS_SAGA_ADM.ADD_COORDINATOR(
 coordinator_name => 'TACoordinator',
 dblink_to_broker => 'LinktoBroker',
 mailbox_schema => 'ADMIN',
 broker_name => 'TravelBroker',
 dblink_to_coordinator => 'LinkToTravelAgency');
END;
/
-- Add the local saga participant TravelAgency and its coordinator as below
BEGIN
DBMS_SAGA_ADM.ADD_PARTICIPANT(
 participant_name=>'TravelAgency',
 coordinator_name=> 'TACoordinator',
 dblink_to_broker=> 'LinktoBroker',
 mailbox_schema=>'ADMIN',
 broker_name=> 'TravelBroker',
 dblink_to_participant=>'LinktoTravelAgency',
 callback_package => 'dbms_ta_cbk'
);
END;
/
```

## 参加者の追加＠AirlinePDB,HotelPDB
AirlinePDBとHotelPDBで参加者を追加します。ブローカーと作成したコールバック・パッケージを指定します。

```sql
-- @AirlinePDB
connect admin/WElcome12345##@AirlinePDB

--Add the  saga participant Airline 
BEGIN
 DBMS_SAGA_ADM.ADD_PARTICIPANT(
 participant_name=>'Airline', 
 dblink_to_broker=> 'LinktoBroker',
 mailbox_schema=>'ADMIN',
 broker_name=> 'TravelBroker',
 dblink_to_participant=>'LinktoAirline',
 callback_package => 'dbms_airline_cbk'
);
END;
/

-- @HotelPDB
connect admin/WElcome12345##@HotelPDB

--Add the  saga participant Hotel 
BEGIN
 DBMS_SAGA_ADM.ADD_PARTICIPANT(
 participant_name=>'Hotel', 
 dblink_to_broker=> 'LinktoBroker',
 mailbox_schema=>'ADMIN',
 broker_name=> 'TravelBroker',
 dblink_to_participant=>'LinktoHotel',
 callback_package => 'dbms_hotel_cbk'
);
END;
/
```

# 7.設定の確認
Oracle Saga Framworkの設定が完了したので、設定が正しいか確認します。

```sql
connect / as sysdba
-- check broker@BrokerPDB
ALTER SESSION SET CONTAINER=BrokerPDB;
SELECT * FROM SYS.SAGA_MESSAGE_BROKER$;

-- check coodinatior,participants
ALTER SESSION SET CONTAINER=cdb$root;
SELECT id,t1.name "NAME",owner,broker_name,coordinator,version,t2.name "PDBNAME" FROM CDB_SAGA_PARTICIPANTS t1,V$PDBS t2 WHERE t1.con_id=t2.con_id;
```

このような結果が返されるはずです。
``` sql
SQL> SELECT * from SYS.SAGA_MESSAGE_BROKER$;

ID                                       NAME                      OWNER                     BROKER_TOPIC                                 REMOTE    VERSION
---------------------------------------- ------------------------- ------------------------- ---------------------------------------- ---------- ----------
00FD25BBE8560665E0633C00640A1DC9         TRAVELBROKER23C           ADMIN                     SAGA$_TRAVELBROKER23C_INOUT                       0          1

SQL> SELECT id,t1.name "NAME",owner,broker_name,coordinator,version,t2.name "PDBNAME" FROM CDB_SAGA_PARTICIPANTS t1,V$PDBS t2 WHERE t1.con_id=t2.con_id ;

ID                                  NAME                      OWNER      BROKER_NAME               COORDINATOR                  VERSION PDBNAME
----------------------------------- ------------------------- ---------- ------------------------- ------------------------- ---------- ---------------
00FD26BF6C7F0680E0633C00640AE0CD    TACOORDINATOR23C          ADMIN      TRAVELBROKER23C                                              1 TRAVELAGENCYPDB
00FD26BF6C860680E0633C00640AE0CD    TRAVELAGENCY23C           ADMIN      TRAVELBROKER23C           TACOORDINATOR23C                   1 TRAVELAGENCYPDB
00FD275CAF9206F6E0633C00640A5556    AIRLINE23C                ADMIN      TRAVELBROKER23C                                              1 AIRLINEPDB
00FD2785F84E0700E0633C00640AD2C5    HOTEL23C                  ADMIN      TRAVELBROKER23C                                              1 HOTELPDB
00FD26BF6C7F0680E0633C00640AE0CD    TACOORDINATOR23C          ADMIN      TRAVELBROKER23C                                              1 BROKERPDB
00FD26BF6C860680E0633C00640AE0CD    TRAVELAGENCY23C           ADMIN      TRAVELBROKER23C           TACOORDINATOR23C                   1 BROKERPDB
00FD275CAF9206F6E0633C00640A5556    AIRLINE23C                ADMIN      TRAVELBROKER23C                                              1 BROKERPDB
00FD2785F84E0700E0633C00640AD2C5    HOTEL23C                  ADMIN      TRAVELBROKER23C                                              1 BROKERPDB
```

# おわりに
設定をその確認できたので、次はSagaを実行してみます。Sagaの実行は[別の記事](https://qiita.com/goo2000/items/4abddf45f0a7b4e356f7)で記載します。

# 追記(2023/9/25)
先週利用可能になったOCI Base Databaseの23cで試してみたところ、プライベート・データベースリンクはまだ使えないようです。add_participantのところでエラーになりました。

# 参考資料
+ [28 Sagaを使用したアプリケーションの開発](https://docs.oracle.com/cd/F82042_01/adfns/developing-applications-saga.html#GUID-DE723847-0907-48E8-87C8-9A5D2DB63A8D)
+ [OracleデータベースのSagaによるマイクロサービス・トランザクションの簡素化]( https://blogs.oracle.com/oracle4engineer/post/simplify-microservice-transactions-with-oracle-database-sagas-ja)
+ [23c Oracle Saga Frameworkを試してみた その2：実行編](https://qiita.com/goo2000/items/4abddf45f0a7b4e356f7)