【Oracle】DBMS_PIPE・DBMS_ALERT完全ガイド|セッション間通信・非同期通知の仕組みと使い分けまで解説

Oracle のセッション間で情報をやりとりしたい場面があります。バッチジョブが処理完了を別のセッションに通知する、DML トリガーが変更イベントをアプリケーションに送信するといった要件です。

Oracle には 2 種類のセッション間通信パッケージが用意されています。DBMS_PIPEは同期的なメッセージパイプで、送信側と受信側が存在し双方向通信ができます。DBMS_ALERTは非同期イベント通知で、イベントが発生したことをサブスクライブしているセッションに一斉通知します。

この記事でわかること

  • DBMS_PIPE と DBMS_ALERT の仕組みと使い分け
  • DBMS_PIPE でメッセージを送受信する方法(PACK_MESSAGE / SEND_MESSAGE / RECEIVE_MESSAGE)
  • 公開パイプとプライベートパイプの違い
  • DBMS_ALERT でイベントを登録・通知・待機する方法(REGISTER / SIGNAL / WAITONE)
  • DBMS_PIPE / DBMS_ALERT に必要な権限
スポンサーリンク

DBMS_PIPE と DBMS_ALERT の比較

項目 DBMS_PIPE DBMS_ALERT
通信モデル 1対1(または複数セッションが取り合い) 1対多(1 つのシグナルを複数受信者が受け取る)
メッセージの消費 RECEIVE_MESSAGE で受け取ると消える WAITONE で受け取っても他のサブスクライバはまだ受け取れる
トランザクション連動 トランザクションと無関係(ROLLBACK しても送信済み) COMMIT 時に通知される(ROLLBACK では通知されない)
格納場所 SGA(共有メモリ) データベース(ALERT$ テーブル)
タイムアウト RECEIVE_MESSAGE の timeout で指定 WAITONE の timeout で指定
主な用途 バッチ間のメッセージパッシング・ステータス確認 DML 後のアプリ通知・イベント駆動処理

DBMS_PIPE の使い方

メッセージを送信する(送信セッション)

DBMS_PIPE でメッセージを送信する
-- プライベートパイプを作成する(作成したセッションのみアクセス可能)
DECLARE
    v_status NUMBER;
BEGIN
    -- CREATE_PIPE でパイプを明示的に作成する
    -- public => FALSE(プライベートパイプ)/ TRUE(公開パイプ)
    v_status := DBMS_PIPE.CREATE_PIPE(
        pipename => 'BATCH_STATUS_PIPE',
        maxpipesize => 8192,   -- パイプの最大サイズ(バイト)
        private  => TRUE       -- TRUE: プライベートパイプ(所有者のみアクセス可)
    );
    IF v_status != 0 THEN
        DBMS_OUTPUT.PUT_LINE('パイプ作成失敗: ' || v_status);
        RETURN;
    END IF;
END;
/

-- パイプにメッセージを送信する
DECLARE
    v_status NUMBER;
BEGIN
    -- PACK_MESSAGE でメッセージバッファに値を積む(複数の値を積める)
    DBMS_PIPE.PACK_MESSAGE('BATCH_COMPLETE');       -- VARCHAR2
    DBMS_PIPE.PACK_MESSAGE(SQL%ROWCOUNT);             -- NUMBER
    DBMS_PIPE.PACK_MESSAGE(SYSDATE);                  -- DATE

    -- SEND_MESSAGE でパイプに送信する
    v_status := DBMS_PIPE.SEND_MESSAGE(
        pipename => 'BATCH_STATUS_PIPE',
        timeout  => 10,     -- タイムアウト秒(パイプが満杯のとき待つ時間)
        maxpipesize => 8192
    );

    IF v_status = 0 THEN
        DBMS_OUTPUT.PUT_LINE('送信成功');
    ELSIF v_status = 1 THEN
        DBMS_OUTPUT.PUT_LINE('タイムアウト(パイプが満杯)');
    ELSE
        DBMS_OUTPUT.PUT_LINE('送信エラー: ' || v_status);
    END IF;
END;
/

メッセージを受信する(受信セッション)

DBMS_PIPE でメッセージを受信する
-- パイプからメッセージを受信する
DECLARE
    v_status   NUMBER;
    v_msg_type NUMBER;
    v_msg_str  VARCHAR2(4000);
    v_msg_num  NUMBER;
    v_msg_date DATE;
BEGIN
    -- RECEIVE_MESSAGE でパイプからメッセージを受信する
    v_status := DBMS_PIPE.RECEIVE_MESSAGE(
        pipename => 'BATCH_STATUS_PIPE',
        timeout  => 30    -- 最大 30 秒待機(0 = ノーウェイト、-1 = 無限待機は非推奨)
    );

    IF v_status = 0 THEN
        -- 受信成功: UNPACK_MESSAGE でバッファから値を取り出す
        -- 積んだ順番と同じ順で取り出す
        DBMS_PIPE.UNPACK_MESSAGE(v_msg_str);   -- 文字列
        DBMS_PIPE.UNPACK_MESSAGE(v_msg_num);   -- 数値
        DBMS_PIPE.UNPACK_MESSAGE(v_msg_date);  -- 日付

        DBMS_OUTPUT.PUT_LINE('メッセージ: ' || v_msg_str);
        DBMS_OUTPUT.PUT_LINE('件数: ' || v_msg_num);
        DBMS_OUTPUT.PUT_LINE('日時: ' || TO_CHAR(v_msg_date, 'YYYY-MM-DD HH24:MI:SS'));

    ELSIF v_status = 1 THEN
        DBMS_OUTPUT.PUT_LINE('タイムアウト: メッセージなし');
    ELSIF v_status = 2 THEN
        DBMS_OUTPUT.PUT_LINE('メッセージが大きすぎる(バッファ不足)');
    ELSE
        DBMS_OUTPUT.PUT_LINE('受信エラー: ' || v_status);
    END IF;
