ヘッダーをスキップ
Oracle Application Development Framework Forms/4GL開発者のための開発者ガイド
10g(10.1.3.0)
B40013-02
  目次
目次
索引
索引

戻る
戻る
 
次へ
次へ
 

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

この章では、ビジネス・ドメイン・レイヤーのエンティティ・オブジェクトで使用する高度な手法について説明します。

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


注意:

この章の例の稼働バージョンを試すには、サンプルのダウンロード・ページ(http://otn.oracle.com/documentation/jdev/b25947_01/)からAdvancedEntityExamplesワークスペースをダウンロードしてください。

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

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

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


注意:

この項の例では、AdvancedEntityExamplesワークスペースのSimpleDomainsプロジェクトを参照します。ダウンロード方法については、この章の最初にある注意を参照してください。プロジェクトに必要な追加のデータベース・オブジェクトを設定するには、「リソース」フォルダにあるCreateObjectType.sqlスクリプトを、SRDemo接続に対して実行してください。

26.1.1 ドメインとは

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

26.1.2 ドメインの作成方法

ドメインを作成するには、ドメイン作成ウィザードを使用します。このウィザードは、「新規ギャラリ」「ADF Business Components」カテゴリで使用できます。

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

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

最後に、「終了」をクリックしてドメインを作成します。

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

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

26.1.4 留意事項

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

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

図26-1 「属性」の「型」ドロップダウン・リストのカスタム・ドメイン型

ドロップダウン・リストで示される使用可能なカスタム・ドメイン型の図

注意:

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

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

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

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

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

package devguide.advanced.domains.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;
  }
}

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

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

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

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

