ヘッダーをスキップ
Oracle Fusion Middleware Oracle Application Development Framework Fusion開発者ガイド
11gリリース1 (11.1.1.7.0)
B52028-05
  目次へ移動
目次

前
 
次
 

38 エンティティ・オブジェクトの高度な手法

この章では、ADFビジネス・コンポーネント・データ・モデル・プロジェクトのADFエンティティ・オブジェクトで使用する高度な手法について説明します。

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


注意:

この章で説明する例を試すには、2.4.4項「AdvancedEntityExamplesアプリケーション・ワークスペースのスタンドアロン・アプリケーション」の説明に従ってFusion Order DemoアプリケーションのStandaloneExamplesモジュールのAdvancedEntityExamplesワークスペースを使用します。Fusion Order Demoを取得およびインストールする方法については、2.2項「Fusion Order Demoアプリケーションの設定」を参照してください。


38.1 ドメインを使用したカスタム検証済データ型の作成

複数のエンティティ・オブジェクトで似た属性値に対して同じ妥当性検査を繰り返し行っている場合は、この検証をカプセル化した独自のデータ型を作成することで、時間と労力を節約できます。たとえば、ビジネス・ドメイン・レイヤーに、電子メール・アドレスを表す文字列を格納する大量のエンティティ・オブジェクト属性がある場合を考えてみてください。ビジネス・ドメイン・レイヤーのどこでもエンド・ユーザーが有効な電子メール・アドレスを入力できるように保証する方法としては、次のようなものがあります。

しかし、このような方法は、大規模なアプリケーションでは急速に冗漫になります。ADFビジネス・コンポーネントで提供されている代替手段を使用すると、電子メール・アドレスを表す独自のEmailAddressデータ型を作成できます。電子メール・アドレスの値に関するすべての妥当性検査をこの新しいカスタム・データ型にまとめた後は、アプリケーション内にある電子メール・アドレスを表すすべての属性の型として、EmailAddressを使用できます。このようにすると、属性値の意図が他の開発者に対していっそう明確になり、検証処理が1か所にまとまるためアプリケーションのメンテナンスが簡単になります。ADFビジネス・コンポーネントでは、これを開発者作成のデータ型ドメインと呼びます。

ドメインとは、StringNumberDateなどの基本データ型を拡張するJavaクラスであり、候補値が関連する妥当性検査に合格することを保証するコンストラクタ時の検証が追加されています。基本データ型検証、書式設定、カスタム・メタデータ・プロパティなどの横断的動作を備えたカスタム・データ型を定義する手段を、ドメインを属性のJava型として使用する任意のエンティティ・オブジェクトまたはビュー・オブジェクトによって継承される方法で提供します。


注意:

この項の例では、Fusion Order DemoアプリケーションのStandaloneExamplesモジュールのAdvancedEntityExamplesアプリケーション・ワークスペースのSimpleDomainsプロジェクトを参照します。


38.1.1 ドメインの作成方法

ドメインを作成するには、ドメインの作成ウィザードを使用します。このウィザードは、「新規ギャラリ」「ADFビジネス・コンポーネント」カテゴリで使用できます。

ドメインを作成するには:

  1. アプリケーション・ナビゲータで、ドメインを作成するプロジェクトを右クリックし、「新規」を選択します。

  2. 「新規ギャラリ」で、「ビジネス層」を展開し、「ADFビジネス・コンポーネント」を選択します。次に「ドメイン」を選択し、「OK」をクリックします。

    これにより、ドメイン作成ウィザードが起動します。

  3. 「名前」ページで、ドメインの名前とドメインが存在するパッケージを指定します。単純なJava型に基づくドメインを作成する場合は、「Oracleオブジェクト型のドメイン」を選択しないでおきます。

  4. 「次へ」をクリックします。

  5. 「設定」ページで、ドメインのベース型と、マップするデータベース列の型を指定します。たとえば、8文字の短い電子メール・アドレスを保持するShortEmailAddressという名前のドメインを作成する場合は、ベース型にStringを設定し、「データベース列の型」VARCHAR2(8)を設定します。このパネルでは、他の共通属性も設定できます。

  6. 「終了」をクリックしてドメインを作成します。

38.1.2 ドメインの作成時に行われる処理

ドメインを作成すると、プロジェクトのソース・パスのサブディレクトリに、選択したパッケージ名に対応するXMLコンポーネント定義が作成されます。たとえば、devguide.advanced.domainsパッケージにShortEmailAddressドメインを作成すると、./devguide/advanced/domainsサブディレクトリにShortEmailAddress.xmlファイルが作成されます。ドメインには常に対応するJavaクラスがあり、ドメインが存在するパッケージのcommonサブパッケージに作成されます。つまり、devguide.advanced.domains.commonパッケージにShortEmailAddress.javaクラスが作成されます。ドメインのJavaクラスは、組込みデータ型と同じように動作する適切なコードを備えて自動的に生成されます。

38.1.3 ドメインについて

ここでは、ドメインを使用する際に理解しておく必要のある事項について説明します。

38.1.3.1 エンティティ・オブジェクトおよびビュー・オブジェクトの属性に対するドメインの使用

プロジェクトでドメインを作成すると、エンティティ・オブジェクトおよびビュー・オブジェクトのウィザードとダイアログで「属性」の「型」ドロップダウン・リストに、使用可能なデータ型の1つとして表示されます。特定の属性の型としてドメインを使用するには、リストから選択します。


注意:

エンティティ・ベースのビュー・オブジェクトにおけるエンティティ・マップの属性は、対応する基礎のエンティティ・オブジェクト属性からデータ型を継承するので、エンティティ属性でドメイン型を使用すると、対応するビュー・オブジェクトの属性もその型になります。ビュー・オブジェクトの一時属性またはSQL導出属性の場合は、基礎のエンティティから継承しないため、ドメインを使用するように型を直接設定できます。


38.1.3.2 妥当性検査が失敗した場合は検証メソッドからDataCreationExceptionをスローする

通常、ドメインについてユーザーが行う必要のある唯一のコーディング作業は、生成されるvalidate()メソッドの内部のカスタム・コードを記述することです。validate()メソッドの実装では、設定している候補値の妥当性検査を行い、検証が失敗した場合はoracle.jboパッケージでDataCreationExceptionをスローします。

翻訳可能な例外メッセージをスローするには、例38-1で示されているようなメッセージ・バンドル・クラスを作成できます。このクラスは、ドメイン・クラス自体と同じcommonパッケージに作成します。メッセージ・バンドルは、{MessageKeyStringTranslatableMessageString}というペアの配列を返します。

例38-1 ドメイン例外メッセージのカスタム・メッセージ・バンドル・クラス

package devguide.advanced.simpledomains.common;

import java.util.ListResourceBundle;

public class ErrorMessages extends ListResourceBundle {
  public static final String INVALID_SHORTEMAIL = "30002";
  public static final String INVALID_EVENNUMBER = "30003";
  private static final Object[][] sMessageStrings = new String[][] {
      { INVALID_SHORTEMAIL,
        "A valid short email address has no @-sign or dot."},
      { INVALID_EVENNUMBER,
        "Number must be even."}
    };

  /**
   * Return String Identifiers and corresponding Messages
   * in a two-dimensional array.
   */
  protected Object[][] getContents() {
    return sMessageStrings;
  }
}

38.1.3.3 String値を集約するStringドメイン

StringはJDKのベース型であるため、Stringに基づくドメインは、private mData Stringメンバー・フィールドを集約して、ドメインが表す値を保持します。その後、クラスはADFランタイムが期待するDomainInterfaceとともにSerializableインタフェースも実装し、ADFコンポーネントのカスタム・クライアント・インタフェースのメソッドの引数または戻り型でドメインを使用できるようにします。

例38-2は、簡単なShortEmailAddressドメイン・クラスのvalidate()メソッドを示しています。このメソッドは、mData値にアットマークまたはドットが含まれないていないことを確認し、含まれている場合は、翻訳可能なエラー・メッセージの適切なメッセージ・バンドルとメッセージ・キーを参照するDataCreationExceptionをスローします。

例38-2 カスタム検証を含む簡単なShortEmailAddress文字列ベース・ドメイン型

public class ShortEmailAddress
               extends Object implements DomainInterface, Serializable {
  private String mData;
  // . . .
  /**Implements domain validation logic and throws a JboException on error. */
  protected void validate() {
    int atpos = mData.indexOf('@');
    int dotpos = mData.lastIndexOf('.');
    if (atpos > -1 || dotpos > -1) {
      throw new DataCreationException(ErrorMessages.class,
      ErrorMessages.INVALID_SHORTEMAIL,null,null);
    }
  }
  // . . .
} 

38.1.3.4 既存のドメイン型を拡張する他のドメイン

oracle.jbo.domainパッケージの組込み型に基づく他の簡単なドメインは、例38-3に示すように、ベース型を拡張します。この例では、偶数を表すEvenNumberという名前のNumberベースのドメインに対するvalidate()メソッドが示されています。

例38-3 カスタム検証を含む簡単なEvenNumber Numberベース・ドメイン型

public class EvenNumber extends Number {
  // . . .
  /**
   * Validates that value is an even number, or else
   * throws a DataCreationException with a custom
   * error message.
   */
  protected void validate() {
    if (getValue() % 2 == 1) {
      throw new DataCreationException(ErrorMessages.class,
      ErrorMessages.INVALID_EVENNUMBER,null,null);
    }
  }
  // . . .
}

38.1.3.5 不変なJavaクラスである単純なドメイン

基本データ型に基づいて作成された単純なドメインは、不変なクラスになります。つまり、次のようにして新しいインスタンスを作成します。

ShortEmailAddress email = new ShortEmailAddress("emailaddress1");

このようにして作成したインスタンスの値は変更できません。別の短い電子メール・アドレスを参照する場合は、別のインスタンスを作成します。

ShortEmailAddress email = new ShortEmailAddress("emailaddress2");

これは新しい概念ではなく、StringNumberDateなどのクラスと同じ動作です。

38.1.3.6 Oracleオブジェクト型に対するドメインの作成

Oracleデータベースでは、データベースでユーザー定義型を作成できます。たとえば、次のDDL文を使用してPOINT_TYPEという名前の型を作成できます。

create type point_type as object (
   x_coord number,
   y_coord number
);

POINT_TYPEのようなユーザー定義型を使用する場合は、その型に基づくドメインを作成したり、オブジェクト型の列を含む表をリバース・エンジニアリングしてドメインを自動的に作成したりできます。

Oracleオブジェクト型ドメインの手動作成

自分でドメインを作成するには、ドメイン作成ウィザードで次の手順を実行します。

  1. アプリケーション・ナビゲータで、ドメインを作成するプロジェクトを右クリックし、「新規」を選択します。

  2. 「新規ギャラリ」で、「ビジネス層」を展開し、「ADFビジネス・コンポーネント」を選択します。次に「ドメイン」を選択し、「OK」をクリックします。

    これにより、ドメイン作成ウィザードが起動します。

  3. 「名前」ページで、「Oracleオブジェクト型のドメイン」チェック・ボックスを選択し、「使用可能な型」リストからドメインを作成するオブジェクト型を選択します。

  4. 「次へ」をクリックします。

  5. 「設定」ページで、「属性」ドロップダウン・リストを使用して複数のドメイン・プロパティを切り替え、適切に設定します。

  6. 「終了」をクリックしてドメインを作成します。

Oracleオブジェクト型ドメインのリバース・エンジニアリング

オブジェクト型ドメインを手動で作成する以外にも、表からのビジネス・コンポーネント・ウィザードを使用してOracleオブジェクト型の列を含む表を選択すると、JDeveloperによってそれらのオブジェクト型のドメインがリバース・エンジニアリング・プロセスの一部として作成されます。たとえば、POINT_TYPE型の列を含む次のような表を作成するものとします。

