7.5 オブジェクト型コール仕様の記述

SQLでは、オブジェクト指向のプログラミングは、データ構造とそのデータの操作に必要なファンクションおよびプロシージャをカプセル化したユーザー定義のコンポジット・データ型である、オブジェクト型に基づきます。データ構造を形成する変数は属性と呼ばれます。オブジェクト型の動作を特徴づけるファンクションおよびプロシージャはメソッドと呼ばれ、Javaで記述できます。

パッケージと同様に、オブジェクト型には、仕様部と本体の2つの部分があります。仕様部はアプリケーションに対するインタフェースで、一連の属性であるデータ構造と、そのデータの操作に必要な操作方法(メソッド)を宣言します。本体は、PL/SQLサブプログラムの本体またはコール仕様を定義して仕様部を実装します。

仕様部で属性またはコール仕様のみを宣言する場合、本体は必要ありません。すべてのメソッドをJavaで実装する場合は、メソッドのコール仕様をオブジェクト型の仕様部に配置して、本体を省略できます。

SQL*Plusでは、次の構文を使用してSQLオブジェクト型を対話形式で定義できます。

CREATE [OR REPLACE] TYPE type_name
  [AUTHID {CURRENT_USER | DEFINER}] {IS | AS} OBJECT (
  attribute_name data_type[, attribute_name data_type]...
  [{MAP | ORDER} MEMBER {function_spec | call_spec},]
  [{MEMBER | STATIC} {subprogram_spec | call_spec}
  [, {MEMBER | STATIC} {subprogram_spec | call_spec}]...]
);

[CREATE [OR REPLACE] TYPE BODY type_name {IS | AS}
  { {MAP | ORDER} MEMBER function_body;
   | {MEMBER | STATIC} {subprogram_body | call_spec};}
  [{MEMBER | STATIC} {subprogram_body | call_spec};]...
END;]

AUTHID句は、そのタイプのすべてのメンバー・メソッドをその定義者(AUTHID DEFINER、デフォルト)と実行者(AUTHID CURRENT_USER)のどちらの権限を使用して実行するかを指定します。スキーマ・オブジェクトに対する未修飾の参照を定義者と実行者のどちらのスキーマで解決するかも指定します。

この項の内容は次のとおりです。

7.5.1 属性について

オブジェクト型仕様部では、属性はすべてメソッドの前に宣言する必要があります。また、少なくとも1つの属性を宣言する必要があります。宣言できる属性の最大数は1000個です。メソッドはオプションです。

Java変数と同様に、属性は名前とデータ型で宣言します。名前はオブジェクト型内で一意であることが必要ですが、他のオブジェクト型では再利用できます。データ型は任意のSQL型に設定できますが、LONGLONG RAWNCHARNVARCHAR2NCLOBROWIDおよびUROWIDには設定できません。

代入演算子またはDEFAULT句を使用して宣言部で属性を初期化することはできません。また、NOT NULL制約を属性に課すこともできません。ただし、制約を課すことができるデータベース表にオブジェクトを格納することはできます。

7.5.2 メソッドの宣言

属性の宣言後に、メソッドを宣言できます。MEMBERメソッドは、オブジェクト型のインスタンスである、SELFと呼ばれる組込みパラメータを受け入れます。暗黙的に宣言されたか明示的に宣言されたかに関係なく、これは常に最初にMEMBERメソッドに渡されるパラメータです。メソッド本体では、SELFはメソッドがコールされたオブジェクトを示します。MEMBERメソッドは、次のように、インスタンスに対してコールされます。

instance_expression.method()

STATICメソッドは、SELFの受入れまたは参照ができず、次のように、インスタンスではなくオブジェクト型に対して起動されます。

object_type_name.method()

staticでないJavaメソッドをコールする場合は、コール仕様でキーワードMEMBERを指定する必要があります。同様に、staticのJavaメソッドをコールする場合は、コール仕様でキーワードSTATICを指定する必要があります。

この項では、次の項目について説明します。

7.5.2.1 マップ・メソッドとオーダー・メソッド

CHARなどのSQLスカラー・データ型の値には、事前定義済の順序があり、これによって他の値と比較できます。ただし、オブジェクト型のインスタンスには事前定義済の順序はありません。これらに順序を付けるには、SQLでユーザー定義のマップ・メソッドをコールします。