public class ShortEmailAddress implements DomainInterface, Serializable {
  private String mData;
  // etc.
  /**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);
    }
  }
  // etc.
}

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

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

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

public class EvenNumber extends Number {
  // etc.
  /**
   * 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);
    }
  }
  // etc.
}

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

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

ShortEmailAddress email = new ShortEmailAddress("smuench");

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

ShortEmailAddress email = new ShortEmailAddress("bribet");

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

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

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

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

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

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

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

  • ドメイン作成ウィザードの手順1の「名前」パネルで、「Oracleオブジェクト型のドメイン」チェック・ボックスを選択し、「使用可能」リストからドメインを作成するオブジェクト型を選択します。

  • 手順2の「設定」パネルで、「属性」ドロップダウン・リストを使用して複数のドメイン・プロパティを切り替え、適切に設定します。

  • 「終了」をクリックします。

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

オブジェクト型ドメインを手動で作成する以外にも、表からのビジネス・コンポーネント・ウィザードを使用して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バインディング・レイヤーでのオブジェクト型ドメインの処理のサポートは完全であるため、宣言的にデータ・バインドされたユーザー・インタフェースでオブジェクト・ドメインを値とする属性を使用することは容易ではありません。

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

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

  • 右クリックで表示されるポップアップ・メニューで「ドメイン・クラスに移動」を選択します。

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

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

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

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

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

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

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

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

監査のため、表に追加した行を表から物理的に削除できない場合があります。そのような場合は、エンド・ユーザーがユーザー・インタフェースで行を削除する際に、DELETED列の値をNからYに変更し、削除済としてマークします。ここでは、この処理を行うためにエンティティ・オブジェクトのデフォルト動作を変更する必要のある2つのメソッドのオーバーライドについて説明します。後の項では、SRDemoアプリケーションのProductエンティティをこのように動作するように変更します。そのために、PRODUCTS表を変更してDELETED列を追加し、Productエンティティとデータベースの同期を取って、対応するDeleted属性を追加してあるものとします。

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

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

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

// In ProductImpl.java
public void remove() {
  setDeleted("Y");
  super.remove();
}

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

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

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

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

// In ProductImpl.java
protected void doDML(int operation, TransactionEvent e) {
   if (operation == DML_DELETE) {
     operation = DML_UPDATE;
   }
   super.doDML(operation, e);
  }

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

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

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

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

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

このような複雑なリレーションシップは、アソシエーション・エディタで設定できます。最初に、「エンティティ・オブジェクト」ページで、必要な追加属性のペアを追加します。この例では、(EffectiveFromRequestDate)のペアと、(EffectiveUntilRequestDate)のペアです。次に、「アソシエーションSQL」ページで、「WHERE」フィールドを編集して、WHERE句を次のように変更します。

(:Bind_ProdId = ServiceRequest.PROD_ID) AND
(ServiceRequest.REQUEST_DATE BETWEEN :Bind_EffectiveFrom
                                 AND :Bind_EffectiveUntil)

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

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

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

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

行セットの作成には若干のオーバーヘッドが伴うため、同じアソシエーション・アクセッサ属性に対して大量の呼出しを行うコードの場合は、アソシエーションのリンク元エンティティ・オブジェクトに対してアソシエーション・アクセッサ行セットの保持を有効にすることを検討する余地があります。アソシエーション・アクセッサの保持機能を使用するには、最初に、エンティティ・オブジェクトでカスタムJavaエンティティ・コレクション・クラスを有効にします。他のカスタム・エンティティJavaクラスと同様に、この設定は、エンティティ・オブジェクト・エディタの「Java」パネルで「エンティティ・コレクション・クラス」チェック・ボックスを選択することにより行います。次に、JDeveloperによって自動的に作成されるYourEntityCollImplクラスで、init()メソッドをオーバーライドし、setAssociationAccessorRetained()メソッドを呼び出してパラメータとしてtrueを渡す行を、super.init()の後に追加します。これは、そのエンティティ・オブジェクトのすべてのアソシエーション・アクセッサ属性に適用されます。

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

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

// In ProductImpl.java
RowSet rs = (RowSet)getServiceRequests();
while (rs.hasNext()) {
  ServiceRequestImpl r = (ServiceRequestImpl)rs.next();
  // Do something important with attributes in each row
}

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

// In ProductImpl.java
RowSet rs = (RowSet)getServiceRequests();
rs.reset(); // Reset default row set iterator to slot before first row!
while (rs.hasNext()) {
  ServiceRequestImpl r = (ServiceRequestImpl)rs.next();
  // Do something important with attributes in each row
}

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

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

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

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

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

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

create or replace package products_api is
  procedure insert_product(p_prod_id number,
                           p_name varchar2,
                           p_image varchar2,
                           p_description varchar2);
  procedure update_product(p_prod_id number,
                           p_name varchar2,
                           p_image varchar2,
                           p_description varchar2);
  procedure delete_product(p_prod_id number);
end products_api;

以降の項では、前述のビューとパッケージの組合せに基づいてエンティティ・オブジェクトを作成する方法について説明します。


注意:

この項の例では、AdvancedEntityExamplesワークスペースのEntityWrappingPLSQLPackageプロジェクトを参照します。ダウンロード方法については、この章の最初にある注意を参照してください。プロジェクトに必要な追加のデータベース・オブジェクトを設定するには、「リソース」フォルダにあるCreateAll.sqlスクリプトを、SRDemo接続に対して実行してください。

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

ビューを基にしてエンティティ・オブジェクトを作成するには、エンティティ・オブジェクト作成ウィザードを使用し、次の手順を実行します。

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

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

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

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


    注意:

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

  • 次に、「終了」をクリックします。

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

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

  • SELECT文(findByPrimaryKey()の場合)

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

  • INSERTUPDATEDELETE文(doDML()の場合)

以降の項では、最初にdoDML()操作をオーバーライドする方法を示し、次に、必要な場合にそれを拡張して、lock()およびfindByPrimaryKey()の処理をオーバーライドする方法を説明します。

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

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

例26-7 操作に基づいて異なるプロシージャを呼び出すための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()の各ヘルパー・メソッドをオーバーライドし、その特定のエンティティに適したストアド・プロシージャの呼出しを実行できます。これらの呼出しを実装する作業を簡単にするには、第25章の「ストアド・プロシージャとストアド・ファンクションの呼出し」で説明したcallStoredProcedure()ヘルパー・メソッドを、PLSQLEntityImplクラスに追加することもできます。このようにすると、このクラスを拡張するPL/SQLベースのエンティティ・オブジェクトはすべて、ヘルパー・メソッドを利用できます。

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

DML操作に対するストアド・プロシージャの呼出しを実装するには、次のようにします。

  • エンティティ・オブジェクト・エディタの「Java」パネルの「クラスの拡張」ボタンを使用して、Productエンティティ・オブジェクトのベース・クラスとしてPLSQLEntityImplクラスを設定します。

  • Productエンティティ・オブジェクトでカスタムJavaクラスを有効にします。

  • 「ソース」「メソッドのオーバーライド」メニュー項目を使用して、callInsertProcedure()メソッド、callUpdateProcedure()メソッド、およびcallDeleteProcedure()メソッドを選択します。

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

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

// In ProductImpl.java
protected void callInsertProcedure(TransactionEvent e) {
  callStoredProcedure("products_api.insert_product(?,?,?,?)",
                      new Object[] { getProdId(), getName(), getImage(),
                                     getDescription() });
}
protected void callUpdateProcedure(TransactionEvent e) {
  callStoredProcedure("products_api.update_product(?,?,?,?)",
                      new Object[] { getProdId(), getName(), getImage(),
                                     getDescription() });
}
protected void callDeleteProcedure(TransactionEvent e) {
  callStoredProcedure("products_api.delete_product(?)",
                      new Object[] { getProdId() });
}

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

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

ただし、ビューが複雑でSELECT FOR UPDATEをサポートしない場合、または他のストアド・プロシージャAPIを使用してfindByPrimaryKey()機能およびlock()機能を実行する必要がある場合は、次の項の手順を実行してもかまいません。

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

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

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

/* Added to PRODUCTS_API package */
  procedure lock_product(p_prod_id number,
                         p_name OUT varchar2,
                         p_image OUT varchar2,
                         p_description OUT varchar2);
  procedure select_product(p_prod_id number,
                           p_name OUT varchar2,
                           p_image OUT varchar2,
                           p_description OUT varchar2);

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

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

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

例26-10 ロック・パラメータに基づいて異なるプロシージャを呼び出すための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());
  }
}

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

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

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

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

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

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

  4. 文を実行します。

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

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

  7. 文を閉じます。

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