create table interesting_points(
  id number primary key,
  coordinates point_type,
  description varchar2(20)
);

表からのビジネス・コンポーネント・ウィザードでINTERESTING_POINTS表に対するエンティティ・オブジェクトを作成すると、InterestingPointsエンティティ・オブジェクトとPointTypeドメインの両方を取得できます。後者がPOINT_TYPEオブジェクト型に基づいて自動的に作成されるのは、InterestingPointsエンティティ・オブジェクトのCoordinates属性のデータ型として必要であるためです。

単純なドメインとは異なり、オブジェクト型ドメインは可変です。オブジェクト型の構造体の各要素に対し、ドメイン・クラスにgetterメソッドとsetterメソッドが生成されます。ドメインのプロパティを変更した後、そのドメインをビュー・オブジェクトまたはエンティティ・オブジェクトの属性として設定するとき、ドメインは1つの単位として扱われます。ADFは、ドメインのプロパティの変更は追跡せず、ドメインが値として使用される属性値の変更のみを追跡します。


注意:

Oracleオブジェクト型に基づくドメインは、基礎となる型がOracleオブジェクト型であるデータをプログラムで処理するときに便利です。また、ストアド・プロシージャとの間での構造体情報の受け渡しも簡単になります。ただし、ADFバインディング・レイヤーでのオブジェクト型ドメインの処理のサポートは完全であるため、宣言的にデータバインドされたユーザー・インタフェースでオブジェクト・ドメインを値とする属性を使用することは容易ではありません。


38.1.3.7 ドメイン・クラスへの素早い移動

アプリケーション・ナビゲータでドメインを選択した後、次のいずれかの方法で実装クラスに素早く移動できます。

  • アプリケーション・ナビゲータでドメインを右クリックして、コンテキスト・メニューから「ドメイン・クラスに移動」を選択します。

  • 構造ウィンドウで、ドメイン・クラスをダブルクリックします。

38.1.3.8 共通JARへのドメインのパッケージ化

37.6項「再利用可能なビジネス・コンポーネントのライブラリでの作業」で説明されているように、ビジネス・コンポーネント・アーカイブを作成すると、プロジェクトのソース・パスの*.commonサブディレクトリにあるドメイン・クラスとメッセージ・バンドル・ファイルは、*CSCommon.jarにパッケージ化されます。これらは、サポートする必要のある中間層アプリケーション・サーバーと最終的なリモート・クライアントの両方に共通のクラスです。

38.1.3.9 カスタム・ドメインのプロパティを継承するエンティティ・オブジェクトとビュー・オブジェクトの属性

ドメインではカスタム・メタデータ・プロパティを定義できます。そのようなドメインに基づくエンティティ・オブジェクトまたはビュー・オブジェクトの属性は、属性自体で定義されている場合と同じように、これらのカスタム・プロパティを継承します。エンティティ・オブジェクトまたはビュー・オブジェクトの属性で同じカスタム・プロパティが定義されている場合は、その設定が、ドメインから継承する値より優先されます。

38.1.3.10 エンティティ・レベルまたはビュー・レベルでは制限を緩めることができないドメイン設定

JDeveloperでは、ドメイン定義レベルで適用されている宣言的な設定を施行します。この設定は、ドメイン型に基づく属性のエンティティ・オブジェクトまたはビュー・オブジェクトでは制限を緩和できません。たとえば、ドメインの「更新可能」プロパティを「新規の間」に設定した場合、そのドメインをエンティティ・オブジェクト属性のJava型として使用するときに、「更新可能」「なし」(より厳しい制限)に設定することはできますが、「常に」に設定することはできません。同様に、ドメインを「永続的」に定義した場合、後でそれを一時的なものにすることはできません。アプリケーションで使用するときは、ドメインの宣言的プロパティはできるかぎり緩く設定し、後で必要に応じて厳しくできるようにします。

38.2 行の削除に代わる削除済フラグの更新

監査のため、表に追加した行を表から物理的に削除できない場合があります。そのような場合は、エンド・ユーザーがユーザー・インタフェースで行を削除する際に、DELETED列の値をNからYに変更し、削除済としてマークします。この処理を行うために、2つのメソッドのオーバーライドを使用して、エンティティ・オブジェクトのデフォルト動作を変更できます。たとえば、Fusion Order DemoアプリケーションのProductsエンティティをこのように動作するように変更できます。

38.2.1 行が削除されたときに削除済フラグを更新する方法

行が削除されたときに削除済フラグを更新するには、エンティティ・オブジェクトのカスタムJavaクラスを有効にし、remove()メソッドをオーバーライドして、super.remove()メソッドを呼び出す前に削除済フラグを設定するようにします。例38-4は、エンティティ・オブジェクトのカスタムJavaクラスが、この変更でどのようになるかを示しています。削除された行の属性を設定しようとするとDeadEntityAccessExceptionが発生するため、super.remove()を呼び出すに属性を設定することが重要です。

この例では、PRODUCTS表を変更してDELETED列を追加し、Productsエンティティとデータベースの同期を取って、対応するDeleted属性を追加してあるものとします。

例38-4 Productエンティティの行が削除されたときの削除済フラグの更新

// In your custom Java entity class
public void remove() {
  setDeleted("Y");
  super.remove();
}

行は行セットから削除されますが、エンティティ・キャッシュでDeletedフラグの値はYに変更されています。この動作の実装の第2の部分は、DML操作の実行を要求されたときに、エンティティにDELETEのかわりにUPDATEの実行を強制することです。完全なソリューションのためには、両方の部分を実装する必要があります。

38.2.2 削除のかわりに更新DML操作の強制

エンティティ・オブジェクトを削除のかわりに更新するには、doDML()メソッドをオーバーライドし、operationフラグを条件付きで変更するコードを記述します。操作フラグがDML_DELETEの場合は、このコードでかわりにDML_UPDATEに変更します。例38-5は、エンティティ・オブジェクトのカスタムJavaクラスが、この変更でどのようになるかを示しています。

この例では、PRODUCTS表を変更してDELETED列を追加し、Productsエンティティとデータベースの同期を取って、対応するDeleted属性を追加してあるものとします。

例38-5 削除のかわりの更新DML操作の強制

// In your custom Java entity class
protected void doDML(int operation, TransactionEvent e) {
   if (operation == DML_DELETE) {
     operation = DML_UPDATE;
   }
   super.doDML(operation, e);
  }

オーバーライドしたdoDML()メソッドを配置し、38.2.1項で説明したオーバーライドされたremove()メソッドを補完することで、対応するエンティティ・オブジェクトの慣用名を持つビュー・オブジェクトからエンティティを削除しようとすると、行が物理的に削除されるかわりに、列DELETEDが更新されます。削除された製品がビュー・オブジェクトの問合せ結果に表示されることを防ぐため、WHERE DELETED = 'N'の製品のみを含むようにWHERE句を変更する必要があります。

38.3 バッチ更新の使用

バッチ更新を使用すると、複数のエンティティ変更の際に発行されるDML文の数を減らすことができます。

デフォルトで、ADFビジネス・コンポーネント・フレームワークは、指定されたエンティティ定義型の変更済エンティティ1つにつき、1つのDML文(INSERTUPDATEDELETE)を実行します。たとえば、Employeeエンティティ・オブジェクト型があり、アプリケーションの通常使用でその複数のインスタンスが変更されたとします。2つのインスタンスが作成され、3つの既存のインスタンスが変更され、4つの既存のインスタンスが削除された場合、それらの変更を保存するために、トランザクション・コミット時間に、フレームワークにより9つのDML文(2つのINSERT文、3つのUPDATE文および4つのDELETE文)が発行されます。

トランザクションで、ある型の複数のエンティティを頻繁に更新する場合、そのエンティティ定義型に対してバッチ更新機能を使用することを検討してください。この例では、バッチ更新(しきい値は1)でフレームワークが発行するDML文は、2件の挿入を処理する1つの一括INSERT文、3件の更新を処理する1つの一括UPDATE文および4件の削除を処理する1つの一括DELETE文の3つのみです。


注意:

次のいずれかの場合、バッチ更新機能は無効になります。

  • エンティティ・オブジェクトにはBLOB型またはCLOB型の属性があります。ストリーミング・データ型を持つバッチDMLはサポートされていません。

  • Refresh After InsertまたはRefresh After Updateに設定されている属性がエンティティ・オブジェクトにあります。1回のラウンド・トリップでトリガー割当て値のすべてを一括で戻すメソッドがないため、属性の取得や更新を行う処理はバッチDMLで動作しません。

  • 主キーがない表からエンティティ・オブジェクトを作成しました。表に主キーがなく、その表からエンティティ・オブジェクトのリバース・エンジニアリングを行った場合、ROWID値属性が作成されて、かわりに主キーとして割り当てられます。ROWID値は、挿入時に取得スタイルの値として管理されるため、バッチDMLで動作しません。


エンティティに対してバッチ更新を有効化する手順:

  1. 概要エディタで目的のエンティティ・オブジェクトを開きます。

  2. アプリケーション・ナビゲータで、適切なエンティティをダブルクリックし、概要エディタで開きます。

  3. 概要エディタの「一般」ページで「チューニング」セクションを展開し、「バッチ更新を使用」チェック・ボックスを選択して、適切なしきい値を指定します。

これにより、それを超えるとADFが一括DML操作を使用して変更を処理する、バッチ処理のしきい値が指定されます。

38.4 エンティティ・アソシエーションの高度な手法

ここでは、エンティティ・オブジェクト間のアソシエーションの処理に関する高度な手法について説明します。

38.4.1 複雑なアソシエーションを実装するためのアソシエーションSQL句の変更

対応する属性の等価性のみに基づく関連より複雑なエンティティ間の関連を表す必要があるときは、アソシエーションのSQL句を変更して、さらに複雑な条件を組み込むことができます。たとえば、2つのエンティティ間の関連が有効期間に依存する場合があります。ProductSupplierと関連付けることができても、サプライヤの名前が時間とともに変化する場合は、その製品行が使用されている(または、使用されていた)期間を記録するEFFECTIVE_FROM列とEFFECTIVE_UNTIL列が、SUPPLIERS表の各行に追加されている可能性があります。その場合、Productとそれが関連付けられるSupplierの間の関係は、SupplierId属性の一致と、製品のRequestDateがサプライヤのEffectiveFromの日付とEffectiveUntilの日付の間にあるという条件の組合せによって記述されます。

このような複雑な関係は、概要エディタで、アソシエーションに対して設定できます。まず、「関係」ページで、必要な追加属性のペアを追加します。この例では、(EffectiveFromRequestDate)のペアと、(EffectiveUntilRequestDate)のペアです。次に、「問合せ」ページで「WHERE」フィールドを編集して、WHERE句を次のように変更できます。

(:Bind_SupplierId = Product.SUPPLIER_ID) AND
(Product.REQUEST_DATE BETWEEN :Bind_EffectiveFrom
                                 AND :Bind_EffectiveUntil)

アソシエーションの作成の詳細は、4.3項「アソシエーションの作成および構成」を参照してください。

38.4.2 エンティティ・レベルでのビュー・リンク・アクセッサ属性の公開

