【PL/SQL】OBJECT TYPEとメンバーメソッドの使い方|MAP・継承・ネスト表まで

【PL/SQL】オブジェクト型とメソッドを使ったオブジェクト指向設計 PL/SQL

OBJECT TYPE は、Oracle Databaseで属性とメソッドをまとめて定義できるSQLオブジェクト型です。PL/SQLの中で「データと振る舞い」を一体にしたい場合、ネスト表やパイプライン関数の戻り値を型として整えたい場合に役立ちます。

ただし、実務では通常の表・レコード・パッケージで十分な場面も多く、安易にオブジェクト型へ寄せるとSQL、ORM、移行、依存関係が複雑になります。この記事では、OBJECT TYPEを使うべき場面と避けるべき場面を分けつつ、メンバーメソッド、コンストラクタ、MAP / ORDER、継承、ネスト表まで整理します。網羅的な仕様は Oracle オブジェクト型(CREATE TYPE)完全ガイド も参考になります。

この記事で扱うこと

  • CREATE TYPE ... AS OBJECTTYPE BODY
  • MEMBER FUNCTION / MEMBER PROCEDURESELF
  • コンストラクタ、ネスト表、オブジェクト列の使い方
  • MAP / ORDER メソッドによる比較・並び替え
  • FINAL / NOT FINAL、継承、オーバーライド
  • USER_TYPESUSER_TYPE_ATTRSUSER_TYPE_METHODS による確認
スポンサーリンク

OBJECT TYPEとは

オブジェクト型は、属性とメソッドを持つユーザー定義型です。属性はデータ構造、メソッドはそのデータに対する処理を表します。Oracle公式ドキュメントでも、メンバーメソッドはオブジェクトインスタンスのデータへアクセスするための処理として説明されています。

属性顧客ID、名称、メールアドレスなど、型が持つデータです。
メソッド属性を使って処理する関数・手続きです。
TYPE BODYメソッドの実装を書く場所です。
SQL型PL/SQLだけでなくSQL、テーブル列、コレクション、パイプライン関数でも使えます。

基本構文

まず、顧客を表すオブジェクト型を定義します。型仕様に属性とメソッド宣言を書き、型本体にメソッド実装を書きます。

customer-type.sql
CREATE OR REPLACE TYPE customer_obj AS OBJECT (
  cust_id   NUMBER,
  cust_name VARCHAR2(100),
  email     VARCHAR2(200),

  MEMBER PROCEDURE show_info,
  MEMBER FUNCTION domain RETURN VARCHAR2
);
/

CREATE OR REPLACE TYPE BODY customer_obj AS
  MEMBER PROCEDURE show_info IS
  BEGIN
    DBMS_OUTPUT.PUT_LINE(
      'ID=' || SELF.cust_id ||
      ', NAME=' || SELF.cust_name ||
      ', EMAIL=' || SELF.email
    );
  END show_info;

  MEMBER FUNCTION domain RETURN VARCHAR2 IS
  BEGIN
    IF SELF.email IS NULL OR INSTR(SELF.email, '@') = 0 THEN
      RETURN NULL;
    END IF;

    RETURN SUBSTR(SELF.email, INSTR(SELF.email, '@') + 1);
  END domain;
END;
/

SELF は現在のオブジェクトインスタンスを表します。属性名だけでも参照できますが、実務では SELF.email のように書くと、ローカル変数や引数との区別が明確になります。

インスタンスを作成してメソッドを呼ぶ

オブジェクト型には暗黙のコンストラクタが作られます。属性順に値を渡してインスタンスを作成し、ドット記法でメソッドを呼び出します。メソッド呼び出しでは、引数がない関数でも括弧を付ける書き方にしておくと安全です。

use-object-type.sql
DECLARE
  l_customer customer_obj;
BEGIN
  l_customer := customer_obj(
                  101,
                  'Sato Taro',
                  'taro@example.com'
                );

  l_customer.show_info();
  DBMS_OUTPUT.PUT_LINE('domain=' || l_customer.domain());