// In ProductImpl.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, VARCHAR2);
    st.registerOutParameter(4, VARCHAR2);
    // 3. Set the IN parameter value
    st.setObject(1,getProdId());
    // 4. Execute the statement
    st.executeUpdate();
    // 5. Retrieve the possibly updated column values
    String possiblyUpdatedName = st.getString(2);
    String possiblyUpdatedImage = st.getString(3);
    String possiblyUpdatedDesc  = st.getString(4);
    // 6. Populate the possibly updated attribute values in the row
    populateAttribute(NAME,possiblyUpdatedName,true,false,false);
    populateAttribute(IMAGE,possiblyUpdatedImage,true,false,false);
    populateAttribute(DESCRIPTION,possiblyUpdatedDesc,true,false,false);
  } catch (SQLException e) {
      throw new JboException(e);
  } finally {
    if (st != null) {
      try {
        // 7. Closing the statement
        st.close();
      } catch (SQLException e) {
      }
    }
  }
}

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

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

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

    ORA-00054: resource busy and acquire with NOWAIT specified
    

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

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

// In ProductImpl.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, VARCHAR2);
      st.registerOutParameter(4, VARCHAR2);
      st.setObject(1,getProdId());
      st.executeUpdate();
      String possiblyUpdatedName = st.getString(2);
      String possiblyUpdatedImage = st.getString(3);
      String possiblyUpdatedDesc  = st.getString(4);
      compareOldAttrTo(NAME,possiblyUpdatedName);
      compareOldAttrTo(IMAGE,possiblyUpdatedImage);
      compareOldAttrTo(DESCRIPTION,possiblyUpdatedDesc);
    } 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パッケージをラップするProductエンティティ・オブジェクトができます。ビュー・オブジェクトのデータ問合せ機能とエンティティ・オブジェクトのデータ検証および保存機能を明確に分離するため、通常のエンティティ・オブジェクトを使用するのと同じように、このProductエンティティ・オブジェクトを利用できます。エンティティ・オブジェクトの慣用名としてProductを使用する異なるビュー・オブジェクトを、必要な数だけ作成できます。