2つのエンティティ・ベースのビュー・オブジェクト間のビュー・リンクを作成する際、「ビュー・リンク・プロパティ」ページに、ビュー・オブジェクト・レベルとエンティティ・オブジェクト・レベルの両方でビュー・リンク・アクセッサ属性を公開するオプションがあります。デフォルトでは、ビュー・リンク・アクセッサは、リンク先ビュー・オブジェクトのビュー・オブジェクト・レベルでのみ公開されます。「エンティティ・オブジェクト: {0}SourceEntityName」チェック・ボックスまたは「エンティティ・オブジェクト: {0}DestinationEntityName」チェック・ボックスから適切なものを選択することで、リンク元またはリンク先のエンティティ・オブジェクトの一方または両方にビュー・リンク属性を含めることができます。これにより、エンティティ・オブジェクトが関連するビュー行のセットにアクセスするための便利な方法が提供されます。特に、行を生成するための問合せが現在の行の属性にのみ依存する場合に有効です。

38.4.3 行セットを保持することによるエンティティ・アクセッサのアクセスの最適化

デフォルトでは、エンティティ・アソシエーション・アクセッサの行セットを取得するたびに、エンティティ・オブジェクトは新しいRowSetオブジェクトを作成し、ユーザーが行を操作できるようにします。これは、そのたびに問合せを再実行して結果を生成するという意味ではなくRowSetオブジェクトの新しいインスタンスを作成してデフォルトのイテレータを先頭行の前のスロットにリセットするだけです。行セットをデータベースの行で強制的に更新するには、executeQuery()メソッドを呼び出します。

行セットの作成には若干のオーバーヘッドが伴うため、同じアソシエーション・アクセッサ属性に対して大量の呼出しを行うコードの場合は、アソシエーションのリンク元エンティティ・オブジェクトに対してアソシエーション・アクセッサ行セットの保持を有効にすることを検討する余地があります。アソシエーション・アクセッサのソースであるエンティティ・オブジェクトについて、概要エディタを使用してアソシエーション・アクセッサ行セットの保存を有効にできます。エンティティ・オブジェクトに対して、概要エディタの「一般」ページの「チューニング」セクションで、「アソシエーション・アクセッサ行セットの保存」を選択します。

エンティティ・オブジェクトに対してカスタムJavaエンティティ・コレクション・クラスを有効にすることもできます。他のカスタム・エンティティJavaクラスと同様に、この設定は、エンティティ・オブジェクトの概要エディタの「Java」ページで開く「Javaオプションの選択」ダイアログを選択して実行します。ダイアログでは、「エンティティ・コレクション・クラスの生成」を選択します。次に、JDeveloperによって自動的に作成されるYourEntityCollImplクラスで、init()メソッドをオーバーライドし、setAssociationAccessorRetained()メソッドを呼び出してパラメータとしてtrueを渡す行を、super.init()の後に追加します。これは、そのエンティティ・オブジェクトのすべてのアソシエーション・アクセッサ属性に適用されます。

エンティティ・オブジェクトに対してこの機能を有効にすると、アソシエーション・アクセッサの行セットが毎回再作成されないため、デフォルトの行セット・イテレータの現在行も維持されるという副作用があります。つまり、デフォルト行セット・イテレータの現在行をリセットして先頭行の前のスロットに戻すには、アソシエーション・アクセッサから取得した行セットに対して、reset()メソッドを明示的に呼び出す必要があります。

ただし、アクセッサの保持を有効にすると、アクセッサ行セットの行を反復処理する前にいつでもreset()を呼び出すようにしないと、微妙で検出が困難なエラーがアプリケーションで発生する可能性があることに注意してください。たとえば、アソシエーション・アクセッサ行セットの行を例38-6に示すように反復処理し、合計を計算するものとします。

例38-6 行セットの誤った反復

// In your custom Java entity classRowSet rs = (RowSet)getProducts();
while (rs.hasNext()) {
  ProductImpl r = (ProductImpl)rs.next();
  // Do something important with attributes in each row
}

アクセッサ行セットを最初に処理するときには、このコードは正しく動作します。しかし、行セット(およびそのデフォルトの行セット・イテレータ)が保持されるため、2回目以降の行セットへのアクセスでは、現在行はすでに行セットの末尾にあり、rs.hasNext()falseになるためwhileループはスキップされます。この機能を有効にする場合は、アクセッサの反復コードは例38-7に示すように記述する必要があります。

例38-7 行セットの反復と最初の行のリセット

// In your custom Java entity class
RowSet rs = (RowSet)getProducts();
rs.reset(); // Reset default row set iterator to slot before first row!
while (rs.hasNext()) {
  ProductImpl r = (ProductImpl)rs.next();
  // Do something important with attributes in each row
}

38.5 PL/SQLパッケージAPIに基づくエンティティ・オブジェクト

基礎となる表に対する挿入、更新、および削除権限をカプセル化するPL/SQLパッケージがある場合は、その表を表すエンティティ・オブジェクトに対するデフォルトのDML処理イベントをオーバーライドし、かわりにPL/SQL APIの中でプロシージャを呼び出すことができます。通常、このようなPL/SQLパッケージは、付随するデータベース・ビューと組み合せて使用されます。クライアント・プログラムは、データベース・ビューを使用して基になる表からデータを読み取り、PL/SQLパッケージのプロシージャを使用してデータを表に書き戻します。

たとえば、このようなビューとパッケージの組合せに基づいてProductエンティティ・オブジェクトを作成するとします。

Fusion Order DemoスキーマのPRODUCTS表を使用し、次のDDL文を使用して作成されるPRODUCTS_Vという名前のデータベース・ビューについて考えます。

create or replace view products_v
as select product_id,name,image,description from products;

さらに、基になるPRODUCTS表に対する挿入、更新、削除権限をカプセル化する、例38-8で示されるような簡単なPRODUCTS_APIパッケージを使用します。

例38-8 PRODUCTS表に対する簡単なPL/SQLパッケージAPI

create or replace package products_api is
  procedure insert_product(p_prod_id number,
                           p_name varchar2,
                           p_supplier_id number,
                           p_list_price number,
                           p_min_price number,
                           p_ship_code varchar2);
  procedure update_product(p_prod_id number,
                           p_name varchar2,
                           p_supplier_id number,
                           p_list_price number,
                           p_min_price number,
                           p_ship_code varchar2);
  procedure delete_product(p_prod_id number);
end products_api;

この組合せのデータベース・ビューとPL/SQLパッケージに基づいてエンティティ・オブジェクトを作成するには:


注意:

この項の例では、Fusion Order DemoアプリケーションのStandaloneExamplesモジュールのAdvancedEntityExamplesアプリケーション・ワークスペースのEntityWrappingPLSQLPackageプロジェクトを参照します。


38.5.1 ビューを基にしてエンティティ・オブジェクトを作成する方法

ビューを基にしてエンティティ・オブジェクトを作成するには、4.2.2項「エンティティの作成ウィザードで単一のエンティティ・オブジェクトを作成する方法」の説明に従ってエンティティ・オブジェクト作成ウィザードを使用します。ただし、次の例外があります。

  • 「名前」ページで、Productのようなエンティティ名を設定し、「データベース・オブジェクト」セクションの下部にある「ビュー」チェック・ボックスを選択します。

    現在のスキーマで使用できるデータベース・ビューが、「スキーマ・オブジェクト」リストに表示されます。

  • 「スキーマ・オブジェクト」リストで、目的のデータベース・ビューを選択します。

  • 「属性の設定」ページで、「属性の選択」ドロップダウン・リストを使用して主キーとして機能する属性を選択し、そのプロパティの「主キー」の設定を有効にします。


    注意:

    ビューを基にしてエンティティを定義するときは、データ・ディクショナリにデータベース・ビューに関連する制約がないため、主キー属性を自動的に決定することはできません。


38.5.2 ビューを基にしてエンティティ・オブジェクトを作成するときに行われる処理

ビューに基づくエンティティ・オブジェクトは、デフォルトで、基礎となるデータベース・ビューに対して次のすべての文を直接実行します。

  • SELECT文(findByPrimaryKey()の場合)

  • SELECT FOR UPDATE文(lock()の場合)

  • INSERTUPDATEDELETE文(doDML()の場合)

ストアド・プロシージャ呼出しを使用するには、(38.5.3項「PL/SQLベースのエンティティに関する詳細のベース・クラスへの集中化」の説明に従って)doDML()操作をオーバーライドし、必要に応じて(38.5.4項「DML操作のストアド・プロシージャ呼出しの実装」の説明に従って)lock()およびfindByPrimaryKey()の処理をオーバーライドする必要があります。

38.5.3 PL/SQLベースのエンティティに関する詳細のベース・クラスへの集中化

PL/SQL APIに基づくエンティティ・オブジェクトが複数ある場合は、一般的な詳細をベース・フレームワーク拡張クラスに抽象化するのがよい方法です。その際、第37章「ビジネス・コンポーネントの高度な手法」に説明されているいくつかの概念を使用します。まず、ベースEntityImplクラスを拡張するPLSQLEntityImplクラスを作成します。このクラスは、PL/SQLベースの各エンティティがベース・クラスとして使用できます。例38-9に示すように、ベース・クラスのdoDML()メソッドをオーバーライドし、操作に基づいて異なるヘルパー・メソッドを呼び出します。


注意:

エンティティにすでに拡張したエンティティ実装クラスを使用している場合、PLSQLEntityImplクラスを使用してさらに拡張できます。たとえば、zzEntityImplというフレームワーク拡張クラスがある場合、zzEntityImplクラスを拡張するPLSQLEntityImplクラスを作成します。


例38-9 操作に基づいて異なるプロシージャを呼び出すためのdoDML()のオーバーライド

// In PLSQLEntityImpl.java
protected void doDML(int operation, TransactionEvent e) {
  // super.doDML(operation, e);
  if (operation == DML_INSERT)
    callInsertProcedure(e);
  else if (operation == DML_UPDATE)
    callUpdateProcedure(e);
  else if (operation == DML_DELETE)
    callDeleteProcedure(e);
}

ベース・クラスPLSQLEntityImpl.javaでは、次に示すように、デフォルトの処理を実行するようにヘルパー・メソッドを記述できます。

// In PLSQLEntityImpl.java
/* Override in a subclass to perform non-default processing */
protected void callInsertProcedure(TransactionEvent e) {
  super.doDML(DML_INSERT, e);
}
/* Override in a subclass to perform non-default processing */  
protected void callUpdateProcedure(TransactionEvent e) {
  super.doDML(DML_UPDATE, e);
}
/* Override in a subclass to perform non-default processing */  
protected void callDeleteProcedure(TransactionEvent e) {
  super.doDML(DML_DELETE, e);
}

このインフラストラクチャを配置した後、PLSQLEntityImplクラスに基づくエンティティ・オブジェクトでは、「ソース」→「メソッドのオーバーライド」メニュー項目を使用して、callInsertProcedure()callUpdateProcedure()およびcallDeleteProcedure()の各ヘルパー・メソッドをオーバーライドして、その特定のエンティティに適したストアド・プロシージャ呼出しを実行できます。


注意:

これらのヘルパー・メソッドをサブクラスでオーバーライドしない場合、スーパークラスで定義されているデフォルトの処理が実行されます。代替処理を提供するdoDML()メソッドで操作のオーバーライドのみを行います。


これらの呼出しを実装する作業を簡単にするには、(第37章「ストアド・プロシージャとストアド・ファンクションの呼出し」で説明した)callStoredProcedure()ヘルパー・メソッドをPLSQLEntityImplクラスに追加することもできます。このようにすると、このクラスを拡張するPL/SQLベースのエンティティ・オブジェクトはすべて、ヘルパー・メソッドを利用できます。

38.5.4 DML操作のストアド・プロシージャ呼出しの実装

DML操作に対するストアド・プロシージャ呼出しを実装するには、エンティティ・オブジェクトのカスタムJavaクラスを作成して、その中の操作をオーバーライドする必要があります。

