株式会社ブレインパッドプロダクトユニットでRtoaster GenAIの開発をしている依田です。
今回はPL/SQLの TYPE オブジェクトを使ってGoFの Template Methodパターン を実装し、複数のバッチ処理を共通の骨格に乗せるハンズオンをお届けします。
はじめに
PL/SQLのストアドプロシージャを利用したバッチ処理で、このようなコードを見たことがある方も多いのではないでしょうか。
PROCEDURE batch_customer_sync IS
v_file UTL_FILE.FILE_TYPE;
BEGIN
v_file := UTL_FILE.FOPEN('LOG_DIR', 'batch.log', 'A');
UTL_FILE.PUT_LINE(v_file, 'START ...');
-- 抽出
...
-- 検証
...
-- 変換
...
-- ロード
...
UTL_FILE.PUT_LINE(v_file, 'END ...');
UTL_FILE.FCLOSE(v_file);
EXCEPTION
WHEN OTHERS THEN
UTL_FILE.PUT_LINE(v_file, 'ERROR ...'); -- エラーログ
UTL_FILE.FCLOSE(v_file);
RAISE;
END;
新しいバッチを追加するたびに、このプロシージャをコピーして中身だけ書き換える。ログ出力やエラーハンドリングの書き方が微妙に人によって違う。半年後に「あのバッチだけログの粒度が違う」と気づいて直して回る。こうした経験に心当たりのある方も少なくないはずです。
この「手順は共通、中身だけ違う」という構造は、GoFデザインパターンの Template Methodパターン がまさに解決したい問題です。PL/SQLはTYPEオブジェクトを使うことで、継承・オーバーライド・ポリモーフィズムを利用したオブジェクト指向プログラミングができます。本記事ではこれを使い、「抽出→検証→変換→ロード」というバッチの骨格を1箇所にまとめ、バッチごとの差分だけをサブタイプに実装する構成を作ります。
この記事で学べること
- PL/SQLの
TYPE/TYPE BODYによるオブジェクト指向の基本構文(継承・オーバーライド・ポリモーフィズム) - Template Methodパターンをバッチ処理に適用する設計
- 抽象メソッド(
NOT INSTANTIABLE)とフック処理(オーバーライド可能なエラーハンドリング)の使い分け - 複数のバッチをポリモーフィズムで一括実行するハンズオン
対象読者
- PL/SQLでストアドプロシージャは書けるが、
TYPEオブジェクトは使ったことがない方 - Javaなど他言語でTemplate Methodパターンを使った経験があり、PL/SQLでの実装に興味がある方
- 夜間バッチの実装がプロシージャのコピペで増殖してしまい、共通化したいと考えている方
前提知識
- 基本的なPL/SQL(プロシージャ、カーソル、例外処理)
- GoFデザインパターンについて名前程度は知っている(本記事内でも簡単に説明する)
Template Methodパターンとは
Template Methodパターンは、処理の骨格(アルゴリズム)を親クラスで固定し、その中の一部のステップだけを子クラスで実装させる パターンです。
親クラスは、処理の「順番」を決めるテンプレートメソッドと共通処理を持ちます。子クラスは、テンプレートメソッドから呼ばれる個別ステップだけを実装します。呼び出す側は子クラスの具体的な型を意識せず、共通の型として扱えます(ポリモーフィズム)。
run()(テンプレートメソッド)は親クラスで変更できないように固定し、extractDataなどの個別ステップだけをサブクラスに実装させます。これにより「処理の順番やログ・エラーハンドリングは統一されているが、中身はバッチごとに違う」という状態を作れます。
PL/SQLオブジェクト指向の基本構文
本題へ入る前に、今回使うPL/SQLのオブジェクト指向構文を簡単に整理します。
| 構文 | 役割 |
|---|---|
CREATE TYPE ... AS OBJECT |
クラスに相当する型定義(属性とメソッドの宣言) |
CREATE TYPE BODY |
メソッドの実装 |
NOT INSTANTIABLE |
そのメンバー(またはTYPE自体)が抽象的であることを示す。Javaのabstractに相当 |
NOT FINAL |
サブタイプによる継承を許可する(デフォルトはFINAL=継承不可) |
UNDER |
親タイプを継承してサブタイプを作る(extendsに相当) |
OVERRIDING MEMBER |
親のメソッドをオーバーライドする際に必須のキーワード |
FINAL MEMBER |
サブタイプでのオーバーライドを禁止する |
SELF |
メソッド内で自分自身のインスタンスを指す(暗黙的にも使われる) |
Oracle DatabaseのTYPEは既定でFINAL(継承不可)・INSTANTIABLE(インスタンス化可能)です。継承させたい場合は明示的にNOT FINALを付ける必要があります。
環境情報
| 項目 | バージョン |
|---|---|
| Oracle Database | Oracle AI Database 26ai Free (23.26.2.0.0) |
本記事はDocker版Oracle Database Free(Apple Silicon Mac + Colima環境)で動作確認しています。ローカル構築手順は以下の記事で解説しています。
https://qiita.com/take-yoda/items/97ac63a530d7e9669513
バッチ用テーブルの準備
まず、2種類のバッチ処理で使うテーブルを作成します。
顧客データ同期バッチ用(ステージングテーブル → マスタテーブル)
CREATE TABLE stg_customers (
customer_id NUMBER,
customer_name VARCHAR2(100),
email VARCHAR2(100),
updated_at DATE
);
CREATE TABLE customers (
customer_id NUMBER PRIMARY KEY,
customer_name VARCHAR2(100),
email VARCHAR2(100),
updated_at DATE
);
注文集計バッチ用(注文明細 → 日次集計テーブル)
CREATE TABLE orders (
order_id NUMBER PRIMARY KEY,
customer_id NUMBER,
order_date DATE,
amount NUMBER(10,2)
);
CREATE TABLE daily_sales_summary (
summary_date DATE PRIMARY KEY,
order_count NUMBER,
total_amount NUMBER(12,2)
);
サンプルデータも入れておきます。あえて不正データ(メールアドレスなし)を1件混ぜています。
INSERT INTO stg_customers VALUES (101, ' 田中 太郎 ', 'Taro.Tanaka@Example.com', DATE '2026-07-01');
INSERT INTO stg_customers VALUES (102, '佐藤 花子', 'hanako@example.com', DATE '2026-07-01');
-- 103は不正データ(メールアドレス無し)
INSERT INTO stg_customers VALUES (103, '鈴木 次郎', NULL, DATE '2026-07-01');
INSERT INTO orders VALUES (1, 101, DATE '2026-07-01', 1500);
INSERT INTO orders VALUES (2, 102, DATE '2026-07-01', 2800);
INSERT INTO orders VALUES (3, 101, DATE '2026-07-01', 999.5);
COMMIT;
親タイプ(テンプレート)の実装
TYPE定義(抽象型)
「抽出→検証→変換→ロード」の4ステップを抽象メソッド(NOT INSTANTIABLE)として宣言し、ログ出力とエラーハンドリングを共通実装として持たせます。
CREATE OR REPLACE TYPE t_batch_template AS OBJECT (
batch_id NUMBER,
batch_name VARCHAR2(50),
-- 抽象メソッド:サブタイプで必ず実装(オーバーライド)する
NOT INSTANTIABLE MEMBER PROCEDURE extract_data(SELF IN OUT NOCOPY t_batch_template),
NOT INSTANTIABLE MEMBER PROCEDURE validate_data(SELF IN OUT NOCOPY t_batch_template),
NOT INSTANTIABLE MEMBER PROCEDURE transform_data(SELF IN OUT NOCOPY t_batch_template),
NOT INSTANTIABLE MEMBER PROCEDURE load_data(SELF IN OUT NOCOPY t_batch_template),
-- フックメソッド:デフォルト実装を持ち、必要に応じてサブタイプで上書きできる
MEMBER PROCEDURE on_error(SELF IN OUT NOCOPY t_batch_template, p_step IN VARCHAR2, p_errm IN VARCHAR2),
-- 共通処理
MEMBER PROCEDURE log_step(SELF IN t_batch_template, p_step IN VARCHAR2, p_status IN VARCHAR2, p_message IN VARCHAR2 DEFAULT NULL),
-- テンプレートメソッド:FINALでサブタイプによる上書きを禁止する
FINAL MEMBER PROCEDURE run(SELF IN OUT NOCOPY t_batch_template)
) NOT INSTANTIABLE NOT FINAL;
/
NOT INSTANTIABLEを型レベルにも付けているのは、「このTYPE自体は直接インスタンス化できない(=抽象クラス)」ことを示すためです。抽象メソッドを1つでも持つTYPEは、型レベルにもNOT INSTANTIABLEを付ける必要があります。
TYPE BODY(テンプレートメソッドの実装)
CREATE OR REPLACE TYPE BODY t_batch_template AS
MEMBER PROCEDURE on_error(SELF IN OUT NOCOPY t_batch_template, p_step IN VARCHAR2, p_errm IN VARCHAR2) IS
BEGIN
log_step(p_step, 'ERROR', p_errm);
END on_error;
MEMBER PROCEDURE log_step(SELF IN t_batch_template, p_step IN VARCHAR2, p_status IN VARCHAR2, p_message IN VARCHAR2 DEFAULT NULL) IS
BEGIN
DBMS_OUTPUT.PUT_LINE(batch_name || ' | ' || p_step || ' | ' || p_status || ' | ' || p_message);
END log_step;
FINAL MEMBER PROCEDURE run(SELF IN OUT NOCOPY t_batch_template) IS
BEGIN
log_step('START', 'RUNNING', 'バッチ開始');
BEGIN
SELF.extract_data();
SELF.validate_data();
SELF.transform_data();
SELF.load_data();
EXCEPTION
WHEN OTHERS THEN
SELF.on_error('PROCESS', SQLERRM);
RAISE;
END;
log_step('END', 'SUCCESS', 'バッチ正常終了');
END run;
END;
/
runメソッドの中身を見てください。SELF.extract_data()のように呼び出していますが、このt_batch_template自体はインスタンス化されません。実際に実行時に動くのは、常にサブタイプ(t_customer_sync_batchやt_order_aggregate_batch)でオーバーライドされた実装です。これがPL/SQLにおける 動的ディスパッチ(dynamic dispatch) であり、Template Methodパターンの核となる仕組みです。
log_stepはDBMS_OUTPUT.PUT_LINEで結果を出力するだけのシンプルな実装にしています。今回はTemplate Methodパターンの骨格を理解することに集中したいため、あえてシンプルな出力方法を選んでいます。
DBMS_OUTPUTはSQL*Plusなどの対話的なセッションでしか結果を確認できません。DBMS_SCHEDULER経由で夜間実行するような非対話的なバッチでは出力を見る手段がないため、実務では別の方法でログを残す必要があります。具体的な方法は記事末尾の「実務で使う際の注意点」で紹介します。
サブタイプ1(顧客データ同期バッチ)の実装
ステージングテーブルからデータを取得し、検証・変換した上でマスタテーブルへMERGEするバッチです。
サブタイプ用の補助TYPE
抽出したステージング行を保持するため、オブジェクト型のネステッドテーブルを用意します。
CREATE OR REPLACE TYPE t_customer_row AS OBJECT (
customer_id NUMBER,
customer_name VARCHAR2(100),
email VARCHAR2(100),
updated_at DATE
);
/
CREATE OR REPLACE TYPE t_customer_tab AS TABLE OF t_customer_row;
/
サブタイプ定義
UNDERで親タイプを継承し、OVERRIDINGで4つの抽象メソッドを実装します。
CREATE OR REPLACE TYPE t_customer_sync_batch UNDER t_batch_template (
stg_rows t_customer_tab,
invalid_count NUMBER,
CONSTRUCTOR FUNCTION t_customer_sync_batch(p_batch_id IN NUMBER) RETURN SELF AS RESULT,
OVERRIDING MEMBER PROCEDURE extract_data(SELF IN OUT NOCOPY t_customer_sync_batch),
OVERRIDING MEMBER PROCEDURE validate_data(SELF IN OUT NOCOPY t_customer_sync_batch),
OVERRIDING MEMBER PROCEDURE transform_data(SELF IN OUT NOCOPY t_customer_sync_batch),
OVERRIDING MEMBER PROCEDURE load_data(SELF IN OUT NOCOPY t_customer_sync_batch)
);
/
サブタイプの実装
CREATE OR REPLACE TYPE BODY t_customer_sync_batch AS
CONSTRUCTOR FUNCTION t_customer_sync_batch(p_batch_id IN NUMBER) RETURN SELF AS RESULT IS
BEGIN
SELF.batch_id := p_batch_id;
SELF.batch_name := 'CUSTOMER_SYNC';
SELF.stg_rows := t_customer_tab();
SELF.invalid_count := 0;
RETURN;
END;
OVERRIDING MEMBER PROCEDURE extract_data(SELF IN OUT NOCOPY t_customer_sync_batch) IS
BEGIN
SELECT t_customer_row(customer_id, customer_name, email, updated_at)
BULK COLLECT INTO SELF.stg_rows
FROM stg_customers;
SELF.log_step('EXTRACT', 'SUCCESS', SELF.stg_rows.COUNT || '件抽出');
END extract_data;
OVERRIDING MEMBER PROCEDURE validate_data(SELF IN OUT NOCOPY t_customer_sync_batch) IS
v_valid_rows t_customer_tab := t_customer_tab();
BEGIN
FOR i IN 1 .. SELF.stg_rows.COUNT LOOP
IF SELF.stg_rows(i).email IS NULL THEN
SELF.invalid_count := SELF.invalid_count + 1;
ELSE
v_valid_rows.EXTEND;
v_valid_rows(v_valid_rows.COUNT) := SELF.stg_rows(i);
END IF;
END LOOP;
SELF.stg_rows := v_valid_rows;
SELF.log_step('VALIDATE', 'SUCCESS', '不正データ ' || SELF.invalid_count || '件を除外');
END validate_data;
OVERRIDING MEMBER PROCEDURE transform_data(SELF IN OUT NOCOPY t_customer_sync_batch) IS
BEGIN
FOR i IN 1 .. SELF.stg_rows.COUNT LOOP
SELF.stg_rows(i).email := LOWER(TRIM(SELF.stg_rows(i).email));
SELF.stg_rows(i).customer_name := TRIM(SELF.stg_rows(i).customer_name);
END LOOP;
SELF.log_step('TRANSFORM', 'SUCCESS', 'メールアドレスと氏名を正規化');
END transform_data;
OVERRIDING MEMBER PROCEDURE load_data(SELF IN OUT NOCOPY t_customer_sync_batch) IS
v_rows t_customer_tab := SELF.stg_rows;
BEGIN
MERGE INTO customers c
USING (
SELECT customer_id, customer_name, email, updated_at
FROM TABLE(v_rows)
) s
ON (c.customer_id = s.customer_id)
WHEN MATCHED THEN
UPDATE SET c.customer_name = s.customer_name,
c.email = s.email,
c.updated_at = s.updated_at
WHEN NOT MATCHED THEN
INSERT (customer_id, customer_name, email, updated_at)
VALUES (s.customer_id, s.customer_name, s.email, s.updated_at);
COMMIT;
SELF.log_step('LOAD', 'SUCCESS', v_rows.COUNT || '件をロード');
END load_data;
END;
/
extract_dataは抽象メソッド(親の宣言は本体を持たない)を、ここで初めて実装します。ステップの中身はバッチごとにまったく違います。ですが、「4ステップを順番に呼び出し、ログを取り、エラー時はハンドリングする」という骨格自体は親のrunが担っています。そのため、このサブタイプ側にはビジネスロジックだけが残ります。
実装時にハマったポイントを1つ共有します。親から継承したlog_stepを、このサブタイプの中で単にlog_step(...)と呼び出すと、以下のコンパイルエラーになります。
PLS-00201: identifier 'LOG_STEP' must be declared
t_batch_template自身のBODY内(runやon_error)では、log_step(...)のように無条件で呼び出せます。一方、サブタイプのBODYから継承メンバーを呼び出す場合は、明示的にSELF.log_step(...)と書く必要があります。SELFを省略すると解決できません。「同じTYPE内の呼び出し」と「サブタイプから継承メンバーを呼ぶ呼び出し」で、Oracleの名前解決の挙動が変わる点は覚えておく価値があります。
サブタイプ2(注文集計バッチ)の実装
同じ「抽出→検証→変換→ロード」の骨格に、まったく違う処理内容(集計バッチ)を乗せてみます。
CREATE OR REPLACE TYPE t_order_aggregate_batch UNDER t_batch_template (
target_date DATE,
order_count NUMBER,
total_amount NUMBER,
CONSTRUCTOR FUNCTION t_order_aggregate_batch(p_batch_id IN NUMBER, p_target_date IN DATE) RETURN SELF AS RESULT,
OVERRIDING MEMBER PROCEDURE extract_data(SELF IN OUT NOCOPY t_order_aggregate_batch),
OVERRIDING MEMBER PROCEDURE validate_data(SELF IN OUT NOCOPY t_order_aggregate_batch),
OVERRIDING MEMBER PROCEDURE transform_data(SELF IN OUT NOCOPY t_order_aggregate_batch),
OVERRIDING MEMBER PROCEDURE load_data(SELF IN OUT NOCOPY t_order_aggregate_batch)
);
/
CREATE OR REPLACE TYPE BODY t_order_aggregate_batch AS
CONSTRUCTOR FUNCTION t_order_aggregate_batch(p_batch_id IN NUMBER, p_target_date IN DATE) RETURN SELF AS RESULT IS
BEGIN
SELF.batch_id := p_batch_id;
SELF.batch_name := 'ORDER_AGGREGATE';
SELF.target_date := p_target_date;
SELF.order_count := 0;
SELF.total_amount := 0;
RETURN;
END;
OVERRIDING MEMBER PROCEDURE extract_data(SELF IN OUT NOCOPY t_order_aggregate_batch) IS
BEGIN
SELECT COUNT(*), NVL(SUM(amount), 0)
INTO SELF.order_count, SELF.total_amount
FROM orders
WHERE order_date = SELF.target_date;
SELF.log_step('EXTRACT', 'SUCCESS', SELF.order_count || '件の注文を集計対象として抽出');
END extract_data;
OVERRIDING MEMBER PROCEDURE validate_data(SELF IN OUT NOCOPY t_order_aggregate_batch) IS
v_negative_count NUMBER;
v_date DATE := SELF.target_date;
BEGIN
SELECT COUNT(*) INTO v_negative_count
FROM orders
WHERE order_date = v_date
AND amount < 0;
IF v_negative_count > 0 THEN
RAISE_APPLICATION_ERROR(-20001, '不正な金額(マイナス値)のレコードが' || v_negative_count || '件あります');
END IF;
SELF.log_step('VALIDATE', 'SUCCESS', '金額の妥当性チェックOK');
END validate_data;
OVERRIDING MEMBER PROCEDURE transform_data(SELF IN OUT NOCOPY t_order_aggregate_batch) IS
BEGIN
SELF.total_amount := ROUND(SELF.total_amount);
SELF.log_step('TRANSFORM', 'SUCCESS', '金額を丸め処理');
END transform_data;
OVERRIDING MEMBER PROCEDURE load_data(SELF IN OUT NOCOPY t_order_aggregate_batch) IS
v_date DATE := SELF.target_date;
BEGIN
MERGE INTO daily_sales_summary d
USING (SELECT v_date AS summary_date FROM dual) s
ON (d.summary_date = s.summary_date)
WHEN MATCHED THEN
UPDATE SET d.order_count = SELF.order_count,
d.total_amount = SELF.total_amount
WHEN NOT MATCHED THEN
INSERT (summary_date, order_count, total_amount)
VALUES (v_date, SELF.order_count, SELF.total_amount);
COMMIT;
SELF.log_step('LOAD', 'SUCCESS', TO_CHAR(v_date, 'YYYY-MM-DD') || 'の集計結果を保存');
END load_data;
END;
/
顧客同期バッチが「行単位のETL」だったのに対し、こちらは「集計クエリの結果を保存するだけ」というまったく違う中身です。それでも同じt_batch_templateを継承し、同じrunメソッドで実行できます。
実行してみる
単体実行
SET SERVEROUTPUT ON;
DECLARE
v_customer_batch t_customer_sync_batch := t_customer_sync_batch(1);
v_order_batch t_order_aggregate_batch := t_order_aggregate_batch(2, DATE '2026-07-01');
BEGIN
v_customer_batch.run();
v_order_batch.run();
END;
/
run()はどちらのインスタンスでも同じメソッド名で呼び出せますが、実行されるextract_data〜load_dataの中身はインスタンスの実際の型によって変わります。
ポリモーフィズムによる一括実行
Template Methodパターンの真価は、呼び出し側が個々のバッチの実装を意識しなくてよくなる点にあります。親タイプのコレクションに異なるサブタイプのインスタンスを詰め込み、同じループで一括実行できます。
DECLARE
TYPE t_batch_list IS TABLE OF t_batch_template;
v_batches t_batch_list;
BEGIN
v_batches := t_batch_list(
t_customer_sync_batch(3),
t_order_aggregate_batch(4, DATE '2026-07-01')
);
FOR i IN 1 .. v_batches.COUNT LOOP
v_batches(i).run();
END LOOP;
END;
/
t_batch_listはt_batch_template(親タイプ)のコレクションです。ですが実際には、t_customer_sync_batchとt_order_aggregate_batchという異なる型のインスタンスが混在しています。それでもv_batches(i).run()という同一の呼び出しで、それぞれの型に応じた処理が実行されます。これがオブジェクト指向におけるポリモーフィズムです。
実行結果
単体実行の結果、SQL*Plus上に以下のようなDBMS_OUTPUTが出力されます。
CUSTOMER_SYNC | START | RUNNING | バッチ開始
CUSTOMER_SYNC | EXTRACT | SUCCESS | 3件抽出
CUSTOMER_SYNC | VALIDATE | SUCCESS | 不正データ 1件を除外
CUSTOMER_SYNC | TRANSFORM | SUCCESS | メールアドレスと氏名を正規化
CUSTOMER_SYNC | LOAD | SUCCESS | 2件をロード
CUSTOMER_SYNC | END | SUCCESS | バッチ正常終了
ORDER_AGGREGATE | START | RUNNING | バッチ開始
ORDER_AGGREGATE | EXTRACT | SUCCESS | 3件の注文を集計対象として抽出
ORDER_AGGREGATE | VALIDATE | SUCCESS | 金額の妥当性チェックOK
ORDER_AGGREGATE | TRANSFORM | SUCCESS | 金額を丸め処理
ORDER_AGGREGATE | LOAD | SUCCESS | 2026-07-01の集計結果を保存
ORDER_AGGREGATE | END | SUCCESS | バッチ正常終了
ポリモーフィズムによる一括実行も、まったく同じパターンの出力がもう1セット続きます。t_customer_sync_batchとt_order_aggregate_batchは別々の型のインスタンスです。それでも、ログの出力順序はSTART → EXTRACT → VALIDATE → TRANSFORM → LOAD → ENDとまったく同じになっています。これが親タイプのrunメソッドで固定した骨格の効果です。
customersテーブルを見ると、不正データ(メール無し)だった顧客ID 103は除外され、正常な2件のみが同期されています。メールアドレスは小文字化、氏名は前後の空白がトリムされています。
| customer_id | customer_name | updated_at | |
|---|---|---|---|
| 101 | 田中 太郎 | taro.tanaka@example.com | 2026-07-01 |
| 102 | 佐藤 花子 | hanako@example.com | 2026-07-01 |
daily_sales_summaryテーブルには注文集計結果が保存されています。1500円 + 2800円 + 999.5円 = 5299.5円がtransform_dataの丸め処理により5300円として保存されています。
| summary_date | order_count | total_amount |
|---|---|---|
| 2026-07-01 | 3 | 5300 |
エラーハンドリングを確認する
t_order_aggregate_batchのvalidate_dataには、マイナス金額のデータを検知したら例外を投げるロジックを入れています。わざと不正データを混入させて、エラーハンドリングの流れを確認しましょう。
INSERT INTO orders VALUES (99, 101, DATE '2026-07-02', -500);
COMMIT;
DECLARE
v_batch t_order_aggregate_batch := t_order_aggregate_batch(5, DATE '2026-07-02');
BEGIN
v_batch.run();
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('バッチ失敗を検知: ' || SQLERRM);
END;
/
実行すると、以下のように出力されます。
ORDER_AGGREGATE | START | RUNNING | バッチ開始
ORDER_AGGREGATE | EXTRACT | SUCCESS | 1件の注文を集計対象として抽出
ORDER_AGGREGATE | PROCESS | ERROR | ORA-20001: 不正な金額(マイナス値)のレコードが1件あります
バッチ失敗を検知: ORA-20001: 不正な金額(マイナス値)のレコードが1件あります
extract_data(EXTRACT)までは成功しました。しかしvalidate_data(VALIDATE)でエラーが発生したため、VALIDATEのSUCCESSログは出力されていません。代わりに、runの例外ハンドラが呼んだon_errorによってPROCESSステップのERRORログが出力されています。load_data(集計結果の保存)まで到達していないため、daily_sales_summaryは更新されていません。
validate_dataで発生したRAISE_APPLICATION_ERRORは、親のrunメソッド内の例外ハンドラが捕捉します。on_errorフックを呼んだ上で、RAISEにより呼び出し元へ再送出しています。最後の行は呼び出し元(このブロックのEXCEPTION句)でエラーを捕捉した結果です。
新しいバッチ(例:在庫同期バッチ)を追加する場合も、t_batch_templateを継承したサブタイプを作り、4つの抽象メソッドを実装するだけで済みます。ログ出力やエラーハンドリングの流れは親タイプのrunがそのまま担うため、コピペによる実装のブレを防げます。
on_errorのようなフックメソッドは、必要なバッチだけデフォルト実装を上書きして拡張できます。たとえば「顧客同期バッチだけはエラー時にSlack通知も送りたい」といった要件にも対応できます。
実務で使う際の注意点
PL/SQLのオブジェクト指向機能は便利ですが、いくつか実務特有の注意点があります。
TYPEの変更はテーブルより慎重に
すでに稼働中のTYPEに列やメソッドを追加する場合、ALTER TYPE ... ADD ATTRIBUTEのような構文もあります。ただし依存オブジェクト(サブタイプやそのTYPEを使うテーブル・プロシージャ)が多いと、CASCADEオプションの影響範囲を事前に把握しておく必要があります。テーブルのカラム追加より一段プランニングが必要だと考えておくとよいでしょう。
テーブルの列としてTYPEを使う場合はSUBSTITUTABLEに注意
本記事のように、TYPEの変数をPL/SQLブロック内のローカル変数やコレクションとして使う分には、サブタイプのインスタンスをそのまま代入できます(デフォルトで多態的)。一方、TYPEをテーブルの列やオブジェクト表として永続化する場合は注意が必要です。CREATE TABLE ... (col t_batch_template) SUBSTITUTABLE AT ALL LEVELSのように明示しないと、サブタイプのインスタンスを格納できません。
パフォーマンス
オブジェクトメソッドの呼び出し(動的ディスパッチ)には多少のオーバーヘッドがありますが、通常のバッチ処理(1回の実行で数千〜数万件を捌く程度)では無視できる範囲です。ただし、1行ごとにメソッド呼び出しが発生するような大量データのループ処理では、通常のプロシージャ呼び出しやSQLのみでの一括処理と比べたパフォーマンス検証をおすすめします。
本番運用ではログの永続化を検討する
本記事ではlog_stepをDBMS_OUTPUT.PUT_LINEで実装し、Template Methodパターンの骨格を理解することに集中しました。実際にDBMS_SCHEDULERなどで非対話的に動かすバッチでは、DBMS_OUTPUTの内容を後から確認できません。
実務でよく使われる方法の1つが、UTL_FILEパッケージでログファイルに追記していく方式です。事前にディレクトリオブジェクトを作成しておきます。
CREATE OR REPLACE DIRECTORY batch_log_dir AS '/opt/oracle/batch_logs';
GRANT READ, WRITE ON DIRECTORY batch_log_dir TO batch_demo;
log_stepは以下のように差し替えられます。
MEMBER PROCEDURE log_step(SELF IN t_batch_template, p_step IN VARCHAR2, p_status IN VARCHAR2, p_message IN VARCHAR2 DEFAULT NULL) IS
v_file UTL_FILE.FILE_TYPE;
BEGIN
v_file := UTL_FILE.FOPEN('BATCH_LOG_DIR', batch_name || '.log', 'A');
UTL_FILE.PUT_LINE(v_file, batch_name || ' | ' || p_step || ' | ' || p_status || ' | ' || p_message);
UTL_FILE.FCLOSE(v_file);
END log_step;
UTL_FILEによるファイル書き込みは、Oracleのトランザクション管理の対象外です。そのため、バッチ本体の処理がエラーでROLLBACKされても、すでに書き込んだログ行はファイルに残ります。
テーブルへのINSERTで同じ動作を実現しようとすると、メイントランザクションの成否と切り離すための仕組みが別途必要になります。UTL_FILEならその心配がありません。
log_stepは親タイプに1箇所だけ実装されているため、このような差し替えもサブタイプ側のコードに一切手を入れずに行えます。
まとめ
PL/SQLは手続き型のイメージが強い言語ですが、TYPEオブジェクトを使うことで、継承・オーバーライド・ポリモーフィズムを活かしたオブジェクト指向ライクな設計もできます。バッチ処理のように「同じような処理が複数存在し、共通化したいが差分もある」ケースでは、Template Methodパターンによる骨格の共通化が効果を発揮します。ぜひ手元のOracle環境で試してみてください。