26.5 結合ビューまたはリモート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属性がエンティティ・オブジェクトの主キーである場合は、「一意キー」プロパティを設定して、エンティティの他の属性を代替一意キーとして識別する必要があります。

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

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

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


注意:

この項の例では、AdvancedEntityExamplesワークスペースのEnInheritanceAndPolymorphicQueriesプロジェクトを参照します。ダウンロード方法については、この章の最初にある注意を参照してください。プロジェクトに必要な追加のデータベース・オブジェクトを設定するには、「リソース」フォルダにあるAlterUsersTable.sqlスクリプトを、SRDemo接続に対して実行してください。

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

アプリケーションのデータベース・スキーマでは、同じ表の行に論理的に異なる種類のビジネス情報が格納される場合があります。このような表には、通常、各行に格納される情報の種類を決定する値を含む列があります。たとえば、SRDemoアプリケーションのUSERS表には、エンド・ユーザー、技術者、および管理者に関する情報が格納されています。また、USER_ROLE列の値usertechnician、またはmanagerによって、その行が表すユーザーの種類が決まります。

簡単なSRDemoアプリケーションの実装のこのリリースにはまだこの区別は含まれていませんが、将来のリリースでは次のことが必要になることが十分考えられます。

  • 管理者または技術者に固有の、データベースで保持される追加属性の管理

  • 管理者または技術者に対しては異なる、全ユーザーに共通の動作の実装

  • 管理者または技術者のみに固有の新機能の実装

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

ManagerおよびTechnicianエンティティ・オブジェクトに対しては、その種類のユーザーに固有の属性やメソッドを追加できます。たとえば、図では、管理者が次に部下の評価を行うときを追跡するDate型のNextReview属性が、Managerに追加されています。また、管理者に固有のperformManagerFeature()メソッドもあります。同様に、Technicianエンティティ・オブジェクトには、技術者がトレーニング認定を終了しているかどうかを追跡するCertified属性が追加されています。performTechnicianFeature()は技術者に固有のメソッドです。最後に、専門分野は技術者に対してのみ関係するため、ユーザーと専門レベルのアソシエーションがTechnicianExpertiseAreaの間に定義されています。

図26-2 継承を使用したユーザー、管理者、技術者の区別

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

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

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

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

alter table users add (
  certified varchar2(1),
  next_review date
);

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

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

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

  • 「表示」「接続ナビゲータ」を選択します。

  • 「データベース」フォルダを展開し、SRDemo接続を選択します。

  • 右クリックで表示されるポップアップ・メニューから、「SQLワークシート」を選択します。

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

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

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

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

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

前に示した例を使用し、CertifiedNextReviewを除くすべての属性はすべてのユーザーに関連し、Certifiedは技術者に固有、NextReviewは管理者に固有であると判断するものとします。この情報から、Usersエンティティ・オブジェクトを階層のベースにし、ManagerおよびTechnicianエンティティ・オブジェクトはUsersを拡張してそれぞれの固有属性を追加することにします。

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

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

  • 手順1の「名前」パネルで、エンティティの名前とパッケージを指定し、エンティティのベースとなるスキーマ・オブジェクトを選択します。

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

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

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

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

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


    注意:

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

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

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

継承階層にサブタイプ・エンティティ・オブジェクトを作成するには、最初に次のことを行います。

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

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

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

    前の項で説明したように、ベース型では、識別子属性がすでに識別されている必要があります。識別されていない場合は、継承された子を作成する前に、エンティティ・オブジェクト・エディタを使用して、親エンティティの識別子属性で「多相化識別子」プロパティを設定します。