メソッドのオーバーライドでカスタムJavaクラスを作成するには:

  1. アプリケーション・ナビゲータで、Productエンティティ・オブジェクトをダブルクリックし、概要エディタで開きます。

  2. 概要エディタの「Java」ページで、「Javaオプションの編集」アイコンをクリックします。

  3. 「Javaオプションの選択」ダイアログで、「クラスの拡張」をクリックします。

  4. 「ベース・クラスのオーバーライド」ダイアログで、「行」フィールドにPLSQLEntityImplクラスのパッケージとクラスを入力するか、「参照」をクリックして検索して選択します。

  5. 次に、「エンティティ・オブジェクト・クラスの生成」を選択して「OK」をクリックします。

  6. アプリケーション・ナビゲータで、ProductsImpl.javaをダブルクリックして概要エディタで開きます。

  7. 「ソース」メニューで「メソッドのオーバーライド」を選択します。

  8. 「メソッドのオーバーライド」ダイアログで、callInsertProcedure()callUpdateProcedure()およびcallDeleteProcedure()の各メソッドを選択し、次に「OK」をクリックします。

  9. 次に、必要なコードを入力してこられのプロシージャをオーバーライドします。

例38-10は、これらのオーバーライドされるヘルパー・メソッドに記述するコードを示しています。

例38-10 ヘルパー・メソッドを利用した挿入、更新、削除プロシージャの呼出し

// In ProductsImpl.java
protected void callInsertProcedure(TransactionEvent e) {
    callStoredProcedure("products_api.insert_product(?,?,?,?,?,?)",
         new Object[] { getProductId(), getProductName(), getSupplierId(),
                getListPrice(), getMinPrice(), getShippingClassCode() });
}
protected void callUpdateProcedure(TransactionEvent e) {
    callStoredProcedure("products_api.update_product(?,?,?,?,?,?)",
         new Object[] { getProductId(), getProductName(), getSupplierId(),
                        getListPrice(), getMinPrice(), getShippingClassCode() });
}
protected void callDeleteProcedure(TransactionEvent e) {
    callStoredProcedure("products_api.delete_product(?)",
         new Object[] { getProductId() });
}

この段階で、Productsエンティティ・オブジェクトに対するProductsという名前のデフォルトのエンティティ・ベースのビュー・オブジェクトを作成し、そのインスタンスをProductsModuleアプリケーション・モジュールに追加すると、ビジネス・コンポーネント・ブラウザで、Productsビュー・オブジェクト・インスタンスの行の挿入、更新、削除を簡単にテストできます。

通常は、挿入、更新、および削除の操作をオーバーライドするのみで十分です。データベース・ビューに対してfindByPrimaryKey()SELECT文およびlock()SELECT FOR UPDATE 文を実行するデフォルトの動作は、ほとんどの基本的な種類のビューで動作します。

ただし、ビューが複雑でSELECT FOR UPDATEをサポートしない場合、または他のストアド・プロシージャAPIを使用してfindByPrimaryKey()機能およびlock()機能を実行する必要がある場合は、38.5.5項「選択処理およびロック処理の追加」に説明されている方法を実行してもかまいません。

38.5.5 選択処理およびロック処理の追加

必要に応じて、ストアド・プロシージャを呼び出して、エンティティ・オブジェクトのlock()およびfindByPrimaryKey()機能を処理できます。PRODUCTS_APIパッケージを更新し、例38-11で示されている2つのプロシージャを追加したものとします。lock_productプロシージャとselect_productプロシージャはどちらも、INパラメータとして主キー属性を受け取り、OUTパラメータを使用して残りの属性の値を返します。

例38-11 PRODUCTS表用に追加するロックおよび選択プロシージャ

/* Added to PRODUCTS_API package */
  procedure lock_product(p_prod_id number,
                         p_name OUT varchar2,
                         p_supplier_id OUT number,
                         p_list_price OUT number,
                         p_min_price OUT number,
                         p_ship_code OUT varchar2);
  procedure select_product(p_prod_id number,
                         p_name OUT varchar2,
                         p_supplier_id OUT number,
                         p_list_price OUT number,
                         p_min_price OUT number,
                         p_ship_code OUT varchar2); 

38.5.5.1 ロックと選択を処理するためのPLSQLEntityImplベース・クラスの更新

挿入、更新、削除用に追加したものと似たヘルパー・メソッドを使用して、lock()およびfindByPrimaryKey()のオーバーライドを処理するように、PLSQLEntityImplベース・クラスを拡張できます。実行時には、lock()操作とfindByPrimaryKey()操作はどちらも最終的に、doSelect(boolean lock)という名前の下位レベルのエンティティ・オブジェクト・メソッドを呼び出します。lock()操作はパラメータをtrue値にしてdoSelect()を呼び出しますが、findByPrimaryKey()操作はfalseを渡して呼び出します。

例38-12は、必要に応じてサブクラスがオーバーライドできる2つのヘルパー・メソッドに委譲するようオーバーライドされたPLSQLEntityImpldoSelect()メソッドを示します。

例38-12 ロック・パラメータに基づいて異なるプロシージャを呼び出すためのdoSelect()のオーバーライド

// In PLSQLEntityImpl.java
protected void doSelect(boolean lock) {
  if (lock) {
    callLockProcedureAndCheckForRowInconsistency();
  } else {
    callSelectProcedure();
  }
}

2つのヘルパー・メソッドは、ベース・クラスPLSQLEntityImplのデフォルト機能を実行するためだけに記述されています。

// In PLSQLEntityImpl.java
/* Override in a subclass to perform non-default processing */  
protected void callLockProcedureAndCheckForRowInconsistency() {
  super.doSelect(true);
}
/* Override in a subclass to perform non-default processing */  
protected void callSelectProcedure() {
  super.doSelect(false);
}

ロックを実行するヘルパー・メソッドの名前がcallLockProcedureAndCheckForRowInconsistency()であることに注意してください。これは、行をロックするときに、新しく選択される行の値が、エンティティ・キャッシュ内のエンティティ・オブジェクトが現在のデータベース値であると認識している値と同じかどうかを検出する検査を実行する必要があることを、開発者に伝えています。

この古い値と新しい値の属性の比較をサブクラスが実行するのを助けるため、次のようなfinalのヘルパー・メソッドをPLSQLEntityImplクラスに追加できます。

// In PLSQLEntityImpl
protected void compareOldAttrTo(int attrIndex, Object newVal) {
  if ((getPostedAttribute(attrIndex) == null && newVal != null) || 
      (getPostedAttribute(attrIndex) != null && newVal == null) || 
      (getPostedAttribute(attrIndex) != null && newVal != null && 
       !getPostedAttribute(attrIndex).equals(newVal))) {
    throw new RowInconsistentException(getKey());
  }
}

38.5.5.2 Productエンティティに対するロックおよび選択の実装

追加のインフラストラクチャをベース・クラスPLSQLEntityImplに置くと、Productエンティティ・オブジェクトのProductImplクラスのcallSelectProcedure()およびcallLockProcedureAndCheckForRowInconsistency()ヘルパー・メソッドをオーバーライドできます。select_productプロシージャとlock_productプロシージャはOUT引数を持っているため、37.4.4項「他の種類のストアド・プロシージャの呼出し方法」で説明したように、JDBCのCallableStatementオブジェクトを使用してこれらの呼出しを実行する必要があります。

例38-13は、select_productプロシージャを呼び出すために必要なコードを示しています。このコードが実行する基本的な手順は次のとおりです。

  1. PL/SQLブロックが呼び出すCallableStatementを作成します。

  2. 1から始まるバインド変数位置で、OUTパラメータと型を登録します。

  3. INパラメータの値を設定します。

  4. 文を実行します。

  5. 更新された列の値を取得します。

  6. 更新された属性値を行に移入します。

  7. 文を閉じます。

例38-13 主キーで行を選択するためのストアド・プロシージャの呼出し

// In ProductsImpl.java
protected void callSelectProcedure() {
    String stmt = "begin products_api.select_product(?,?,?,?,?,?);end;";
    // 1. Create a CallableStatement for the PLSQL block to invoke
    CallableStatement st = 
        getDBTransaction().createCallableStatement(stmt, 0);
    try {
        // 2. Register the OUT parameters and types
        st.registerOutParameter(2, VARCHAR2);
        st.registerOutParameter(3, NUMBER);
        st.registerOutParameter(4, NUMBER);
        st.registerOutParameter(5, NUMBER);
        st.registerOutParameter(6, VARCHAR2);

        // 3. Set the IN parameter value
        st.setObject(1, getProductId());
        // 4. Execute the statement
        st.executeUpdate();
        // 5. Retrieve the possibly updated column values
        String possiblyUpdatedName = st.getString(2);
        String possiblyUpdatedSupplierId = st.getString(3);
        String possiblyUpdatedListPrice = st.getString(4);
        String possiblyUpdatedMinPrice = st.getString(5);
        String possiblyUpdatedShipCode = st.getString(6);

        // 6. Populate the possibly updated attribute values in the row
        populateAttribute(PRODUCTNAME, possiblyUpdatedName, true, false, 
                                false);
        populateAttribute(SUPPLIERID, possiblyUpdatedSupplierId, true, 
                                false, false);
        populateAttribute(LISTPRICE, possiblyUpdatedListPrice, true, false, 
                                false);
        populateAttribute(MINPRICE, possiblyUpdatedMinPrice, true, false, 
                                false);
        populateAttribute(SHIPPINGCLASSCODE, possiblyUpdatedShipCode, true, 
                                false, false);
    } catch (SQLException e) {
        throw new JboException(e);
    } finally {
        if (st != null) {
            try {
                // 7. Closing the statement
                st.close();
            } catch (SQLException e) {
            }
        }
    }
}

例38-14は、lock_productプロシージャを呼び出すためのコードを示しています。基本的には例38-13と同じ手順を行いますが、次の2点のみ異なります。

  • OUTパラメータから更新された可能性のある列の値を取得した後、PLSQLEntityImplから継承されたcompareOldAttrTo()ヘルパー・メソッドを使用して、行ロック試行の結果としてRowInconsistentExceptionをスローする必要があるかどうかを判定します。

  • catch (SQLException e)ブロックでは、データベースがエラーをスローしたかどうかを検査しています。

    ORA-00054: resource busy and acquire with NOWAIT specified
    

    例外がスローされている場合は、lock()機能のデフォルトのエンティティ・オブジェクトの実装がこのような状況で行うのと同じように、再びADFビジネス・コンポーネントのAlreadyLockedExceptionをスローします。

例38-14 主キーで行をロックするためのストアド・プロシージャの呼出し

// In ProductsImpl.java
protected void callLockProcedureAndCheckForRowInconsistency() {
    String stmt = "begin products_api.lock_product(?,?,?,?,?,?);end;";
    CallableStatement st = 
             getDBTransaction().createCallableStatement(stmt, 0);
        try {
            st.registerOutParameter(2, VARCHAR2);
            st.registerOutParameter(3, NUMBER);
            st.registerOutParameter(4, NUMBER);
            st.registerOutParameter(5, NUMBER);
            st.registerOutParameter(6, VARCHAR2);
            st.setObject(1, getProductId());
            st.executeUpdate();
            String possiblyUpdatedName = st.getString(2);
            String possiblyUpdatedSupplierId = st.getString(3);
            String possiblyUpdatedListPrice = st.getString(4);
            String possiblyUpdatedMinPrice = st.getString(5);
            String possiblyUpdatedShipCode = st.getString(6);
            compareOldAttrTo(PRODUCTNAME, possiblyUpdatedName);
            compareOldAttrTo(SUPPLIERID, possiblyUpdatedSupplierId);
            compareOldAttrTo(LISTPRICE, possiblyUpdatedListPrice);
            compareOldAttrTo(MINPRICE, possiblyUpdatedMinPrice);
            compareOldAttrTo(SHIPPINGCLASSCODE, possiblyUpdatedShipCode);
       } catch (SQLException e) {
            if (Math.abs(e.getErrorCode()) == 54) {
            throw new AlreadyLockedException(e);
        } else {
            throw new JboException(e);
        }
    } finally {
        if (st != null) {
            try {
                st.close();
            } catch (SQLException e) {
            }
        }
    }
}