END;
/

初期化していないオブジェクト変数の属性やメソッドに触ると、ORA-06530 になることがあります。未初期化の複合型エラーは ORA-06530の原因と解決方法 が参考になります。

独自コンストラクタを定義する

既定のコンストラクタは属性順にすべての値を渡します。初期値を補完したい、入力を検証したい、属性順に依存したくない場合は、独自コンストラクタを用意できます。

custom-constructor.sql
CREATE OR REPLACE TYPE customer_obj AS OBJECT (
  cust_id   NUMBER,
  cust_name VARCHAR2(100),
  email     VARCHAR2(200),

  CONSTRUCTOR FUNCTION customer_obj(
    p_cust_id   IN NUMBER,
    p_cust_name IN VARCHAR2
  ) RETURN SELF AS RESULT,

  MEMBER FUNCTION domain RETURN VARCHAR2
);
/

CREATE OR REPLACE TYPE BODY customer_obj AS
  CONSTRUCTOR FUNCTION customer_obj(
    p_cust_id   IN NUMBER,
    p_cust_name IN VARCHAR2
  ) RETURN SELF AS RESULT
  IS
  BEGIN
    SELF.cust_id := p_cust_id;
    SELF.cust_name := p_cust_name;
    SELF.email := NULL;
    RETURN;
  END;

  MEMBER FUNCTION domain RETURN VARCHAR2 IS
  BEGIN
    IF SELF.email IS NULL OR INSTR(SELF.email, '@') = 0 THEN
      RETURN NULL;
    END IF;
    RETURN SUBSTR(SELF.email, INSTR(SELF.email, '@') + 1);
  END;
END;
/

独自コンストラクタを増やすと便利ですが、属性追加や仕様変更時の影響も増えます。通常のパッケージAPIで生成を隠す設計も候補です。パッケージ設計の考え方は パッケージ設計でコード管理と再利用性を極める と合わせて考えると整理しやすくなります。

ネスト表で複数オブジェクトを扱う

オブジェクト型は、ネスト表やVARRAYの要素型として使えます。複数の顧客、明細、検証結果などをまとめて返したい場合に便利です。

object-nested-table.sql
CREATE OR REPLACE TYPE customer_tab AS TABLE OF customer_obj;
/

DECLARE
  l_customers customer_tab := customer_tab();
BEGIN
  l_customers.EXTEND(2);
  l_customers(1) := customer_obj(1, 'Tanaka Hanako', 'hanako@example.com');
  l_customers(2) := customer_obj(2, 'Yamada Jiro', 'jiro@example.com');

  FOR i IN 1 .. l_customers.COUNT LOOP
    l_customers(i).show_info();
  END LOOP;
END;
/

コレクション型の基本は PL/SQL コレクション型完全ガイド、実戦パターンは コレクションを実戦活用する完全ガイド も参考になります。

SQLで使う場合

オブジェクト型はSQL型なので、テーブル列やSQLのSELECTでも使えます。ただし、オブジェクト列は便利な反面、検索・索引・ORM・移行で扱いが難しくなることがあります。

object-column.sql
CREATE TABLE order_master (
  order_id NUMBER PRIMARY KEY,
  customer customer_obj
);

INSERT INTO order_master(order_id, customer)
VALUES (
  1,
  customer_obj(100, 'Takahashi Ichiro', 'ichiro@example.com')
);

SELECT o.order_id,
       o.customer.cust_id,
       o.customer.cust_name,
       o.customer.domain() AS email_domain
FROM order_master o;

業務テーブルの主要データをオブジェクト列に入れると、後続の集計や検索で通常の列より扱いづらくなることがあります。長期運用の基幹テーブルでは、通常のリレーショナル列に分解し、オブジェクト型はAPI境界や戻り値に使う方が無難なことが多いです。

MAPメソッドで並び替える