次に、エンティティ・オブジェクト作成ウィザードを使用し、次の手順に従って、階層に新しいサブタイプ・エンティティ・オブジェクトを作成します。

  • 手順1の「名前」パネルで、エンティティの名前とパッケージを指定し、拡張フィールドの隣の「参照」ボタンをクリックして、作成しているエンティティが拡張する親エンティティを選択します。

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

  • 手順2の「属性」パネルで、「表からの新規作成」ボタンを使用して、基になっている表の列に対応し、この新しいエンティティ・サブタイプに固有の属性を追加します。

    たとえば、NEXT_REVIEW列を選択し、対応するNextReview属性をManagerエンティティ・オブジェクトに追加します。

  • 手順2の続きとして、「オーバーライド」ボタンを使用して識別子属性をオーバーライドし、属性メタデータをカスタマイズしてManagerサブタイプに個別の「デフォルト値」を提供できるようにします。

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

  • 手順3の「属性の設定」パネルで、「属性の選択」ドロップダウン・リストを使用して識別子属性を選択します。重要なのは、「デフォルト値」フィールドを変更し、作成しているエンティティ・サブタイプを定義する識別子属性に個別のデフォルト値を設定する必要があるということです。

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

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


注意:

同じ手順を繰り返して、Userを拡張するTechnicianエンティティを定義し、Certified属性を追加し、UserRole識別子属性の「デフォルト値」を値technicianに変更できます。

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

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

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

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

// In UserImpl.java
public void performUserFeature() {
  System.out.println("## performUserFeature as User");
}

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

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

// In ManagerImpl.java
public void performUserFeature() {
  System.out.println("## performUserFeature as Manager");
}

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

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

## performUserFeature as Manager

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

## performUserFeature as User

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

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

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

26.6.4 留意事項

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

前に示した例で、Userエンティティ・オブジェクトは、USERS表の実際の行に対応するとともに、階層でベース・エンティティの役割も果していました。つまり、そのすべての属性は、階層内の全エンティティ・オブジェクトに共通でした。ここで、ユーザーには固有であっても、管理者または技術者には共通しないプロパティがUserエンティティに必要な場合は、どうなるでしょう。エンド・ユーザーは顧客満足調査に参加可能だが、管理者や技術者は参加しない場合を考えてみてください。この要件に対処するためのLastSurveyDate属性は、Userエンティティには必要ですが、それを継承するManagerおよびTechnicianエンティティ・オブジェクトには無意味です。

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

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

エンティティ定義でfindByPrimaryKey()メソッドを使用するときは、それを呼び出すエンティティ・オブジェクト型のエンティティ・キャッシュのみが検索されます。前述の例では、Userエンティティ・オブジェクトでfindByPrimaryKey()を呼び出し、UserImpl.getDefinitionObject()を呼び出してそのエンティティ定義にアクセスする場合は、キャッシュ内にあるユーザーのエンティティのみを検索できます。これが求める動作である場合もあります。しかし、主キーで継承階層内のサブタイプも含めてエンティティ・オブジェクトを検索する場合は、かわりにEntityDefImplクラスのfindByPKExtended()メソッドを使用できます。この項で説明したUserの例では、この代替検索メソッドを使用すると、UserManager、またはTechnicianのエンティティ・オブジェクトを主キーで検索できます。その後は、Javaのinstanceof演算子を使用して発見された型を検査し、UserImplオブジェクトをさらに固有のエンティティ・オブジェクト型にキャストして、そのサブタイプに固有の機能で作業できます。

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

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

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

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


注意:

この項の例では、AdvancedEntityExamplesワークスペースのControllingPostingOrderプロジェクトを参照します。ダウンロード方法については、この章の最初にある注意を参照してください。

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

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

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

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

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

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

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

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

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

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

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

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

  5. Result Java Beanを作成し、新しい製品IDとサービス・リクエストIDを保持します。

  6. 結果を返します。


注意:

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

例26-13 新しいサービス・リクエストと製品の作成および新しいIDの返却