これらのメソッドを配置すると、すべてのデータベース操作についてPRODUCTS_APIパッケージをラップするProductsエンティティ・オブジェクトができます。ビュー・オブジェクトのデータ問合せ機能とエンティティ・オブジェクトのデータ検証および保存機能を明確に分離するため、通常のエンティティ・オブジェクトを使用するのと同じように、このProductsエンティティ・オブジェクトを利用できます。エンティティ・オブジェクトの慣用名としてProductsを使用する異なるビュー・オブジェクトを、必要な数だけ作成できます。

38.5.5.3 RowInconsistentException後のエンティティ・オブジェクトのリフレッシュ

RowInconsistentExceptionが発生した後で、lock()メソッドをオーバーライドして、エンティティ・オブジェクトをリフレッシュすることができます。例38-15に、エンティティ・オブジェクト実装クラスに追加すると、RowInconsistentExceptionを捕捉し、エンティティ・オブジェクトをリフレッシュするコードを示します。

例38-15 RowInconsistentException上でエンティティ・オブジェクトをリフレッシュする、オーバーライドされたlock()メソッド

// In the entity object implementation class 
@Override
public void lock() { 
  try { 
    super.lock(); 
  } 
  catch (RowInconsistentException ex) { 
    this.refresh(REFRESH_UNDO_CHANGES); 
    throw ex; 
  } 
}

38.6 結合ビューまたはリモートDBLinkに基づくエンティティ・オブジェクト

次のいずれかに基づくエンティティ・オブジェクトを作成する必要がある場合があります。

このような場合、いずれかの属性がRefresh on InsertまたはRefresh on Updateとしてマークされていると、次のようなエラーが発生します。

JBO-26041: Failed to post data to database during "Update"
## Detail 0 ##
ORA-22816: unsupported feature with RETURNING clause

これらの種類のスキーマ・オブジェクトはRETURNING句をサポートしません。この句は、デフォルトでは、INSERT操作またはUPDATE操作が実行されたものと同じデータベース・ラウンドトリップでリフレッシュされた値をより効率よく返すために、エンティティ・オブジェクトが使用します。

この種のエンティティ・オブジェクトでRETURNING句の使用を無効にするには:

  1. エンティティ・オブジェクトのカスタム・エンティティ定義クラスを有効にします。

  2. カスタム・エンティティ定義クラスで、createDef()をオーバーライドして次のメソッドを呼び出します。

    setUseReturningClause(false)
    
  3. Refresh on Insert属性がエンティティ・オブジェクトの主キーである場合は、「一意キー」プロパティを設定して、エンティティの他の属性を代替一意キーとして指定する必要があります。

このようにしてRETURNING句の使用を無効にすると、実行時には、エンティティ・オブジェクトが別のSELECT文を使用してRefresh on Insert動作およびRefresh on Update動作を実装し、挿入または更新の後でリフレッシュするための値を取得します。

38.7 ビジネス・ドメイン・レイヤーでの継承の使用

継承はオブジェクト指向開発の強力な機能であり、適切に使用すれば開発とメンテナンスを簡単にできます。37.8項「継承を使用する拡張コンポーネントの作成」で説明されているように、ADFビジネス・コンポーネントでは、継承を使用して既存のコンポーネントを拡張する新しいコンポーネントを作成し、新しいプロパティまたは動作を追加したり、親コンポーネントの動作を変更したりできます。継承は、再利用可能なビジネス・ドメイン・レイヤーで異なる種類のエンティティをモデリングする際に便利です。


注意:

この項の例では、Fusion Order DemoアプリケーションのStandaloneExamplesモジュールのAdvancedEntityExamplesアプリケーション・ワークスペースのInheritanceAndPolymorphicQueriesプロジェクトを参照します。


38.7.1 継承が有効な状況の理解

アプリケーションのデータベース・スキーマでは、同じ表の行に論理的に異なる種類のビジネス情報が格納される場合があります。このような表には、通常、各行に格納される情報の種類を決定する値を含む列があります。たとえば、Fusion Order DemoのPERSONS表では、顧客、サプライヤ、スタッフに関する情報が、同一の表に格納されます。また、PERSON_TYPE_CODE列の値(STAFF、CUSTまたはSUPP)によって、その行が表すPERSONの種類が決まります。

Fusion Order Demoの実装のこのリリースにはまだこの区別は含まれていませんが、将来のリリースでは次のことが必要になることが十分考えられます。

  • サプライヤまたはスタッフに固有の、データベースで保持される追加属性の管理

  • サプライヤまたはスタッフとは異なる、全ユーザーに共通の動作の実装

  • サプライヤまたはスタッフのみに固有の新機能の実装

図38-1は、PersonsStaffSupplierの各エンティティ・オブジェクトを個別に作成し、異なる種類のビジネス情報をアプリケーション内でより形式的に区別できるようにした場合の、ビジネス・ドメイン・レイヤーの様子を示しています。サプライヤとスタッフは個人(person)の特別な種類であるため、対応するエンティティ・オブジェクトは、ベースのPersonsエンティティ・オブジェクトを拡張しています。このベースのPersonsエンティティ・オブジェクトには、すべての種類のユーザーに共通する属性とメソッドがすべて含まれています。図のperformPersonFunction()メソッドは、共通メソッドの1つを表しています。

SupplierおよびStaffエンティティ・オブジェクトに対しては、その種類のユーザーに固有の属性やメソッドを追加できます。たとえば、サプライヤの現在の契約が期限切れになる時期を追跡するDate型のContractExpires属性が、Supplierに追加されています。また、サプライヤに固有のperformSupplierFunction()メソッドもあります。同様に、Staffエンティティ・オブジェクトには、この個人に社員割引の資格があるかどうかを追跡するDiscountEligible属性が追加されています。performStaffFunction()は、スタッフに固有のメソッドです。

図38-1 継承を使用した個人、サプライヤ、スタッフの区別

ユーザー間の継承を示す図

異なる種類の個人を、ビジネス・ドメイン・レイヤーの継承階層において異なるエンティティ・オブジェクトとしてモデル化することで、共通するデータや動作の共有が簡単になり、ユーザーを区別するアプリケーションの部分を実装できます。

38.7.2 継承階層内にエンティティ・オブジェクトを作成する方法

継承階層にエンティティ・オブジェクトを作成するには、エンティティ・オブジェクト作成ウィザードを使用して各エンティティを作成します。ここで説明する例では、次のDDL文を実行してFODアプリケーションのPERSONS表を変更し、2つの新しい列を追加してあるものとします。

alter table persons add (
  discount_eligible varchar2(1),
  contract_expires date
);

38.7.2.1 識別子列と個別の値の識別

異なる種類の情報を含む表に基づいて継承階層内にエンティティ・オブジェクトを作成する前に、まず、行の種類を区別するために使用されている表の列を識別する必要があります。FODアプリケーションのPERSONS表では、これはPERSON_TYPE_CODE列です。このような列は表の行を異なるグループに分離つまり識別するのに役立つため、識別子列と呼ばれます。

次に、表の識別子列で有効な値を決定します。値がわからない場合は、JDeveloperの「SQLワークシート」で簡単なSQL文を実行して答えを得ることもできます。ワークシートを使用するには次のようにします。

  1. 「表示」メニューから「データベース・ナビゲータ」を選択します。

  2. AdvancedEntityExamplesフォルダを展開し、FOD接続を選択します。

  3. FODを右クリックし、ポップアップ・メニューから「SQLワークシートを開く」を選択します。

図38-2は、「SQLワークシート」で、PERSONS表のPERSON_TYPE_CODE列に対してSELECT DISTINCT問合せを実行した結果を示しています。これは、PERSON_TYPE_CODE識別子の値SUPPSTAFF、およびCUSTに基づいて行が3つのグループに分類されることを示しています。

図38-2 「SQLワークシート」を使用した識別子列の異なる値の検索

「SQLワークシート」を使用して列の値を検索した図

38.7.2.2 エンティティの種類に関連する属性のサブセットの識別

表に格納されている異なるビジネス・エンティティの種類の数がわかると、個別の項目をモデル化するために作成する必要のあるエンティティ・オブジェクトの数もわかります。通常は、項目の種類ごとに1つのエンティティ・オブジェクトを作成します。次に、階層のベースとして機能するエンティティを識別するため、各項目の種類に関係する属性のサブセットを特定する必要があります。

たとえば、ContractExpiresDiscountEligibleを除くすべての属性はすべてのユーザーに関連し、ContractExpiresはサプライヤに固有、DiscountEligibleはスタッフに固有であると判断するものとします。この情報から、Personsエンティティ・オブジェクトを階層のベースにし、SupplierおよびStaffエンティティ・オブジェクトはPersonsを拡張してそれぞれの固有属性を追加することにします。

38.7.2.3 継承階層へのベース・エンティティ・オブジェクトの作成

継承階層にベース・エンティティ・オブジェクトを作成するには、エンティティ・オブジェクト作成ウィザードを使用します。

ベース・エンティティ・オブジェクトを作成する手順

  1. アプリケーション・ナビゲータで、エンティティ・オブジェクトを追加するプロジェクトを右クリックし、「新規」を選択します。

  2. 「新規ギャラリ」で、「ビジネス層」を展開し、「ADFビジネス・コンポーネント」をクリックして「エンティティ・オブジェクト」を選択し、「OK」をクリックします。

  3. エンティティ・オブジェクト作成ウィザードの「名前」ページで、エンティティの名前とパッケージを指定し、エンティティのベースとなるスキーマ・オブジェクトを選択します。

    たとえば、エンティティ・オブジェクトにPersonsという名前を設定し、PERSONS表をベースにします。

  4. 「属性」ページで、ベース・エンティティ・オブジェクトに関係のない属性(ある場合)を「エンティティ属性」リストで選択し、「削除」をクリックして削除します。

    たとえば、DiscountEligible属性とContractExpires属性をリストから削除します。

  5. 「属性の設定」ページで、「属性の選択」ドロップダウン・リストを使用して、継承されるエンティティ・オブジェクトのファミリの識別子として機能する属性を選択し、「多相化識別子」チェック・ボックスを選択してそのことを示します。重要なこととして、このベース・エンティティ・タイプの行を識別するため、この識別子属性に対する「デフォルト値」を指定する必要もあります。

    たとえば、PersonTypeCode属性を選択して識別子属性としてマークし、「デフォルト値」に値custを設定します。


    注意:

    識別子属性の「デフォルト値」を空白のままにしてもかまいません。空白のデフォルト値は、識別子列の値がIS NULLである行がこのベース・エンティティ・タイプとして扱われることを意味します。


  6. 次に、「終了」をクリックしてエンティティ・オブジェクトを作成します。

38.7.2.4 継承階層へのサブタイプ・エンティティ・オブジェクトの作成

継承階層にサブタイプ・エンティティ・オブジェクトを作成するには、エンティティ・オブジェクト作成ウィザードを使用します。

作業を始める前に、次のようにします。

  1. 新しいエンティティ・オブジェクトが拡張する親エンティティ・オブジェクトであるエンティティ・オブジェクトを決定します。

    たとえば、新しいManagerエンティティ・オブジェクトの親エンティティは、Userエンティティです。

  2. 親エンティティで識別子属性がすでに識別されていることを確認します。

    識別されていない場合は、継承された子を作成する前に、概要エディタを使用して親エンティティの識別子属性で「多相化識別子」プロパティを設定します。