オブジェクト同士を比較・並び替えしたい場合、MAP MEMBER FUNCTION または ORDER MEMBER FUNCTION を定義します。MAP はオブジェクトを比較可能なスカラー値へ写像する方法です。Oracle公式でも、MAPメソッドは比較やソートの基準になるスカラー値を返すと説明されています。

map-method.sql
CREATE OR REPLACE TYPE amount_obj AS OBJECT (
  currency_code VARCHAR2(3),
  amount        NUMBER,

  MAP MEMBER FUNCTION sort_key RETURN NUMBER
);
/

CREATE OR REPLACE TYPE BODY amount_obj AS
  MAP MEMBER FUNCTION sort_key RETURN NUMBER IS
  BEGIN
    RETURN NVL(SELF.amount, 0);
  END;
END;
/

CREATE OR REPLACE TYPE amount_tab AS TABLE OF amount_obj;
/

SELECT VALUE(t).currency_code,
       VALUE(t).amount
FROM TABLE(amount_tab(
       amount_obj('JPY', 1200),
       amount_obj('USD', 10),
       amount_obj('EUR', 8)
     )) t
ORDER BY VALUE(t);

ひとつのオブジェクト型に定義できる比較方法は、基本的に MAPORDER のどちらかです。多くのケースでは、比較基準が単一値で表せる MAP の方がシンプルです。

継承とオーバーライド

オブジェクト型は NOT FINAL にするとサブタイプを作れます。親型のメソッドを子型でオーバーライドする場合は、親側のメソッドもオーバーライド可能な設計にします。

inheritance-override.sql
CREATE OR REPLACE TYPE base_customer AS OBJECT (
  cust_id   NUMBER,
  cust_name VARCHAR2(100),

  MEMBER FUNCTION get_info RETURN VARCHAR2
) NOT FINAL;
/

CREATE OR REPLACE TYPE BODY base_customer AS
  MEMBER FUNCTION get_info RETURN VARCHAR2 IS
  BEGIN
    RETURN 'customer=' || SELF.cust_name;
  END;
END;
/

CREATE OR REPLACE TYPE corporate_customer UNDER base_customer (
  company_name VARCHAR2(200),

  OVERRIDING MEMBER FUNCTION get_info RETURN VARCHAR2
);
/

CREATE OR REPLACE TYPE BODY corporate_customer AS
  OVERRIDING MEMBER FUNCTION get_info RETURN VARCHAR2 IS
  BEGIN
    RETURN 'company=' || SELF.company_name || ', contact=' || SELF.cust_name;
  END;
END;
/

継承は強力ですが、型階層が深くなると依存関係と変更影響が読みにくくなります。通常のPL/SQL設計では、パッケージとレコード型で十分な場面も多いため、継承は本当に必要な場合に絞るのが現実的です。

JSON連携は自作メソッドとして扱う

注意点として、TO_JSONFROM_JSON はOracleのすべてのオブジェクト型に自動で生える標準メソッドではありません。JSONへ変換したい場合は、JSON_OBJECT などのSQL/JSON関数を使うか、自作のメンバーメソッドとして実装します。

to-json-method.sql
CREATE OR REPLACE TYPE customer_json_obj AS OBJECT (
  cust_id   NUMBER,
  cust_name VARCHAR2(100),
  email     VARCHAR2(200),

  MEMBER FUNCTION to_json RETURN CLOB
);
/

CREATE OR REPLACE TYPE BODY customer_json_obj AS
  MEMBER FUNCTION to_json RETURN CLOB IS
    l_json CLOB;
  BEGIN
    SELECT JSON_OBJECT(
             'custId' VALUE SELF.cust_id,
             'name'   VALUE SELF.cust_name,
             'email'  VALUE SELF.email
             RETURNING CLOB
           )
    INTO l_json
    FROM dual;

    RETURN l_json;
  END;
END;
/

JSON機能全体は Oracle JSON完全ガイド、JSONを表形式へ展開する処理は JSON_TABLEでJSONを取り込む方法 が参考になります。