// In ExampleModuleImpl.java
public Result newServiceRequestForNewProduct(String prodName,
                                             String prodDesc,
                                             String problemDesc,
                                             Number customerId) {
  // 1. Create a new ServiceRequest
  ServiceRequestImpl newSR = createNewServiceRequest();
  // 2. Create a new Product
  ProductImpl newProd = createNewProduct();
  newProd.setName(prodName);
  newProd.setDescription(prodDesc);
  // 3. Set the product id to which service request pertains
  newSR.setProdId(newProd.getProdId().getSequenceNumber());
  newSR.setProblemDescription(problemDesc);
  newSR.setCreatedBy(customerId);
  // 4. Commit the transaction
  getDBTransaction().commit();
  // 5. Construct a bean to hold new product id and SR id
  Result result = new Result();
  result.setSvrId(newSR.getSvrId().getSequenceNumber());
  result.setProdId(newProd.getProdId().getSequenceNumber());
  // 6. Return the result
  return result;
}
private ServiceRequestImpl createNewServiceRequest() {
  EntityDefImpl srDef   = ServiceRequestImpl.getDefinitionObject();
  return (ServiceRequestImpl)srDef.createInstance2(getDBTransaction(),null);
}
private ProductImpl createNewProduct() {
  EntityDefImpl srDef   = ProductImpl.getDefinitionObject();
  return (ProductImpl)srDef.createInstance2(getDBTransaction(),null);
}

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

oracle.jbo.DMLConstraintException:
JBO-26048: Constraint "SVR_PRD_FK" violated during post operation:
"Insert" using SQL Statement
"BEGIN
  INSERT INTO SERVICE_REQUESTS(
    SVR_ID,STATUS,REQUEST_DATE,
    PROBLEM_DESCRIPTION,PROD_ID,CREATED_BY)
   VALUES (?,?,?,?,?,?)
   RETURNING SVR_ID INTO ?;
END;".
## Detail 0 ##
java.sql.SQLException:
ORA-02291: integrity constraint (SRDEMO.SVR_PRD_FK) violated
           - parent key not found

このエラーの内容は、SERVICE_REQUESTS行を挿入しようとしたが、その外部キーPROD_IDの値に対応する行がPRODUCTS表に存在しないというものです。このエラーは次のような理由で発生します。

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

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

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

26.7.3.2 ServiceRequestより前のProductの強制的なポスト

この問題は、例のコード行の順序を変更し、Productを先に作成してからServiceRequestを作成するようにすると解決できます。目先の問題にはこれで対処できますが、別のアプリケーション開発者が間違った順序でエンティティを作成する可能性が残っています。

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

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

例26-14 Productを先にポストするためのServiceRequestImplのpostChanges()のオーバーライド

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

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

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


注意:

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

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

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

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

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

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

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

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

例26-15 この新しいProductを参照するエンティティ行に対する参照の保存

// In ProductImpl.java
RowSet newServiceRequestsBeforePost = 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 service requests related
     * to this new product before calling super
     */
    newServiceRequestsBeforePost = (RowSet)getServiceRequest();
  }
  super.postChanges(TransactionEvent);
}

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

例26-16 新しいProdId値による参照しているエンティティ行のカスケード更新

// In ProductImpl.java
protected void refreshFKInNewContainees() {
  if (newServiceRequestsBeforePost != null) {
    Number newProdId = getProdId().getSequenceNumber();
    /*
     * Process the rowset of service requests that referenced
     * the new product prior to posting, and update their
     * ProdId attribute to reflect the refreshed ProdId value
     * that was assigned by a database sequence during posting.
     */
    while (newServiceRequestsBeforePost.hasNext()) {
      ServiceRequestImpl svrReq =
        (ServiceRequestImpl)newServiceRequestsBeforePost.next();
      svrReq.setProdId(newProdId);
    }
    closeNewServiceRequestRowSet();
  }
}

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

26.8 属性の自動再計算の実装

6.10項「エンティティ・オブジェクトへの一時および計算属性の追加」では、エンティティ・オブジェクトに計算属性を追加する方法を説明しました。値の計算式が、エンティティ内の他の属性値に依存することがよくあります。たとえば、注文の明細項目を表すLineItemエンティティ・オブジェクトについて考えます。LineItemは、PriceQuantityなどの属性を持つ場合があります。ExtendedTotalという名前の計算属性を導入し、単価と数量の積を計算します。Price属性またはQuantity属性が変更された場合、計算属性ExtendedTotalが更新されて新しい合計が反映されることが期待されますが、これは自動的には行われません。スプレッドシートとは異なり、エンティティ・オブジェクトには、計算式が依存する属性を認識する組込みの式評価エンジンはありません。