階層にサブタイプ・エンティティ・オブジェクトを作成するには:

  1. アプリケーション・ナビゲータで、エンティティ・オブジェクトを追加するプロジェクトを右クリックし、「新規」を選択します。

  2. 「新規ギャラリ」で、「ビジネス層」を展開し、「ADFビジネス・コンポーネント」をクリックして「エンティティ・オブジェクト」を選択し、「OK」をクリックします。

  3. エンティティ・オブジェクト作成ウィザードの「名前」ページで、エンティティの名前とパッケージを指定し、「拡張」フィールドの隣の「参照」ボタンをクリックして、作成しているエンティティが拡張される親エンティティを選択します。

    たとえば、新しいエンティティの名前をStaffにし、拡張フィールドでPersonsエンティティ・オブジェクトを選択します。

  4. 「属性」ページの「エンティティ属性」リストに、基礎となる表からベース・エンティティ・オブジェクトに含まれない属性が表示されます。このエンティティ・オブジェクトに含めない属性を選択し、「削除」をクリックします。

    たとえば、ここではStaffエンティティを作成するので、ContractExpires属性を削除して、DiscountEligible属性を残します。

  5. 「オーバーライド」をクリックして識別子属性を選択し、属性メタデータをカスタマイズしてStaffサブタイプに個別の「デフォルト値」を提供できるようにします。

    たとえば、PersonTypeCode属性をオーバーライドします。

  6. 「属性の設定」ページで、「属性の選択」ドロップダウン・リストを使用して識別子属性を選択します。「デフォルト値」フィールドを変更し、作成しているエンティティ・サブタイプを定義する識別子属性に個別のデフォルト値を設定します。

    たとえば、PersonTypeCode属性を選択し、「デフォルト値」を値staffに変更します。

  7. 「終了」をクリックしてサブタイプ・エンティティ・オブジェクトを作成します。


注意:

同じ手順を繰り返して、Personsを拡張するSupplierエンティティ・オブジェクトを定義し、ContractExpires属性を追加し、UserRole識別子属性の「デフォルト値」を値suppに変更できます。


38.7.3 継承階層内のエンティティ・オブジェクトにメソッドを追加する方法

継承階層内のエンティティ・オブジェクトにメソッドを追加するには、エンティティ・オブジェクトでカスタムJavaクラスを有効にし、ソース・エディタを使用してメソッドを追加します。

38.7.3.1 階層内の全エンティティ・オブジェクトに共通するメソッドの追加

階層内のすべてのエンティティ・オブジェクトに共通するメソッドを追加するには、階層のベース・エンティティ・オブジェクトでカスタムJavaクラスを有効にし、ソース・エディタでメソッドを追加します。たとえば、Userベース・エンティティ・オブジェクトのPersonsImplクラスに次のメソッドを追加すると、階層のすべてのエンティティ・オブジェクトがそれを継承します。

// In PersonsImpl.java
public void performPersonFunction() {
  System.out.println("## performPersonFunction as Customer");
}

38.7.3.2 サブタイプ・エンティティでの共通メソッドのオーバーライド

階層内のすべてのエンティティ・オブジェクトに共通のサブタイプ・エンティティでメソッドをオーバーライドするには、サブタイプ・エンティティでカスタムJavaクラスを有効にし、「ソース」メニューから「メソッドのオーバーライド」を選択して「メソッドのオーバーライド」ダイアログを開きます。オーバーライドするメソッドを選択し、「OK」をクリックします。次に、オーバーライドするメソッドの実装をソース・エディタでカスタマイズします。たとえば、Staffサブタイプ・エンティティ・オブジェクトのStaffImplクラスでperformPersonFunction()メソッドをオーバーライドし、実装を次のように変更するものとします。

// In StaffImpl.java
public void performPersonFunction() {
  System.out.println("## performPersonFunction as Staff");
}

サブタイプ階層でエンティティ・オブジェクトのインスタンスについての作業を行う場合、複数の異なるサブタイプのインスタンスを処理することがあります。StaffエンティティとSupplierエンティティはPersonsの特別な種類であるため、これらすべてに共通の汎用性の高いPersonsImpl型を使用して、すべてのエンティティで動作するコードを作成できます。階層内のサブタイプ・ファミリであるクラスのこのような汎用処理を行うとき、Javaは、使用可能なメソッドで最も固有性の高いオーバーライドを常に呼び出します。

つまり、PersonsImplのインスタンスでperformPersonFunction()メソッドを呼び出し、実際にはさらに固有のStaffImplサブタイプがある場合、結果の出力は次のようになります。

## performPersonFunction as Staff

これに対し、標準のPersonsImplインスタンスによるデフォルトの出力結果は次のようになります。

## performPersonFunction as Customer

38.7.3.3 サブタイプ・エンティティに固有のメソッドの追加

階層内のサブタイプ・エンティティ・オブジェクトに固有のメソッドを追加するには、そのエンティティでカスタムJavaクラスを有効にし、ソース・エディタでメソッドを追加します。たとえば、Supplierサブタイプ・エンティティ・オブジェクトのSupplierImplクラスに、次のようなメソッドを追加できます。

// In SupplierImpl.java
public void performSupplierFunction() {
  System.out.println("## performSupplierFunction called");    
}

38.7.4 継承の使用について

継承を使用すると、新しいベース・エンティティの導入、主キーによるサブタイプ・エンティティの検索、および多相エンティティ・オブジェクトの慣用名を持つビュー・オブジェクトの作成も行うことができます。

38.7.4.1 新しいベース・エンティティを導入する必要がある場合

InheritanceAndPolymorphicQueriesサンプル・プロジェクトで、Personsエンティティ・オブジェクトは、PERSONS表の実際の行に対応するとともに、階層でベース・エンティティの役割も果たしていました。つまり、そのすべての属性は、階層内の全エンティティ・オブジェクトに共通でした。ここで、顧客には固有であっても、スタッフまたはサプライヤには共通しないプロパティがPersonsエンティティに必要な場合は、どうなるでしょう。顧客は顧客満足度調査に参加可能だが、スタッフやサプライヤは参加しない場合を考えてみてください。この要件に対処するためのLastSurveyDate属性は、Personsエンティティには必要ですが、それを継承するStaffおよびSupplierエンティティ・オブジェクトには無意味です。

このような場合は、階層内でベース・エンティティとして機能するBasePersonsといった名前の新しいエンティティ・オブジェクトを導入できます。それは、PersonsStaff、およびSupplierの全エンティティ・オブジェクトに共通するすべての属性を保持します。そして、この表に出現する具体的な行に対応する3つの各エンティティは、BasePersonsから継承する属性と、サブタイプに固有の属性を保持します。BasePersons型では、PersonTypeCode属性を識別子属性としてマークするかぎり、「デフォルト値」を空白にしておいてかまいません(または、表のPERSON_TYPE_CODE列に出現しない他の値にします)。実行時にはBasePersonsエンティティのインスタンスは使用されないため、実際には識別子のデフォルト値は問題ではありません。

38.7.4.2 主キーによるサブタイプ・エンティティの検索

エンティティ定義でfindByPrimaryKey()メソッドを使用するときは、それを呼び出すエンティティ・オブジェクト型のエンティティ・キャッシュのみが検索されます。InheritanceAndPolymorphicQueriesサンプル・プロジェクトが意味するのは、PersonsImpl.getDefinitionObject()をコールしてPersonsエンティティ・オブジェクトのエンティティ定義にアクセスする場合、そこでfindByPrimaryKey()をコールすると、たまたま顧客であるキャッシュ内のエントリのみが検索結果となります。これが求める動作である場合もあります。しかし、主キーで継承階層内のサブタイプも含めてエンティティ・オブジェクトを検索する場合は、かわりにEntityDefImplクラスのfindByPKExtended()メソッドを使用できます。この項で説明したPersonsの例では、この代替検索メソッドを使用すると、エンティティ・オブジェクトが顧客、サプライヤまたはスタッフのいずれであっても、主キーで検索できます。その後は、Javaのinstanceof演算子を使用して発見された型を検査し、PersonsImplオブジェクトをさらに固有のエンティティ・オブジェクト型にキャストして、そのサブタイプに固有の機能で作業できます。

38.7.4.3 多相エンティティ・オブジェクトの慣用名を持つビュー・オブジェクトを作成する方法

継承階層のベース・エンティティ・オブジェクトに対応するエンティティ・オブジェクトの慣用名を持つエンティティ・ベースのビュー・オブジェクトを作成するときは、ベース・エンティティのサブタイプ階層の複数の異なるサブタイプに対応する行を問い合せるように、ビュー・オブジェクトを構成できます。ビュー・オブジェクトの各行は、識別子属性の値の対応に基づいて、適切なサブタイプ・エンティティ・オブジェクトをエンティティ行の部分として使用します。このようなビュー・オブジェクトを設定して使用する具体的な方法については、39.6.2項「多相エンティティ・オブジェクトの慣用名によるビュー・オブジェクトの作成方法」を参照してください。

38.8 制約違反を防ぐためのエンティティ・ポスト順序の制御

データベース制約のため、同じトランザクション内でDML操作を実行して複数の関連するエンティティ・オブジェクトに変更を保存するときは、操作の実行順序が重要になる場合があります。外部キー参照を含む新しい行を、参照されている行を挿入するに挿入しようとすると、データベースで制約違反が発生する可能性があります。コミット時のエンティティ・オブジェクトのデフォルトの処理順序、および必要に応じてその順序をプログラムで変更する方法を理解している必要があります。


注意:

この項の例では、Fusion Order DemoアプリケーションのStandaloneExamplesモジュールのAdvancedEntityExamplesアプリケーション・ワークスペースのControllingPostingOrderプロジェクトを参照します。


38.8.1 デフォルトの後処理順序

デフォルトでは、トランザクションをコミットすると、保留中の変更リスト内のエンティティ・オブジェクトは、時間順、つまりエンティティがリストに追加された順序で処理されます。たとえば、新しいProductを作成し、次にその製品に関連する新しいSupplierを作成すると、新しいProductが先に挿入され、その後で新しいSupplierが挿入されます。

38.8.2 コンポジットによるデフォルトの処理順序の変更

2つのエンティティ・オブジェクトがコンポジットで関連付けられていると、厳密な時間順序が自動的に変更されて、コンポジット関係にある親と子のエンティティ行が制約に違反しない順序で保存されることが保証されます。たとえば、コンポジットされる新しい子のエンティティ行より前に、新しい親のエンティティ行が挿入されます。

38.8.3 ポスト順序を制御するためのpostChanges()のオーバーライド

関係のあるエンティティが関連付けられてはいてもコンポジットではない場合は、コードを少し作成して、関連するエンティティが正しい順序で保存されるようにする必要があります。

38.8.3.1 実際のポスト順序の問題の観察

例38-16PostModuleアプリケーション・モジュールのnewProductForNewSupplier()カスタム・メソッドについて考えます。このメソッドは、一連のパラメータを受け取って、次の処理を行います。

  1. 新しいProductを作成します。

  2. 新しいSupplierを作成します。

  3. サービス・リクエストが関係する製品IDを設定します。

  4. トランザクションをコミットします。

  5. Result Java Beanを作成し、新しい製品IDとサプライヤIDを保持します。

  6. 結果を返します。


注意:

このコードでは、Products.ProductIdSuppliers.SupplierIdには、順序に基づいて主キーを設定するためDBSequenceにデータ型が設定されているものと想定しています。


例38-16 新しいProductとSupplierの作成および新しいIDの戻り