SQLは、順序付けを使用してx > yなどのブール式を評価し、DISTINCTGROUP BYおよびORDER BYの各句に伴う比較を行います。マップ・メソッドは、それらすべてのオブジェクトの順序付けにおけるオブジェクトの相対的な位置を戻します。オブジェクト型に含めることができるマップ・メソッドは1つのみで、戻り型がDATENUMBERまたはVARCHAR2のいずれかである、パラメータのないファンクションであることが必要です。

また、2つのオブジェクトを比較するオーダー・メソッドを持つSQLを提供することもできます。オーダー・メソッドでは、2つのパラメータ、つまり組込みパラメータSELFとそれと同じ型の別のオブジェクトのみが使用されます。o1o2のオブジェクトがある場合は、o1 > o2のような比較によってorderメソッドが自動的にコールされます。メソッドは、負数、0(ゼロ)または正数を戻し、それぞれSELFがもう一方のパラメータと比較して小さい、等しいまたは大きいことを示します。オブジェクト型に含めることができるオーダー・メソッドは1つのみで、それは数値結果を戻すファンクションであることが必要です。

マップ・メソッドまたはオーダー・メソッドのいずれかを宣言できますが、両方を宣言することはできません。どちらかのメソッドを宣言すると、SQLとPL/SQLでオブジェクトを比較できます。ただし、どちらのメソッドも宣言しない場合、比較できるのは、SQLのオブジェクトの等価または非等価のみです。

注意:

同じ型の2つのオブジェクトは、対応する属性の値が等しい場合は等価です。

7.5.2.2 コンストラクタ・メソッド

オブジェクト型にはすべて、そのオブジェクト型と名前が同じシステム定義のファンクションであるコンストラクタがあります。コンストラクタでは、そのオブジェクト型のインスタンスが初期化されて戻されます。

Oracle Databaseでは、すべてのオブジェクト型に対してデフォルト・コンストラクタが生成されます。コンストラクタの仮パラメータは、オブジェクト型の属性と一致します。つまり、パラメータと属性は同じ順序で宣言され、名前とデータ型が同じです。SQLではコンストラクタは暗黙的にはコールされません。そのため、明示的にコールする必要があります。コンストラクタ・コールは、ファンクション・コールが使用できる場所で使用できます。

注意:

JavaコンストラクタをSQLから起動するには、コンストラクタのコールをstaticメソッドにラップし、対応するコール仕様をオブジェクト型のSTATICメンバーとして宣言する必要があります。

7.5.2.3

この項の各例は、それぞれ前の例に基づきます。まず、部門と従業員を表す2つのSQLオブジェクト型を作成します。最初に、オブジェクト型Departmentの仕様部を記述します。仕様部で宣言されるのは属性のみであるため、本体は必要ありません。仕様部は次のとおりです。

CREATE TYPE Department AS OBJECT ( 
deptno NUMBER(2),
dname VARCHAR2(14),
loc VARCHAR2(13)
);

次に、オブジェクト型Employeeを作成します。deptno属性には、REFと呼ばれる、Department型のオブジェクトのハンドルが格納されています。REFは、オブジェクト型のインスタンスを格納するデータベース表である、オブジェクト表の中のオブジェクトの位置を示します。REFは、メモリーにある特定のインスタンスのコピーは指し示しません。REFを宣言するには、データ型REFREFの対象となるオブジェクト型を指定します。Employee型は、次のように作成されます。

CREATE TYPE Employee AS OBJECT (
empno NUMBER(4),
ename VARCHAR2(10),
job VARCHAR2(9),
mgr NUMBER(4),
hiredate DATE,
sal NUMBER(7,2),
comm NUMBER(7,2),
deptno REF Department
);

次に、Department型とEmployee型のオブジェクトを格納するSQLオブジェクト表を作成します。Department型のオブジェクトを格納するオブジェクト表deptsを作成します。リレーショナル表deptからデータを選択して、オブジェクト型と同じ名前のシステム定義のファンクションであるコンストラクタに渡すことによって、オブジェクト表にデータを移入します。コンストラクタを使用して、そのオブジェクト型のインスタンスを初期化して戻します。depts表は、次のように作成されます。

CREATE TABLE depts OF Department AS
SELECT Department(deptno, dname, loc) FROM dept;