このような限界に対処するには、エンティティ・オブジェクトのフレームワーク拡張クラスで、再計算機能を追加するコードを記述できます。SRDemoアプリケーションのSREntityImplフレームワーク拡張クラスには、例26-17で示すようなコードが含まれており、この処理を行います。これは、高度な式評価機能を実装しようとするものではありません。カスタム・プロパティのメカニズムを利用して、Aのような別の属性が変更されたときに再計算する必要のある属性(XYZなど)に関する宣言されたヒントを、開発者が提供できるようにするものです。

この汎用機能を利用するには、エンティティ・オブジェクトの開発時に次のことを行います。

この機能を実装するには、SREntityImplクラスのnotifyAttributesChanged()メソッドをオーバーライドします。このメソッドは、エンティティ・オブジェクトの属性の値が変化するたびに呼び出されます。引数として、メソッドは2つの配列を受け取ります。

このコードが実行する基本的な手順は次のとおりです。

  1. カスタム・エンティティ・プロパティのセットを反復処理します。

  2. プロパティ名がRecalc_で始まっている場合は、この接頭辞に続く部分文字列を取得し、値が変化すると他の属性の再計算をトリガーする属性の名前を認識します。

  3. 再計算をトリガーする属性の索引を特定します。

  4. 変化した属性の索引の配列に、再計算をトリガーする属性の索引が含まれる場合は、プロパティのカンマで区切られた値をトークン化して、再計算する属性の名前を識別します。

  5. 再計算する属性があった場合は、その属性索引を、値が変化した属性の新しいint[]に追加します。

    新しい配列は、attrIndices配列の既存の配列要素を新しい配列にコピーし、それに新しい属性索引番号を追加することで作成します。

  6. 更新された可能性のある変化した属性の配列を使用して、superを呼び出します。

例26-17 導出属性を自動的に再計算するためのエンティティ・フレームワーク拡張コード

// In SREntityImpl.java
protected void notifyAttributesChanged(int[] attrIndices, Object[] values) {
  int attrIndexCount = attrIndices.length;
  EntityDefImpl def = getEntityDef();
  HashMap eoProps = def.getPropertiesMap();
  if (eoProps != null && eoProps.size() > 0) {
    Iterator iter = eoProps.keySet().iterator();
    ArrayList otherAttrIndices = null;
    // 1. Iterate over the set of custom entity properties
    while (iter.hasNext()) {
      String curPropName = (String)iter.next();
      if (curPropName.startsWith(RECALC_PREFIX)) {
        // 2. If property name starts with "Recalc_" get follow attr name
        String changingAttrNameToCheck = curPropName.substring(PREFIX_LENGTH);
        // 3. Get the index of the recalc-triggering attribute
        int changingAttrIndexToCheck =
            def.findAttributeDef(changingAttrNameToCheck).getIndex();
        if (isAttrIndexInList(changingAttrIndexToCheck,attrIndices)) {
          // 4. If list of changed attrs includes recalc-triggering attr,
          //    then tokenize the comma-separated value of the property
          //    to find the names of the attributes to recalculate
          String curPropValue = (String)eoProps.get(curPropName);
          StringTokenizer st = new StringTokenizer(curPropValue,",");
          if (otherAttrIndices == null) {
            otherAttrIndices = new ArrayList();
          }
          while (st.hasMoreTokens()) {
            String attrName = st.nextToken();
            int attrIndex = def.findAttributeDef(attrName).getIndex();
            if (!isAttrIndexInList(attrIndex,attrIndices)) {
              Integer intAttr = new Integer(attrIndex);
              if (!otherAttrIndices.contains(intAttr)) {
                otherAttrIndices.add(intAttr);
              }
            }
          }
        }
      }
    }
    if (otherAttrIndices != null && otherAttrIndices.size() > 0) {
      // 5. If there were any attributes to recalculate, add their attribute
      //    indexes to the int[] of attributes whose values have changed
      int extraAttrsToAdd = otherAttrIndices.size();
      int[] newAttrIndices = new int[attrIndexCount + extraAttrsToAdd];
      Object[] newValues = new Object[attrIndexCount + extraAttrsToAdd];
      System.arraycopy(attrIndices,0,newAttrIndices,0,attrIndexCount);
      System.arraycopy(values,0,newValues,0,attrIndexCount);
      for (int z = 0; z < extraAttrsToAdd; z++) {
        newAttrIndices[attrIndexCount+z] =
         ((Integer)otherAttrIndices.get(z)).intValue();
        newValues[attrIndexCount+z] =
         getAttribute((Integer)otherAttrIndices.get(z));
      }
      attrIndices = newAttrIndices;
      values = newValues;
    }
  }
  // 6. Call the super with the possibly updated array of changed attributes
  super.notifyAttributesChanged(attrIndices, values);
}

