Oracleでデータの整合性を守る方法は、アプリケーション側のif文だけではありません。同じ行の値から必ず導ける派生値は仮想列(Virtual Column)、単純な禁止ルールは制約、監査ログや別表更新などの副作用が必要な場合はトリガーに寄せると、更新漏れや実装差を減らせます。
ただし、仮想列とトリガーを何でも自動整合の道具として使うと、逆に保守しにくくなります。仮想列は同じ行の値から計算できる式に向き、トリガーはDMLの裏で処理が走るため、性能、ロック、例外、無効化、mutating tableを意識する必要があります。トリガー単体の基礎は PL/SQLトリガー完全ガイド、例外処理は PL/SQL例外処理完全ガイド もあわせて確認してください。
- 仮想列、制約、トリガー、アプリ実装の使い分け
- 仮想列で派生値を持つ基本パターン
- CHECK制約・UNIQUE制約・索引との組み合わせ
- トリガーで監査・補正・別表反映を行うときの注意点
- mutating table、compound trigger、INVALIDトリガーの実務対策
- 本番投入前のチェックリスト
最初に結論:まず制約、次に仮想列、最後にトリガー
整合性設計では、トリガーを最初に選ばない方が安全です。トリガーは強力ですが、DMLの裏で処理が走るため、呼び出し元から見えにくく、失敗するとINSERTやUPDATE自体を止めます。まず制約で守れるか、次に仮想列で表現できるか、それでも足りない場合にトリガーを使う順番にします。
NOT NULL、CHECK、UNIQUE、外部キーで表現できるルールは制約にします。最も明示的で、DBが最適化しやすいです。性能が理由でDB側に寄せる場合でも、まず現状計測が必要です。実行計画や待機を見ずにトリガーを増やすと、原因が追いにくい遅延になります。性能改善全体は PL/SQLパフォーマンス改善プレイブック、運用観測は PL/SQLインストゥルメンテーション設計 が関連します。
仮想列が向いているケース
仮想列は、表に格納されている列をもとに計算される列です。通常の列のようにSELECTでき、必要に応じて制約や索引と組み合わせられます。同じ計算式をアプリやSQLの各所に散らさず、テーブル定義に寄せられるのが利点です。
同じ行から計算できる派生値
CREATE TABLE order_lines (
order_line_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
quantity NUMBER NOT NULL,
unit_price NUMBER NOT NULL,
tax_rate NUMBER DEFAULT 0.10 NOT NULL,
subtotal NUMBER GENERATED ALWAYS AS (quantity * unit_price) VIRTUAL,
tax_amount NUMBER GENERATED ALWAYS AS (quantity * unit_price * tax_rate) VIRTUAL,
total_amount NUMBER GENERATED ALWAYS AS (quantity * unit_price * (1 + tax_rate)) VIRTUAL
);
SELECT order_line_id,
subtotal,
tax_amount,
total_amount
FROM order_lines;
合計金額のような派生値を実列として持つと、更新漏れで整合性が崩れます。仮想列なら、元の列が変われば計算結果も常に変わります。ただし、仮想列は計算式で表せる範囲に限られるため、別表参照や複数行集計まで抱え込む設計にはしません。
検索キーを正規化する
CREATE TABLE customers (
customer_id NUMBER PRIMARY KEY,
customer_code VARCHAR2(30) NOT NULL,
customer_name VARCHAR2(200) NOT NULL,
normalized_code VARCHAR2(30)
GENERATED ALWAYS AS (UPPER(TRIM(customer_code))) VIRTUAL
);
CREATE UNIQUE INDEX ux_customers_normalized_code
ON customers(normalized_code);
大文字小文字や前後空白を無視して一意にしたい場合、仮想列と索引を組み合わせると、アプリごとの正規化漏れを防げます。索引を作る場合は、登録・更新時のコストも測定します。
制約で済むケース
値の範囲、必須、重複禁止、状態遷移の一部などは、トリガーより制約で表現した方が明確です。制約はデータモデルの一部として見えやすく、トリガーのように裏で何かが動くわけではありません。
CHECK制約と仮想列を組み合わせる
CREATE TABLE contracts (
contract_id NUMBER PRIMARY KEY,
start_date DATE NOT NULL,
end_date DATE,
status VARCHAR2(20) NOT NULL,
active_flag NUMBER
GENERATED ALWAYS AS (
CASE
WHEN end_date IS NULL OR end_date >= start_date THEN 1
ELSE 0
END
) VIRTUAL,
CONSTRAINT ck_contract_period
CHECK (end_date IS NULL OR end_date >= start_date),
CONSTRAINT ck_contract_status
CHECK (status IN ('DRAFT', 'ACTIVE', 'CLOSED'))
);
CREATE INDEX ix_contracts_active_flag
ON contracts(active_flag);
この例では、期間の妥当性はCHECK制約で止め、検索や判定に使う値を仮想列にしています。「エラーにしたいルール」は制約、「読みやすくしたい派生値」は仮想列、と分けると設計が読みやすくなります。
仮想列の注意点
仮想列は便利ですが、万能ではありません。同じ行の値から安定して計算できる式に絞り、複雑な業務手続きや副作用を持たせないことが重要です。
仮想列をパーティションキーに使う設計もありますが、データ量、ロード方式、検索条件、メンテナンス手順を含めて検討します。パーティション設計は Oracleパーティションテーブル完全ガイド が関連します。
JSON値を仮想列として検索しやすくする
JSONをそのまま保存する設計では、よく検索する属性だけ仮想列として取り出すと扱いやすくなります。すべてを仮想列にするのではなく、WHERE句やJOIN条件で頻繁に使う項目だけに絞ります。
CREATE TABLE api_events (
event_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
payload CLOB CHECK (payload IS JSON),
event_type VARCHAR2(50)
GENERATED ALWAYS AS (
JSON_VALUE(payload, '$.type' RETURNING VARCHAR2(50))
) VIRTUAL,
tenant_id VARCHAR2(50)
GENERATED ALWAYS AS (
JSON_VALUE(payload, '$.tenantId' RETURNING VARCHAR2(50))
) VIRTUAL
);
CREATE INDEX ix_api_events_tenant_type
ON api_events(tenant_id, event_type);
JSON処理の詳細は Oracle JSON完全ガイド と PL/SQL JSON_TABLEでJSONを取り込む方法 が近いテーマです。REST APIで例外をJSONとして返す設計は PL/SQL例外をJSON形式で返すREST対応API設計 も参考になります。
トリガーが向いているケース
トリガーは、DMLに連動して別の処理を必ず実行したい場合に使います。代表例は監査ログ、作成者・更新者の自動設定、履歴表への出力、非正規化列の補正です。ただし、処理が見えにくくなるため、業務の中心ロジックを大量に詰め込まないようにします。
更新者と更新日時を自動設定する
CREATE OR REPLACE TRIGGER trg_orders_biu
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
BEGIN
IF INSERTING THEN
:NEW.created_at := SYSTIMESTAMP;
:NEW.created_by := SYS_CONTEXT('USERENV', 'SESSION_USER');
END IF;
:NEW.updated_at := SYSTIMESTAMP;
:NEW.updated_by := SYS_CONTEXT('USERENV', 'SESSION_USER');
END;
/
:NEW は登録・更新後の値、:OLD は更新前・削除前の値です。行トリガーでは1行ごとに処理が走るため、大量更新時のコストを必ず確認します。権限や実行者の考え方は AUTHIDと権限管理の設計 も関連します。
監査ログを残す
CREATE TABLE order_audit_logs (
audit_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_id NUMBER NOT NULL,
action_name VARCHAR2(20) NOT NULL,
old_status VARCHAR2(20),
new_status VARCHAR2(20),
changed_by VARCHAR2(128),
changed_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE OR REPLACE TRIGGER trg_orders_audit
AFTER UPDATE OF status ON orders
FOR EACH ROW
WHEN (OLD.status <> NEW.status)
BEGIN
INSERT INTO order_audit_logs (
order_id,
action_name,
old_status,
new_status,
changed_by
) VALUES (
:NEW.order_id,
'STATUS_CHANGE',
:OLD.status,
:NEW.status,
SYS_CONTEXT('USERENV', 'SESSION_USER')
);
END;
/
監査目的なら、トリガーで業務ログを残す設計のほかに、Oracleの監査機能を使う選択肢もあります。細かい条件で監査したい場合は DBMS_FGAでファイングレイン監査を設定する方法 も検討対象になります。
mutating tableを避ける
行トリガーの中で、同じ表をSELECTしたり更新したりすると、mutating table問題にぶつかることがあります。「そのDMLの途中で変化中の表」を、行ごとのトリガーから参照しようとするためです。
避けたい例
CREATE OR REPLACE TRIGGER trg_orders_check_ng
AFTER INSERT OR UPDATE ON order_lines
FOR EACH ROW
DECLARE
l_total NUMBER;
BEGIN
SELECT SUM(amount)
INTO l_total
FROM order_lines
WHERE order_id = :NEW.order_id;
IF l_total > 1000000 THEN
RAISE_APPLICATION_ERROR(-20001, 'order total limit exceeded');
END IF;
END;
/
このような検証は、行トリガーの中で同じ表を集計するのではなく、制約、アプリ側の明示処理、バッチ検証、compound triggerなどに分けて考えます。
compound triggerで行情報をまとめる
CREATE OR REPLACE TRIGGER trg_order_lines_limit
FOR INSERT OR UPDATE OR DELETE ON order_lines
COMPOUND TRIGGER
TYPE t_order_ids IS TABLE OF order_lines.order_id%TYPE INDEX BY PLS_INTEGER;
g_order_ids t_order_ids;
g_count PLS_INTEGER := 0;
AFTER EACH ROW IS
BEGIN
g_count := g_count + 1;
g_order_ids(g_count) := COALESCE(:NEW.order_id, :OLD.order_id);
END AFTER EACH ROW;
AFTER STATEMENT IS
l_total NUMBER;
BEGIN
FOR i IN 1 .. g_count LOOP
SELECT SUM(amount)
INTO l_total
FROM order_lines
WHERE order_id = g_order_ids(i);
IF l_total > 1000000 THEN
RAISE_APPLICATION_ERROR(-20001, 'order total limit exceeded');
END IF;
END LOOP;
END AFTER STATEMENT;
END trg_order_lines_limit;
/
compound triggerは、行ごとの情報を保持し、文の最後にまとめて処理できます。ただし、これも大量DMLでは重くなります。複雑な検証はトランザクション設計やバッチ検証に寄せる方が安定する場合があります。トランザクション全体は PL/SQLトランザクション設計パターン、大量処理のCOMMIT設計は 大量データ処理のコミット頻度とUNDO最適化 を確認してください。
トリガーの例外処理
トリガー内で例外が発生すると、呼び出し元のDMLも失敗します。WHEN OTHERS THEN NULL で握りつぶすと、データ不整合や監査漏れに気づけません。業務上止めるべきエラーは明示的に止め、ログだけ残したい処理は設計を分けます。
CREATE OR REPLACE TRIGGER trg_orders_validate
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
BEGIN
IF :NEW.order_status = 'SHIPPED'
AND :NEW.shipped_at IS NULL THEN
RAISE_APPLICATION_ERROR(
-20010,
'shipped_at is required when order_status is SHIPPED'
);
END IF;
END;
/
トリガーで止めるエラーは、アプリ側に返るメッセージも含めて設計します。例外設計や再試行が絡む場合は 例外設計と再試行パターン を組み合わせると整理しやすくなります。
INVALIDトリガーと依存関係を監視する
トリガーが依存する表、列、パッケージ、関数を変更すると、トリガーがINVALIDになることがあります。無効なトリガーは、DML時に再コンパイルを試み、失敗するとDMLを止める可能性があります。本番では、リリース後にトリガー状態を確認する手順が必要です。
SELECT object_name,
object_type,
status
FROM user_objects
WHERE object_type = 'TRIGGER'
ORDER BY status, object_name;
SELECT name,
type,
referenced_name,
referenced_type
FROM user_dependencies
WHERE type = 'TRIGGER'
ORDER BY name, referenced_type, referenced_name;
ALTER TRIGGER trg_orders_biu COMPILE;
トリガーが無効でDMLが失敗する例は ORA-04098の原因と解決方法、依存オブジェクトの管理は 依存オブジェクトとINVALID再コンパイルの制御 が関連します。
避けたい設計
本番投入前チェックリスト
- 制約で表現できるルールをトリガーにしていない
- 仮想列は同じ行の値から安定して計算できる式に限定している
- 仮想列に索引を付ける場合、検索側とDML側の両方を測定している
- トリガー内で同じ表を不用意に参照していない
- 大量INSERT/UPDATE/DELETE時の実行時間、UNDO、REDO、ロックを確認している
:NEWと:OLDの使えるタイミングを確認している- トリガー内の例外を握りつぶしていない
- リリース後に
USER_OBJECTSでINVALIDトリガーを確認する - 監査ログの保存期間、削除、パーティション、検索条件を決めている
- アプリ側から見えない副作用を設計書やコメントに残している
-- Virtual columns
SELECT table_name,
column_name,
data_default
FROM user_tab_cols
WHERE virtual_column = 'YES'
ORDER BY table_name, column_id;
-- Triggers
SELECT trigger_name,
table_name,
triggering_event,
trigger_type,
status
FROM user_triggers
ORDER BY table_name, trigger_name;
-- Invalid objects
SELECT object_name,
object_type,
status
FROM user_objects
WHERE status <> 'VALID'
ORDER BY object_type, object_name;
まとめ
仮想列とトリガーは、どちらもデータベース側で整合性を守るための強い道具です。しかし、まず制約で守れるかを確認し、同じ行から導ける派生値は仮想列にし、副作用が必要な場合だけトリガーを使う、という順番が大切です。
トリガーは特に、mutating table、性能、例外、INVALID、監査ログ肥大の影響を受けます。本番では、DML側の性能測定、依存関係の確認、リリース後のINVALIDチェック、ログ運用まで含めて設計しましょう。