// In PostModuleImpl.java
public Result newProductForNewSupplier(String supplierName, 
                                           String supplierStatus,
                                           String productName,
                                           String productStatus,
                                           Number listPrice,
                                           Number minPrice,
                                           String shipCode) {
    oracle.jbo.domain.Date today = new Date(Date.getCurrentDate());
    Number objectId = new Number(0);
    // 1. Create a new product
    ProductsBaseImpl newProduct = createNewProduct();
    // 2. Create a new supplier
    SuppliersImpl newSupplier = createNewSupplier();
    newSupplier.setSupplierName(supplierName);
    newSupplier.setSupplierStatus(supplierStatus);
    newSupplier.setCreatedBy("PostingModule");
    newSupplier.setCreationDate(today);
    newSupplier.setLastUpdatedBy("PostingModule");
    newSupplier.setLastUpdateDate(today);
    newSupplier.setObjectVersionId(objectId);
    // 3. Set the supplier id to which the product pertains
    newProduct.setSupplierId(newSupplier.getSupplierId().getSequenceNumber());
    newProduct.setProductName(productName);
    newProduct.setProductStatus(productStatus);
    newProduct.setListPrice(listPrice);
    newProduct.setMinPrice(minPrice);
    newProduct.setShippingClassCode(shipCode);
    newProduct.setCreatedBy("PostingModule");
    newProduct.setCreationDate(today);
    newProduct.setLastUpdatedBy("PostingModule");
    newProduct.setLastUpdateDate(today);
    newProduct.setObjectVersionId(objectId);
    // 4. Commit the transaction
    getDBTransaction().commit();
    // 5. Construct a bean to hold new supplier id and product id
    Result result = new Result();
    result.setProductId(newProduct.getProductId().getSequenceNumber());
    result.setSupplierId(newSupplier.getSupplierId().getSequenceNumber());
    // 6. Return the result
    return result;
}
private ProductsBaseImpl createNewProduct(){
   EntityDefImpl productDef = ProductsBaseImpl.getDefinitionObject();
   return (ProductsBaseImpl) productDef.createInstance2(getDBTransaction(), null);
}private SuppliersImpl createNewSupplier(){
   EntityDefImpl supplierDef = SuppliersImpl.getDefinitionObject();
   return (SuppliersImpl) supplierDef.createInstance2(getDBTransaction(), null);
}

このメソッドをアプリケーション・モジュールのクライアント・インタフェースに追加し、テスト用のクライアント・プログラムからテストすると、エラーが発生します:

oracle.jbo.DMLConstraintException:
JBO-26048: Constraint "PRODUCT_SUPPLIER_FK" violated during post operation:
"Insert" using SQL Statement
"BEGIN
  INSERT INTO PRODUCTS(
    SUPPLIER_NAME,SUPPLIER_STATUS,PRODUCT_NAME,
    PRODUCT_STATUS,LIST_PRICE,MIN_PRICE, SHIPPING_CLASS_CODE)
   VALUES (?,?,?,?,?,?,?)
   RETURNING PRODUCT_ID INTO ?; 
END;".
## Detail 0 ##
java.sql.SQLException:
ORA-02291: integrity constraint (FOD.PRODUCT_SUPPILER_FK) violated
           - parent key not found

PRODUCTS行が挿入されるとき、その外部キーSUPPLIER_IDの値がSUPPLIERS表のどの行にも対応しないというエラーをデータベースが出力します。これは次の理由で発生します:

  • コードがSupplierより前にProductを作成した。

  • エンティティ・オブジェクトProductsSuppliersが、関連付けられてはいるがコンポジットではない。

  • 新しいエンティティ行を保存するDML操作が時間順に行われるため、新しいSupplierより前に新しいProductが挿入される。

38.8.3.2 Productより前のSupplierの強制的なポスト

まだ有効でないサプライヤIDを持つ製品を追加しようとする問題は、例のコード行の順序を変更し、Supplierを先に作成してからProductを作成すると解決できます。目先の問題にはこれで対処できますが、別のアプリケーション開発者が間違った順序でエンティティを作成する可能性が残っています。

さらによい解決策は、エンティティ・オブジェクト自体がポスト順序を処理するようにして、作成順序に関係なく正しく動作するようにすることです。そのためには、関連するエンティティ・オブジェクトを参照する外部キー属性を含むエンティティのpostChanges()メソッドをオーバーライドし、例38-17に示すようにコードを記述します。この例では、Supplierエンティティに対する外部キーが含まれているのはProductであるため、Productを更新し、条件によっては、サービス・リクエストをポストする前に関連する新しいSupplierを強制的にポストするようにします。

このコードでは、ポストしているエンティティがSTATUS_NEW状態またはSTATUS_MODIFIED状態のいずれかであるかを検査します。その場合は、getSupplier()アソシエーション・アクセッサを使用して、関連する製品を取得します。関連するSupplierのポスト状態もSTATUS_NEWである場合は、super.postChanges()を呼び出して自分のDMLを実行する前に、まず関連する親行のpostChanges()を呼び出します。

例38-17 Supplierを先にポストするためのProductsBaseImplのpostChanges()のオーバーライド

// In ProductsBaseImpl.java
public void postChanges(TransactionEvent e) {
  /* If current entity is new or modified */
  if (getPostState() == STATUS_NEW ||
      getPostState() == STATUS_MODIFIED) {
    /* Get the associated supplier for the product */
    SuppliersImpl supplier = getSupplier();
    /* If there is an associated supplier */
    if (supplier != null) {
      /* And if it's post-status is NEW */
      if (supplier.getPostState() == STATUS_NEW) {
        /*
         * Post the supplier first, before posting this
         * entity by calling super below
         */
        supplier.postChanges(e);
      }
    }
  }
  super.postChanges(e);
}

この状態で例を再び実行すると、newProductForNewSupplier()メソッドのコードの作成順序を変更しなくても、エンティティは、新しいSupplierが先で新しいProductが後という正しい順序でポストされます。しかし、まだ問題があります。まだ制約違反が発生しますが、今度は理由が異なります。

Suppliersエンティティ・オブジェクトの主キーがユーザーによって割り当てられている場合は、例38-17のコードのみで、ポスト順序を修正して制約違反に対処できます。


注意:

このプログラム的な手法(Java EEアプリケーション・レイヤーで問題を解決する)のかわりに、データベース・レイヤーで遅延可能制約を使用する方法もあります。データベース・スキーマを制御できる場合は、外部キー制約をDEFERRABLE INITIALLY DEFERREDとして定義する(または変更する)ことを検討します。このようにすると、データベースは制約の検査をトランザクションのコミット時まで遅延します。これにより、COMMIT時までにすべての適切な関連する行が保存されている場合、アプリケーションはDML操作を任意の順序で実行でき、親/子の順序は緩和されます。ただし、親の主キーが順序から割り当てられる場合は、外部キーの値をカスケード更新するために、38.8.3.3項「DBSequenceの値が設定される主キーに基づくアソシエーションの理解」38.8.3.4項「DBSequence割当ての外部キーに対する参照の更新」で説明するコードを作成する必要があります。


しかし、この例では、Suppliers.SupplierIdはデータベース順序から割り当てられ、ユーザー割当てではありません。そのため、新しいSuppliersエンティティ行がポストされる時点で、データベースが割り当てる順序値を反映するように、SupplierId属性が更新されます。新しいサプライヤを参照するProducts.SupplierId属性の外部キー値は、サプライヤID値のこの更新により親を失います。製品行が保存されるときには、SUPPLIER_IDの値と一致する行がSUPPLIERS表にまだ存在しないため、制約違反が再び発生します。次の2つの項では、このような親を失うという問題に対処するための解決策について説明します。

38.8.3.3 DBSequenceの値が設定される主キーに基づくアソシエーションの理解

4.10.10項「トリガーによってデータベース順序から割り当てられた主キー値の取得方法」で説明されているように、エンティティ・オブジェクトの主キー属性がDBSequence型の場合は、そのオブジェクトが作成されるトランザクションの間、キーの数値は一時的に一意の負の値になります。同じトランザクション内で関連するエンティティの値を作成すると、これらの間の関連は、この一時的な負のキー値に基づくことになります。DBSequenceの値が主キーであるエンティティ・オブジェクトをポストすると、その主キーはデータベースが割り当てる正しい順序番号を反映して更新されますが、それに関連付けられたエンティティは一時的な負の外部キー値を保持したままで親を失います。

コンポジットに基づくエンティティ・オブジェクトの場合は、親エンティティ・オブジェクトのDBSequence値が設定されている主キーが更新されると、コンポジットされる子エンティティ行の一時的な負の外部キー値は、親の更新されたデータベース割当ての主キーを反映して、自動的に更新されます。つまり、コンポジット・エンティティの場合、親を失うという問題は発生しません。

ただし、エンティティ・オブジェクトがコンポジットではないアソシエーションによって関連付けられている場合は、コードを作成して、一時的に負の値を参照する関連エンティティ行を、更新されたデータベース割当ての主キー値で更新する必要があります。次の項では、必要なコードについて説明します。

38.8.3.4 DBSequence割当ての外部キーに対する参照の更新

この例のSuppliersのようなエンティティがDBSequence値の主キーを持ち、それと関連付けられた(ただしコンポジットではない)他のエンティティによって外部キーとして参照される場合は、例38-18に示すように、この新しいSuppliers行を参照する可能性のあるエンティティ行の行セットに対する参照を保存するように、postChanges()メソッドをオーバーライドする必要があります。現在のSupplier行のステータスがNewの場合は、getProduct()アソシエーション・アクセッサから返るRowSetの値を、super.postChanges()を呼び出す前に、newProductsBeforePostメンバー・フィールドに代入します。

例38-18 この新しいSupplierを参照するエンティティ行に対する参照の保存

// In SuppliersImpl.java
RowSet newProductsBeforePost = null;
public void postChanges(TransactionEvent TransactionEvent) {
  /* Only bother to update references if Product is a NEW one */
  if (getPostState() == STATUS_NEW) {
    /* 
     * Get a rowset of products related
     * to this new supplier before calling super
     */
    newProductsBeforePost = (RowSet)getProductsBase();
  }
  super.postChanges(TransactionEvent);
}

この保存したRowSetオブジェクトは、後で例38-19で示されているオーバーライドされたrefreshFKInNewContainees()メソッドで使用します。新しいエンティティ行は、このメソッドを呼び出すことで、postChanges()を呼び出す前にそれを参照していた他のエンティティ行を更新された主キー値でカスケード更新できます。newProductsBaseBeforePost行セット(NULLでない場合)のProductsBaseImpl行を反復処理し、それぞれのサプライヤID値に、新しくポストされたSupplierエンティティの、順序から割り当てられた新しいサプライヤ値を設定します。

例38-19 新しいSupplierId値のエンティティ行のカスケード更新

// In SuppliersImpl.java
protected void refreshFKInNewContainees() {
  if (newProductsBeforePost != null) {
    Number newSupplierId = getSupplierId().getSequenceNumber();
    /* 
     * Process the rowset of products that referenced
     * the new supplier prior to posting, and update their
     * SupplierId attribute to reflect the refreshed SupplierId value
     * that was assigned by a database sequence during posting.
     */
    while (newProductsBeforePost.hasNext()) {
      ProductsBaseImpl svrReq =
        (ProductsBaseImpl)newProductsBeforePost.next();
      product.setSupplierId(newSupplierId);
    }
    closeNewProductRowSet();
  }  
}

この変更を実装すると、例38-16のコードは、データベース制約違反を起こさないで動作するようになります。

38.9 カスタム検証規則の実装

ADFビジネス・コンポーネントには、開発者が使用できる組込みの宣言的検証規則の基本セットが付属しています。ただし、エンティティ・オブジェクト用のバリデータ・アーキテクチャの強力な機能は、独自のカスタム検証規則を作成できることです。同じ種類の検証コードを繰り返し作成している場合は、カスタム検証規則クラスを作成し、このような共通の検証パターンをパラメータ化された方法で取得できます。