SRDemoアプリケーションのServiceHistoryエンティティ・オブジェクトは、Recalc_SvhTypeという名前のカスタム・エンティティ・プロパティと値Hiddenを設定して、この機能を使用しています。これにより、SvhType属性の値が変更されるたびに、計算属性Hiddenの値が再計算されます。

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

ADF Business Componentsには、開発者が使用できる組込みの宣言的検証規則の基本セットが付属しています。ただし、エンティティ・オブジェクト用のバリデータ・アーキテクチャの最も強力な機能は、独自のカスタム検証規則を作成できることです。同じ種類の検証コードを繰り返し作成している場合は、カスタム検証規則クラスを作成し、このような共通の検証パターンをパラメータ化された方法で取得できます。定義したカスタム検証規則クラスは、JDeveloperに登録し、組込み規則と同じように簡単に使用できます。実際、以降の項で説明するように、カスタム検証規則をカスタムUIパネルにバンドルすることさえできます。JDeveloperは、これを自動的に利用して、検証規則が必要とするパラメータを開発者が簡単に使用および構成できるようにします。

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

エンティティ・オブジェクト用のカスタム検証規則を作成するには、oracle.jbo.rulesパッケージのJboValidatorInterfaceを実装するJavaクラスを作成します。例26-18で示すように、このインタフェースには、1つのメインvalidate()メソッドと、Descriptionプロパティ用のgetterおよびsetterメソッドが含まれます。

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

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

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

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

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

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

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

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

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

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

例26-19 SRDemoアプリケーションのカスタムのDateMustComeAfterRule

package oracle.srdemo.model.frameworkExt.rules;
// NOTE: 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ファイルにパッケージします。SRDemoアプリケーションでは、FrameworkExtensionsプロジェクトにDateMustComeAfterRule.deployデプロイメント・プロファイルが含まれます。このプロファイルは、実行時と設計時に使用するため、規則クラスをDateMustComeAfterRule.jarという名前のJARファイルにパッケージします。

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

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

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

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

検証規則カスタマイザをビジュアルに作成している図

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

通常は、カスタマイザ・クラスとこのBean情報を、設計時専用の独立したJARファイルにパッケージします。SRDemoアプリケーションのFrameworkExtensionsプロジェクトには、これらのクラスをDateMustComeAfterRuleDT.jarにパッケージするデプロイメント・プロファイルが含まれます。

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

package oracle.srdemo.model.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);
  }
}

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

エンティティ・オブジェクトを含むプロジェクトでカスタム検証規則を使用するには、次の手順に従います。

  1. 規則のJARファイル用のプロジェクト・レベルのライブラリを定義します。

  2. このライブラリをプロジェクトのライブラリ・リストに追加します。

  3. 「プロジェクト・プロパティ」ダイアログの「ビジネス・コンポーネント」「登録済の規則」パネルを使用して、1つまたは複数の検証規則を追加します。

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

図26-5は、SRDemoアプリケーションのServiceRequestエンティティ・オブジェクトに対するエンティティ・オブジェクト・エディタの「検証」パネルを示しています。DateMustComeAfter規則を編集するときは、規則クラスのBeanInfoからカスタム編集パネルが自動的に検出されて、設計時に開始日と終了日の属性名を開発者に示すために使用されます。JDeveloperは、実行時に検証規則が不合格になった場合にエンド・ユーザーに対して示される翻訳可能なエラー・メッセージを取得するためのサポートを提供します。

図26-5 JDeveloperのカスタム・エディタのパネルに表示されたカスタム検証規則

カスタム検証規則の「検証規則の編集」を示す図