Employee型のオブジェクトを格納するオブジェクト表empsを作成します。オブジェクト表empsの最後の列は、オブジェクト型Employeeの最後の属性に対応しており、Department型のオブジェクトへの参照が格納されます。この列に参照をフェッチするには、オブジェクト表の行に関連付けられた表の別名を引数として取る、演算子REFを使用します。emps表は、次のように作成されます。

CREATE TABLE emps OF Employee AS
SELECT Employee(e.employee_id, e.first_name, e.job_id, e.manager_id, e.hire_date, e.salary, e.commission_pct, 
(SELECT REF(d) FROM departments d WHERE d.department_id = e.department_id))
FROM employees e;

REFを選択すると、オブジェクトのハンドルが戻されます。オブジェクト自体は具体化されません。これを行うには、Oracleオブジェクト参照をサポートするoracle.sql.REFクラスのメソッドを使用できます。このクラスはoracle.sql.Datumのサブクラスで、標準のJDBCインタフェースoracle.jdbc2.Refを拡張します。

oracle.sql.STRUCTクラスの使用

さらに処理を続けて、Javaストアド・プロシージャを次のように記述します。Paymasterクラスには、従業員の賃金を計算する1つのメソッドがあります。oracle.sql.STRUCTクラスで定義されているgetAttributes()メソッドでは、属性の型に対するデフォルトのJDBCマッピングが使用されます。たとえば、NUMBERBigDecimalにマッピングされます。Paymasterクラスは、次のように作成されます。

import java.sql.*;
import java.io.*;
import oracle.sql.*;
import oracle.jdbc.*;
import oracle.oracore.*;
import oracle.jdbc2.*;
import java.math.*;

public class Paymaster
{
  public static BigDecimal wages(STRUCT e) throws java.sql.SQLException
  {
    // Get the attributes of the Employee object.
    Object[] attribs = e.getAttributes();
    // Must use numeric indexes into the array of attributes.
    BigDecimal sal = (BigDecimal)(attribs[5]); // [5] = sal
    BigDecimal comm = (BigDecimal)(attribs[6]); // [6] = comm
    BigDecimal pay = sal;
    if (comm != null)
      pay = pay.add(comm);
    return pay;
  }
}

wages()メソッドは値を戻すため、そのファンクションのコール仕様を次のように記述します。

CREATE OR REPLACE FUNCTION wages (e Employee) RETURN NUMBER AS
LANGUAGE JAVA
NAME 'Paymaster.wages(oracle.sql.STRUCT) return BigDecimal';

パッケージまたはオブジェクト型の内部で定義されていないため、これはトップレベルのコール仕様です。

SQLDataインタフェースの実装

オブジェクトの属性にさらに自然にアクセスできるように、SQLDataインタフェースを実装するJavaクラスを作成します。そのためには、SQLDataインタフェースで定義されているとおりに、readSQL()およびwriteSQL()メソッドを提供する必要があります。JDBCドライバはreadSQL()メソッドをコールして、データベースの値のストリームを読み取り、Javaクラスのインスタンスに移入します。次の例では、Paymasterを変更して、raiseSal()という2番目のメソッドを追加します。

import java.sql.*;
import java.io.*;
import oracle.sql.*;
import oracle.jdbc.*;
import oracle.oracore.*;
import oracle.jdbc2.*;
import java.math.*;

public class Paymaster implements SQLData
{
  // Implement the attributes and operations for this type.
  private BigDecimal empno;
  private String ename;
  private String job;
  private BigDecimal mgr;
  private Date hiredate;
  private BigDecimal sal;
  private BigDecimal comm;
  private Ref dept;

  public static BigDecimal wages(Paymaster e)
  {
    BigDecimal pay = e.sal;
    if (e.comm != null)
      pay = pay.add(e.comm);
    return pay;
  }

  public static void raiseSal(Paymaster[] e, BigDecimal amount)
  {
    e[0].sal = // IN OUT passes [0]
    e[0].sal.add(amount); // increase salary by given amount
  }

  // Implement SQLData interface.

  private String sql_type;

  public String getSQLTypeName() throws SQLException
  {
    return sql_type;
  }

  public void readSQL(SQLInput stream, String typeName) throws SQLException
  {
    sql_type = typeName;
    empno = stream.readBigDecimal();
    ename = stream.readString();
    job = stream.readString();
    mgr = stream.readBigDecimal();
    hiredate = stream.readDate();
    sal = stream.readBigDecimal();
    comm = stream.readBigDecimal();
    dept = stream.readRef();
  }