定義したカスタム検証規則クラスは、JDeveloperに登録し、組込み規則と同じように簡単に使用できます。実際、カスタム検証規則をカスタムUIパネルにバンドルすることもできます。JDeveloperはこれを利用して、検証規則で必要とするパラメータを開発者が簡単に使用および構成できるようにします。

38.9.1 カスタム検証規則の作成方法

エンティティ・オブジェクト用のカスタム検証規則を作成するには、oracle.jbo.rulesパッケージのJboValidatorInterfaceを実装するJavaクラスが必要です。「新規ギャラリ」からスケルトン・クラスを作成できます。

カスタム・バリデータを作成するには:

  1. アプリケーション・ナビゲータで、バリデータを作成するプロジェクトを右クリックし、ポップアップ・メニューから「新規」を選択します。

  2. 「新規ギャラリ」で「ビジネス層」を展開し、「ADFビジネス・コンポーネント」をクリックして「検証規則」を選択し、「OK」をクリックします。

例38-20に示すように、JBOValidatorInterfaceには、1つのメインvalidate()メソッドと、Descriptionプロパティ用のgetterおよびsetterメソッドが含まれます。

例38-20 すべての検証規則が実装する必要のあるJboValidatorInterface

package oracle.jbo.rules;
public interface JboValidatorInterface {
  void validate(JboValidatorContext valCtx) { }
  java.lang.String getDescription() { }
  void setDescription(String description) { }
}

検証規則の動作がパラメータ化されて柔軟性が増してから、検証クラスの各パラメータにBeanプロパティを追加します。たとえば、例38-21のコードにはDateMustComeAfterRuleという名前のカスタム検証規則が含まれており、ある日付属性が別の日付属性より後でなければならないことを検証します。規則を使用する開発者が検証対象の2つの日付として使用する日付属性の名前を構成できるように、このクラスではinitialDateAttrNamelaterDateAttrNameという2つのプロパティが定義されています。

例38-21は、カスタム検証規則を実装するコードを示しています。これは、AbstractValidatorを拡張し、エンティティ・オブジェクトのカスタム・メッセージ・バンドルの使用を継承しています。開発者がエンティティ・オブジェクトの規則を使用すると、JDeveloperは検証エラー・メッセージをここに保存します。

検証規則のvalidate()メソッドは、実行時に規則クラスが機能を実行する必要がある場合に常に呼び出されます。このコードが実行する基本的な手順は次のとおりです。

  1. エンティティ・レベルでバリデータが正しくアタッチされていることを確認します。

  2. 検証するエンティティ行を取得します。

  3. 先の日と後の日の属性の値を取得します。

  4. 先の日が後の日より前であることを検証します。

  5. 検証が失敗した場合は例外をスローします。

例38-21 カスタムのDateMustComeAfterRule

// NOTE: package and imports omitted
public class DateMustComeAfterRule extends AbstractValidator
       implements JboValidatorInterface {
  /**
   * This method is invoked by the framework when the validator should do its job
   */
  public void validate(JboValidatorContext valCtx) {
    // 1. If validator is correctly attached at the entity level...
    if (validatorAttachedAtEntityLevel(valCtx)) {
      // 2. Get the entity row being validated
      EntityImpl eo = (EntityImpl)valCtx.getSource();
      // 3. Get the values of the initial and later date attributes
      Date initialDate = (Date) eo.getAttribute(getInitialDateAttrName());
      Date laterDate = (Date) eo.getAttribute(getLaterDateAttrName());
      // 4. Validate that initial date is before later date
      if (!validateValue(initialDate,laterDate)) {
        // 5. Throw the validation exception
        RulesBeanUtils.raiseException(getErrorMessageClass(),
                                      getErrorMsgId(),
                                      valCtx.getSource(),
                                      valCtx.getSourceType(),
                                      valCtx.getSourceFullName(),
                                      valCtx.getAttributeDef(),
                                      valCtx.getNewValue(),
                                      null, null);
      }
    }
    else {
      throw new RuntimeException("Rule must be at entity level");
    }
  }
  /**
   * Validate that the initialDate comes before the laterDate.
   */
  private boolean validateValue(Date initialDate, Date laterDate) {
    return (initialDate == null) || (laterDate == null) ||
    (initialDate.compareTo(laterDate) < 0);
  }
  /**
   * Return true if validator is attached to entity object
   * level at runtime.
   */
  private boolean validatorAttachedAtEntityLevel(JboValidatorContext ctx) {
    return ctx.getOldValue() instanceof EntityImpl;
  }
  // NOTE: Getter/Setter Methods omitted
  private String description;
  private String initialDateAttrName;
  private String laterDateAttrName;
}

カスタム検証規則を簡単に再利用できるように、通常は、規則を利用するアプリケーションでの参照用として規則をJARファイルにパッケージします。

38.9.2 規則用の設計時Beanカスタマイザの追加

検証規則クラスはBeanであるため、標準のJavaBeanカスタマイザ・クラスを実装して、設計時にBeanのプロパティを設定しやすくすることができます。例38-21DateMustComeAfter規則の例では、開発者はinitialDateAttrNamelaterDateAttrNameの2つのプロパティを構成する必要があります。

図38-3は、Swing用のJDeveloperビジュアル・デザイナを使用して作成されたDateMustComeAfterRuleCustomizerを示しており、タイトル付きの境界を持つJPanelに、ドロップダウン用の2つのJLabelプロンプトと2つのJComboBoxコントロールが含まれています。クラスのコードによって、IDEで現在編集されているエンティティ・オブジェクトのDate値属性の名前がドロップダウン・リストに移入されます。これにより、DateMustComeAfterRule検証をエンティティ・オブジェクトに追加する開発者は、検証の開始日と終了日に使用する日付属性を簡単に選択できます。

図38-3 JDeveloperのSwingビジュアル・デザイナを使用した検証規則カスタマイザの作成

カスタマイズされた検証規則エディタの図

カスタマイザとDateMustComeAfterRule Java Beanを関連付けるには、BeanInfoクラスを作成するときの標準的な手順に従います。例38-22に示すようにDateMustComeAfterRuleBeanInfoは、カスタマイザ・クラスをDateMustComeAfter Beanクラスと関連付けるBeanDescriptorを返します。

通常は、カスタマイザ・クラスとこのBean情報を、設計時専用の独立したJARファイルにパッケージします。

例38-22 カスタマイザとカスタム検証規則を関連付けるためのBeanInfo

package oracle.fodemo...frameworkExt.rules;
import java.beans.BeanDescriptor;
import java.beans.SimpleBeanInfo;
public class DateMustComeAfterRuleBeanInfo extends SimpleBeanInfo {
  public BeanDescriptor getBeanDescriptor() {
    return new BeanDescriptor(DateMustComeAfterRule.class,
                              DateMustComeAfterRuleCustomizer.class);
  }
}

38.9.3 JDeveloperでのカスタム規則の登録と使用

カスタム検証規則は作成後、JDeveloper IDEのプロジェクトまたはアプリケーションレベルに追加できるため、他の開発者がその規則を宣言的に使用できます。

エンティティ・オブジェクトを含むプロジェクトでカスタム検証規則を登録するには:

  1. アプリケーション・ナビゲータで、目的のプロジェクトを右クリックして、ポップアップ・メニューから「プロジェクト・プロパティ」を選択します。

  2. 「プロジェクト・プロパティ」ダイアログで、「ビジネス・コンポーネント」を展開し、「登録済の規則」を選択します。

  3. 「登録済の規則」ページで、「追加」をクリックします。

  4. 「検証規則の登録」ダイアログで、作成した検証規則を参照して見つけ(38.9.1項「カスタム検証規則の作成方法」で作成したような検証規則)、「OK」をクリックします。

IDEレベルのカスタム・バリデータを登録するには:

  1. 「ツール」メニューで「設定」を選択します。

  2. 「ビジネス・コンポーネント」→「規則の登録」ページから、1つまたは複数の検証規則を追加できます。

    検証規則を追加するときは、検証規則クラスの完全修飾名を指定し、JDeveloperの使用可能なバリデータのリストで表示される検証規則の名前を設定します。

38.10 新規履歴タイプの作成

履歴タイプは、ある時点に固有のデータの追跡に使用されます。JDeveloperにはいくつかの履歴タイプが付属していますが、独自のものも作成できます。標準の履歴タイプとその使用方法の詳細は、4.10.12項「履歴列を使用して作成および変更した日付を追跡する方法」を参照してください。

38.10.1 新規履歴タイプの作成方法

付属の履歴タイプに限定されることなく、「設定」ダイアログの「履歴タイプ」ページを使用してカスタム履歴タイプを追加または削除してから、カスタムJavaコードを記述して目的の動作を実装できます。カスタム履歴タイプを処理するコードは、再利用できるようアプリケーション全体にわたるエンティティ・ベース・クラスで記述します。

図38-5は、タイプIDが11last update loginカスタム・タイプです。last_update_loginFND_LOGINS表の外部キーであると仮定します。

図38-4 「新規エンティティ属性」ダイアログの新規の履歴タイプ

新規エンティティ属性エディタでの履歴列タイプの図

カスタム履歴列タイプを作成するには:

  1. 「ツール」メニューで「設定」を選択します。

  2. 「設定」ダイアログで、「ビジネス・コンポーネント」を展開し、「履歴タイプ」をクリックします。

  3. 「履歴タイプ」ページで「新規」をクリックします。

  4. 「履歴タイプ」ダイアログで、名前(スペースの使用可)の文字列値および数値IDを入力します。

    「タイプID」は11から126までの整数である必要があります。0 - 10の数値は内部用に予約されています。表示文字列は、次に「属性の編集」ダイアログを使用する際、「履歴列」ドロップダウン・リストに表示されます。

    図38-5 新規履歴タイプの作成

    設定エディタでの履歴タイプの図
  5. EntityImpl.javaファイルを開いて、例38-23のような定義を追加します。

    例38-23 履歴タイプの定義

    private static final byte LASTUPDATELOGIN_HISTORY_TYPE = 11;
    
  6. 例38-24のようなコードで、EntityImplベース・クラスのgetHistoryContextForAttribute(AttributeDefImpl attr)メソッドをオーバーライドします。

    例38-24 getHistoryContextForAttribute()のオーバーライド

    @Override
    protected Object getHistoryContextForAttribute(AttributeDefImpl attr) {
        if (attr.getHistoryKind() == LASTUPDATELOGIN_HISTORY_TYPE) {
            // Custom History type logic goes here
        }
        else {
            return super.getHistoryContextForAttribute(attr);
        }
    }
    

38.10.2 履歴タイプの削除方法

履歴タイプは通常、アプリケーションの存続期間中は値の監査に使用されるため、削除する必要はありません。ただし、削除する必要がある場合は、次の作業を実行します。

  1. 「設定」ダイアログで、JDeveloper履歴タイプ・リストから目的の履歴タイプを削除します。

  2. ベースのEntityImpl.getHistoryContextForAttributeメソッドの履歴タイプをサポートするため実装したカスタム・コードがあれば削除します。

  3. エンティティ属性メタデータの履歴タイプのすべての慣用名を削除します。この履歴タイプを使用するように定義した属性があれば、編集する必要があります。

JDeveloper履歴タイプ・リストから目的の履歴タイプを削除するには:

  1. 「ツール」メニューで「設定」を選択します。

  2. 「設定」ダイアログで、「ビジネス・コンポーネント」を展開し、「履歴タイプ」をクリックします。

  3. 「履歴タイプ」ページで、削除する履歴タイプを選択して「削除」をクリックします。