END;
/

-- パイプのクリーンアップ(使用後はパイプを削除する)
DECLARE
    v_status NUMBER;
BEGIN
    v_status := DBMS_PIPE.REMOVE_PIPE('BATCH_STATUS_PIPE');
END;
/

DBMS_ALERT の使い方

アラートを受信する(サブスクライバ)

DBMS_ALERT にアラートを登録して待機する(受信側)
-- アラートを受信するセッション
DECLARE
    v_message VARCHAR2(1800);
    v_status  NUMBER;
BEGIN
    -- REGISTER でアラート名を登録する(受け取りたいアラートを登録)
    DBMS_ALERT.REGISTER('ORDERS_INSERT_ALERT');
    DBMS_ALERT.REGISTER('ORDERS_UPDATE_ALERT');

    DBMS_OUTPUT.PUT_LINE('アラート待機中...');

    -- WAITONE で特定のアラートが来るまで待機する
    DBMS_ALERT.WAITONE(
        name    => 'ORDERS_INSERT_ALERT',
        message => v_message,   -- アラートとともに送られたメッセージを格納
        status  => v_status,    -- 0=受信成功, 1=タイムアウト
        timeout => 60           -- 最大 60 秒待機
    );

    IF v_status = 0 THEN
        DBMS_OUTPUT.PUT_LINE('アラート受信: ' || v_message);
    ELSE
        DBMS_OUTPUT.PUT_LINE('タイムアウト');
    END IF;

    -- セッション終了時はアラート登録を解除する
    DBMS_ALERT.REMOVE('ORDERS_INSERT_ALERT');
    DBMS_ALERT.REMOVEALL;   -- すべてのアラート登録を一括解除
END;
/

アラートを送信する(シグナル側)

トリガーからアラートを送信する(送信側)
-- orders テーブルへの INSERT をアラートで通知するトリガー
-- DBMS_ALERT.SIGNAL は COMMIT 時に通知される(ROLLBACK では通知されない)
CREATE OR REPLACE TRIGGER trg_orders_alert
    AFTER INSERT ON orders
    FOR EACH ROW
BEGIN
    -- SIGNAL でアラートを送信する(COMMIT 後に受信側に届く)
    DBMS_ALERT.SIGNAL(
        name    => 'ORDERS_INSERT_ALERT',
        message => 'order_id=' || :NEW.order_id || ', customer_id=' || :NEW.customer_id
    );
    -- 注意: SIGNAL はトランザクションの COMMIT を待って通知される
    -- このトリガーの COMMIT 前は受信側に届かない
END trg_orders_alert;
/

-- PL/SQL プロシージャからもアラートを送信できる
BEGIN
    DBMS_ALERT.SIGNAL('BATCH_DONE', '月次バッチが完了しました');
    COMMIT;   -- COMMIT をしてアラートを発火させる
END;
/

-- WAITANY で複数のアラートをまとめて待機する
DECLARE
    v_name    VARCHAR2(30);
    v_message VARCHAR2(1800);
    v_status  NUMBER;
BEGIN
    DBMS_ALERT.REGISTER('ORDERS_INSERT_ALERT');
    DBMS_ALERT.REGISTER('BATCH_DONE');

    -- いずれかのアラートが来るまで待機する
    DBMS_ALERT.WAITANY(
        name    => v_name,
        message => v_message,
        status  => v_status,
        timeout => 120
    );

    IF v_status = 0 THEN
        DBMS_OUTPUT.PUT_LINE('受信アラート: ' || v_name || ' / メッセージ: ' || v_message);
    END IF;
    DBMS_ALERT.REMOVEALL;
END;
/

まとめ

  • DBMS_PIPE:セッション間のメッセージパッシング。PACK_MESSAGE で積んで SEND_MESSAGE で送信、RECEIVE_MESSAGE で受信。トランザクションと無関係に動作する
  • DBMS_ALERT:COMMIT 連動のイベント通知。REGISTER でサブスクライブ、SIGNAL で通知、WAITONE / WAITANY で待機。ROLLBACK ではシグナルが発火しない
  • 使い分け:1対1のメッセージ送受信には DBMS_PIPE、DML 完了後のイベント通知・1対多通知には DBMS_ALERT が向いている
  • DBMS_PIPE の注意:プライベートパイプは所有者のみアクセス可能。使用後は REMOVE_PIPE でパイプを削除してリソースを解放する
  • DBMS_ALERT の注意:セッション終了時に REMOVE / REMOVEALL でアラート登録を解除する

セッション間の排他制御には DBMS_LOCK完全ガイドを参照してください。DBMS_SCHEDULER でジョブを非同期実行する方法は DBMS_SCHEDULER完全ガイドも参照してください。