  public void writeSQL(SQLOutput stream) throws SQLException
  {
    stream.writeBigDecimal(empno);
    stream.writeString(ename);
    stream.writeString(job);
    stream.writeBigDecimal(mgr);
    stream.writeDate(hiredate);
    stream.writeBigDecimal(sal);
    stream.writeBigDecimal(comm);
    stream.writeRef(dept);
  }
}

パラメータがOracle.sql.STRUCTからPaymasterに変更されたため、wages()のコール仕様を次のように変更する必要があります。

CREATE OR REPLACE FUNCTION wages (e Employee) RETURN NUMBER AS
LANGUAGE JAVA
NAME 'Paymaster.wages(Paymaster) return BigDecimal';

新しいraiseSal()メソッドは戻り値がvoidであるため、そのプロシージャのコール仕様を次のように記述します。

CREATE OR REPLACE PROCEDURE raise_sal (e IN OUT Employee, r NUMBER)
AS LANGUAGE JAVA
NAME 'Paymaster.raiseSal(Paymaster[], java.math.BigDecimal)';

このコール仕様もトップレベルです。

オブジェクト型メソッドの実装

トップレベルのコール仕様wagesおよびraise_salを削除し、それらをオブジェクト型Employeeのメソッドとして再宣言するとします。オブジェクト型の仕様部では、すべてのメソッドを属性の後に宣言する必要があります。仕様部で宣言されるのは属性とコール仕様のみであるため、オブジェクト型の本体は必要ありません。Employeeオブジェクト型は、次のように再作成できます。

CREATE TYPE Employee AS OBJECT (
empno NUMBER(4),
ename VARCHAR2(10),
job VARCHAR2(9),
mgr NUMBER(4),
hiredate DATE,
sal NUMBER(7,2),
comm NUMBER(7,2),
deptno REF Department
MEMBER FUNCTION wages RETURN NUMBER
AS LANGUAGE JAVA
NAME 'Paymaster.wages() return java.math.BigDecimal',
MEMBER PROCEDURE raise_sal (r NUMBER)
AS LANGUAGE JAVA
NAME 'Paymaster.raiseSal(java.math.BigDecimal)'
);

次に、この変更に応じてPaymasterを変更します。SQLパラメータSELFは直接Javaパラメータthisに対応しているため、SELFIN OUT(プロシージャのデフォルト)として宣言されている場合でも、配列をraiseSal()に渡す必要はありません。

import java.sql.*;
import java.io.*;
import oracle.sql.*;
import oracle.jdbc.*;
import oracle.oracore.*;
import oracle.jdbc2.*;
import java.math.*;

public class Paymaster implements SQLData
{
  // Implement the attributes and operations for this type.
  private BigDecimal empno;
  private String ename;
  private String job;
  private BigDecimal mgr;
  private Date hiredate;
  private BigDecimal sal;
  private BigDecimal comm;
  private Ref dept;

  public BigDecimal wages()
  {
    BigDecimal pay = sal;
    if (comm != null)
      pay = pay.add(comm);
    return pay;
  }

  public void raiseSal(BigDecimal amount)
  {
    // For SELF/this, even when IN OUT, no array is needed.
    sal = sal.add(amount);
  }

  // Implement SQLData interface.

  String sql_type;

  public String getSQLTypeName() throws SQLException
  {
    return sql_type;
  }

  public void readSQL(SQLInput stream, String typeName) throws SQLException
  {
    sql_type = typeName;
    empno = stream.readBigDecimal();
    ename = stream.readString();
    job = stream.readString();
    mgr = stream.readBigDecimal();
    hiredate = stream.readDate();
    sal = stream.readBigDecimal();
    comm = stream.readBigDecimal();
    dept = stream.readRef();
  }

  public void writeSQL(SQLOutput stream) throws SQLException
  {
    stream.writeBigDecimal(empno);
    stream.writeString(ename);
    stream.writeString(job);
    stream.writeBigDecimal(mgr);
    stream.writeDate(hiredate);
    stream.writeBigDecimal(sal);
    stream.writeBigDecimal(comm);
    stream.writeRef(dept);
  }
}