パイプライン関数の戻り値に使う

オブジェクト型は、パイプライン表関数の行型としてよく使われます。複数列を1行のオブジェクトとして返し、それをSQLから表のようにSELECTできます。

pipeline-row-type.sql
CREATE OR REPLACE TYPE sales_row_obj AS OBJECT (
  customer_id NUMBER,
  total_amt   NUMBER
);
/

CREATE OR REPLACE TYPE sales_row_tab AS TABLE OF sales_row_obj;
/

CREATE OR REPLACE FUNCTION get_sales_rows
RETURN sales_row_tab PIPELINED
IS
BEGIN
  FOR r IN (
    SELECT customer_id, SUM(amount) total_amt
    FROM sales
    GROUP BY customer_id
  ) LOOP
    PIPE ROW (sales_row_obj(r.customer_id, r.total_amt));
  END LOOP;

  RETURN;
END;
/

SELECT *
FROM TABLE(get_sales_rows());

パイプライン表関数での使い方は パイプライン表関数完全ガイドパイプライン関数で大量データ処理を勝たせる完全ガイド と関連します。

メタデータを確認する

作成済みのオブジェクト型は、データディクショナリで確認できます。属性、メソッド、依存関係、コンパイルエラーを確認するSQLを用意しておくと、変更時の調査が楽になります。

inspect-object-type.sql
SELECT type_name, typecode, attributes, methods, final, instantiable
FROM user_types
WHERE type_name IN ('CUSTOMER_OBJ', 'CUSTOMER_TAB');

SELECT type_name, attr_name, attr_type_name, length, precision, scale
FROM user_type_attrs
WHERE type_name = 'CUSTOMER_OBJ'
ORDER BY attr_no;

SELECT type_name, method_name, method_type, final, instantiable
FROM user_type_methods
WHERE type_name = 'CUSTOMER_OBJ'
ORDER BY method_no;

SELECT name, type, referenced_name, referenced_type
FROM user_dependencies
WHERE referenced_name = 'CUSTOMER_OBJ'
ORDER BY type, name;

SELECT name, type, line, position, text
FROM user_errors
WHERE name = 'CUSTOMER_OBJ'
ORDER BY sequence;

コンパイルエラーや PLS-00302 の確認は PL/SQLコンパイル時エラーと警告の対処、コンポーネント未宣言エラーは PLS-00302の原因と解決方法 が参考になります。

使うべきケース・避けるべきケース

向いているパイプライン関数の行型、複雑な戻り値、比較ロジックを型に閉じ込めたい処理。
向いている属性と振る舞いを一体で扱う小さな値オブジェクト。
避けたい通常の検索・集計が多い基幹テーブルの主要列。
避けたいORMや外部ツール、ETL、移行で単純な表構造が求められる領域。
避けたい頻繁に属性追加・削除が起こる不安定なデータモデル。

本番前チェックリスト

通常表で十分かOBJECT TYPEでないと解決できない理由があるか。
依存関係型を変更したときに影響する表、関数、パッケージを確認したか。
メソッドSELF、NULL、例外、戻り値を明確にしているか。
比較ソートや重複排除に使うなら MAP / ORDER を設計したか。
継承NOT FINAL にする必要が本当にあるか。
JSONTO_JSON などは自作メソッドとして実装しているか。
テスト未初期化、NULL属性、不正値、型変更時の依存をテストしたか。

まとめ

OBJECT TYPE を使うと、Oracle PL/SQLでも属性とメソッドをまとめたオブジェクト指向的な設計ができます。メンバーメソッド、独自コンストラクタ、ネスト表、MAPメソッド、継承を使えば、複雑な値や戻り値を型として表現できます。

一方で、通常の表・レコード・パッケージで十分な場面も多く、オブジェクト列や深い継承は運用を難しくします。実務では、パイプライン関数の行型、API境界の値オブジェクト、比較ロジックを閉じ込めたい型などに絞って使うのが現実的です。