ADF ToyStore
- 5: サンプル・アプリケーションの分析 「カスタマイズと拡張」 -

前に戻る | 目次に戻る | 次へ進む

ここでは、次の内容を中心としてサンプル・アプリケーションの詳細について説明します。

注意

ここでは、 「サンプル・アプリケーションのインストールと設定」の項の指示に従っていること、 ADFToyStore.jwsのワークスペースがJDeveloper で開かれていること を前提にしています。


フレームワークのデフォルト動作のカスタマイズ

フレームワーク・ベースのJ2EE開発には、次の2つのメリットがあります。

  1. アプリケーション・コンポーネントは、ベース・フレームワーク機能に基づいている
  2. アプリケーション全体の変更が必要な場合に、ベース・フレームワークを拡張できる

たとえば、ADFフレームワークのベース EntityImplクラスは、ユーザーがエンティティ・オブジェクトに要求する機能のすべてをサポートしていなくても、問題はありません。このような場合に、オラクル社に対して拡張機能の要求を出したり、ADFが対象の機能を実装してくれるのを待つ必要もありません。フレームワークのカスタマイズによって自身で機能を追加できるのですから。

oracle.jbo.server.EntityImplを拡張するJavaクラスを作成し、次のようにして、そのクラスに機能を追加します。

public class ToyStoreEntityImpl extends oracle.jbo.server.EntityImpl {
  /* 
   * Any new or customized entity object behavior goes here
   */
}

次に、アプリケーションにエンティティ・オブジェクトを作成するときに、デフォルトのEntityImplベース・クラスではなく、 ToyStoreEntityImplから拡張するように、コンポーネントを設定します。 図59は、ADF ToyStoreサンプル・アプリケーションのエンティティ・オブジェクトの一つである Accountで、これを実際に実施している様子を図式化したものです。

コンポーネントは、カスタマイズされたフレームワークのベース・クラスを拡張可能
図59: コンポーネントは、カスタマイズされたフレームワークのベース・クラスを拡張可能

Strutsのフレームワークの多くの局面に対しても、ADFフレームワークのベース・クラスのカスタマイズと同じようなことが実施できます。 この項では、ADF ToyStoreサンプル・アプリケーションのための、ADFフレームワークのカスタマイズについて中心に説明します。 これらのすべてのカスタマイズ・クラスは、 FwkExtensionsプロジェクトにあります。

注意

ADF ToyStoreサンプル・アプリケーションのすべてのコードと同様に、フレームワークのカスタマイズ・クラスには、ソース内に、何を行うかを説明した多数のコメントが付加されています。それぞれのフレームワークの拡張機能の内容の詳細については、FwkExtensionsプロジェクトの実際のコードを参照するとよいでしょう。


コントローラ層に対するStrutsフレームワークのカスタマイズ

toystore.fwk.controllerパッケージには ToyStoreDataForwardActionがあります。これは、ベースのADF DataForwardActionを拡張して、次のようなカスタマイズ機能を加えています。

  1. initializeModelForPage()initializeBindingsForPage()という2つのDataActionライフサイクル・メソッドを追加しました。これらのメソッドは、アクションによって処理されるポストバック・イベントがない場合に、デフォルトの prepareModel()ライフサイクル・メソッドの直前および直後に起動されます。 デフォルトでは、これらのメソッドは何も処理せず、サブクラスによってオーバーライドされた場合にのみ機能します。
    • prepareModel()フェーズでバインディング・コンテナ内のイテレータを実行する前に、カスタム・メソッドをコールして、ビジネス・サービス問合せにバインド変数を設定したい場合には、 initializeModelForPage()が役に立ちます。 バインド変数の設定は、別のバインディング・コンテナを持つDataActionとそこでのカスタム・メソッドの呼出しによっても実現できます。ただしこれは、バインド変数の設定が要求されるすべてのページでは、ページ・フロー上、アクションが一つ余計に必要になることを意味します。これをシンプルにしたいために、このメソッドを用意しました。
    • initializeBindingsForPage() を使用すると、ページにバインディングの値を出力する前に、その値をプログラム・コードで修正することができます。
  2. DataControlの名前に基づいてアプリケーション・モジュールを取得するためのヘルパー・メソッド getApplicationModule()を追加しました。
  3. reportErrors()メソッドをオーバーライドすることによって、ADFによってまとめられた例外ツリーをStrutsのActionErrorオブジェクトに変換し、ユーザーに表示するデフォルトの方法をカスタマイズしました。
  4. ブール値を返すメソッドreleaseStateless()を追加しました。サブクラスは、このメソッドをオーバーライドして true を返すようにすることで、現在のバインディング・コンテナで使用中のすべてのデータ・コントロールを、リクエストの最後にステートレス・モードで解放することを指示できます(デフォルトでは false を返すようになっており、このため、ADFアプリケーション・モジュールは、フレームワークによって保留ステートが管理されます)。 このクラスには、releaseDataControlStateless()メソッドを使用した別のアプローチも用意されています。リクエストの最後に、このメソッドを、データ・コントロール名を指定する形で明示的にコールすると、ステートレス・モードで解放することができます。
  5. evalEL()ヘルパー・メソッドを追加しました。これは、EL式を評価するためにサブクラスのアクションでコールすることができます。
  6. invokeEventAction()ヘルパー・メソッドを追加しました。これは、ADFのデフォルトの宣言的なアクション・バインディングによる処理(イベント名と一致するアクション・バインディングを呼び出す)を実施します。サブクラスのアクションで、ADFのデフォルトの宣言的なアクション・バインディング処理の前または後に何か固有のコードを実施したい場合のために用意したもので、on EventNameイベントハンドラ・メソッドの中で、このメソッドをはさむように記述することができます。

モデル層のビジネス・オブジェクトに対するカスタマイズ

toystore.fwk.model.businessobjectsパッケージに ToyStoreEntityImplクラスを作成しました。これはエンティティ実装のベース・クラスを拡張し、次の機能を追加したものです。

  1. 属性値を強制的に UPPERまたは lowerにする機能を宣言する。
  2. 一意キーに違反したときに、カスタム例外 EntityAlreadyExistsExceptionをスローする

これらの2つのカスタマイズは、フレームワークの setAttributeInternal()メソッドをオーバーライドすることによって実現しています。 宣言的な大文字/小文字の設定機能を実装するために、 super.setAttributeInternal()をコールする前にいくつかのカスタム・コードを追加しました。 また、カスタム例外処理をサポートするために、super.setAttributeInternal()のコールを try で囲み、catchブロックにいくつかのカスタム・コードを記述しています。

プライベート・メソッド foldCaseOfStringIfCasePropertySet()は、 Caseという名前の属性レベルのカスタム・プロパティに対するチェックを行い、それが指定されている場合は、値が lowerupperかによって適切に動作します。 Accountエンティティの Stateおよび Country属性、および Ordersエンティティの ShipstateBillstateShipcountry、および Billcountry属性は、 upper に設定されたカスタム・プロパティを持っていることが、XMLメタデータ内で確認できます。

ビジネス・オブジェクト用のパッケージのディレクトリ内のAccount.xmlファイルまたは Orders.xmlファイルを調べてみると、XML内の<Attribute>要素内でネストされた部分に、属性レベルのカスタム・プロパティがあることがわかります (例25を参照ください)。

例25: XMLメタデータにおけるカスタム・プロパティ
<Entity Name="Account" DBObjectName="ACCOUNT" AliasName="Account"
        BindingStyle="Oracle" RowClass="toystore.model.businessobjects.AccountImpl"
        MsgBundleClass="toystore.model.businessobjects.common.AccountImplMsgBundle">
   :
   <Attribute Name="State" IsNotNull="true" Precision="2" Type="java.lang.String"
      ColumnName="STATE" ColumnType="VARCHAR2" SQLType="VARCHAR" >
      <Properties>
         <Property Name ="Case" Value ="Upper" />
      </Properties>
   </Attribute>
    :
</Entity>

図60は、エンティティ・オブジェクトに対する、ベース・クラスのカスタマイズ方法を示しています。 オブジェクト・エディタの 「Java」タブには、 「拡張」ダイアログを表示するための 「クラスの拡張」ボタンがあります。ユーザーはここで、ベース・クラスとしてカスタム・クラスを使用するように設定できます。 この図はエンティティ・オブジェクト・ウィザードを示していますが、同様の作業はビュー・オブジェクトとアプリケーション・モジュールにも実施できます。

Javaパネルからフレームワークのベース・クラスを設定する
図60: Javaパネルからフレームワークのベース・クラスを設定する

注意

特に必要がない場合でも、ADF Business Componentsフレームワークの拡張クラス・セットを作成する法が良いこともあります。 これは、後で、すでに作成されている特定のタイプのすべてのコンポーネントに適用したいような新しい機能に対処しなければならない場合に、非常に役に立ちます。たとえば、ビジネス・コンポーネントに対してフレームワーク・カスタマイズ・クラスの複数の層を設定し、最初の層は「会社全体」のクラス・セットとして oracle.jbo.server.*のベース・コンポーネントを拡張します。また、それぞれのアプリケーション・プロジェクトに対して、プロジェクト・レベルのフレームワーク・カスタマイズ・クラスのセットを作成します。 「ツール→設定...」ダイアログの「ビジネス・コンポーネント→ベース・クラス」パネルにおいて、IDEレベルでのADF Business Componentsベース・クラスを設定することができます。 特定のプロジェクトに対してこれらのグローバル設定をオーバーライドしたい場合は、「プロジェクト・プロパティ」ダイアログの 「ビジネス・コンポーネント→ベース・クラス」パネルで設定することもできます。


モデル層のデータ・アクセス・コンポーネントに対するカスタマイズ

toystore.fwk.model.dataaccessパッケージにおいて、次のカスタマイズを実装しました。

ビジネス・サービス層に対するカスタマイズ

toystore.fwk.model.serviceパッケージにおいて、 ToyStoreDBTransactionImplクラスは、フレームワークの oracle.jbo.server.DBTransactionインタフェースのカスタマイズ実装を提供します。 ここでは、コードの書き直しを最小限にするために、 oracle.jbo.server.DBTransactionImpl2クラスを拡張し、 postChanges()メソッドをオーバーライドしています。 カスタマイズされた postChanges()の実装では、変更内容のデータベースへの反映(ポスト)時に生成されたすべてのDMLConstraintExceptionをキャッチし、アプリケーション固有のエラー・メッセージとともに JboExceptionをスローします。このエラー・メッセージは、違反したデータベースの制約名に基づいて toystore.fwk.exceptions.ErrorMessagesメッセージ・バンドルから参照されます。

このパッケージに付随している ToyStoreDBTransactionFactoryクラスは、フレームワークの oracle.jbo.server.DatabaseTransactionFactoryクラスを拡張して、カスタマイズされた ToyStoreDBTransactionImplのインスタンスを返します。

TransactionFactoryという名前のADFアプリケーション・モジュールの構成プロパティには、カスタム DBTransactionFactoryクラスの完全修飾クラス名を設定する必要があるため、

TransactionFactory=toystore.fwk.model.service.ToyStoreDBTransactionFactory

のようにして、カスタムDBトランザクション実装クラスを使用できるようにします。

ToyStoreApplicationModuleImplクラスはベースのアプリケーション・モジュール実装クラスを拡張して、getConfigurationProperty()というヘルパー・メソッドを追加します。 これは、実行時に、アプリケーション・モジュールの構成定義のプロパティを取得、または、存在しない場合は、Java Systemプロパティで同じ名前のプロパティ値を戻します。 これは、2箇所のサンプル・アプリケーションの実装コード (toystore.model.dataaccessパッケージの ShoppingCartImpl、および toystore.model.servicesパッケージの LockAllInventoryItemsHelper)で使用されており、構成プロパティの値に基づいて、適切な処理を選択します。

フレームワーク拡張としてのカスタム検証ルール

toystore.fwk.rulesパッケージには VerifyStateForCountryRuleクラスが含まれています。このクラスは、 oracle.jbo.server.rulesパッケージの JbiValidatorインタフェースを実装し、パラメータで指定可能なカスタムのビジネス・ルールを提供します。 Accountエンティティと Ordersエンティティは、再利用可能なこの検証ルールを宣言的に使用しています。 たとえば Account.xmlファイルには、次のようなXMLコードがあります。この部分では、このカスタム・ビジネス・ルールの使用の定義と、汎用的なルールで検証のために使用するパラメータ値の指定が行われています。 ここでは、 countryAttributeNameパラメータが Countryに、 stateAttributeNameStateに設定されていることがわかります。

<ValidationBean
  OperandType="LITERAL"
  Name="VerifyStateForCountryRule"
  BeanClass="toystore.fwk.rules.VerifyStateForCountryRule" >
  <NamedData
    NDName="countryAttributeName"
    NDType="java.lang.String"
    NDValue="Country" >
  </NamedData>
  <NamedData
    NDName="stateAttributeName"
    NDType="java.lang.String"
    NDValue="State" >
  </NamedData>
</ValidationBean>

Orders.xmlファイルにも、類似したXMLタグ・ブロックがあります。ここでは、異なるパラメータ値が使用されており、 countryAttributeNameパラメータが Shipcountryに、 stateAttributeNameShipstateに設定されています。

VerifyStateForCountryのコードと StateForCountryビュー・オブジェクト定義をみると、いくつかの興味深いテクニックが使用されていることがわかります。

  1. toystore.fwk.rules.dataaccess.StatesForCountryビュー・オブジェクトを必要なときに作成する
  2. ルールを連続して実行する場合に、同じビュー・オブジェクト・インスタンスを再利用する
  3. Maximum Fetch Size を1に設定して、1行だけのフェッチであることがわかっているビュー・オブジェクトのパフォーマンスを向上させる

また、この例は、再利用可能なルールを作成し、その中でビュー・オブジェクトのようなコンポーネントを内部使用することが可能であることも表しています。このようなコンポーネントは、ルールと一緒に、ライブラリ(FwkExtensionsプロジェクトのデプロイメント・プロファイルによって作成される FwkExtensions.jarなど)としてパッケージ化することで再利用可能になります。

カスタムXSQLアクション・ハンドラ

toystore.fwk.xsql.ADFViewObjectクラスは、現在のバインディング・コンテナ内のビュー・オブジェクトのデータをXML/XSLTベースのビュー層に出力するための、カスタムXSQLアクション・ハンドラを実装します。


サンプル・アプリケーションのその他のポイント

この項では、サンプル・アプリケーションの実装で、まだ取り上げていない事項について説明します。

ビュー・オブジェクトのフェッチに関するチューニング・パラメータについて

ADFビュー・オブジェクト・コンポーネントには、類似した名前の2つのプロパティ、 FetchSizeMaxFetchSizeがあります。これらのプロパティは、パフォーマンス・チューニングの観点で、重要です。 これらのプロパティは、ビュー・オブジェクト・エディタの 「チューニング」パネルで宣言的に設定することも、適切なAPIを使用して実行時にプログラムによって設定することもできます。

大規模なバッチ処理における、データベースからの行のフェッチ

FetchSizeプロパティは、データベースから一度にフェッチする行数を決定します。 たとえば、ビュー・オブジェクトの問合せの結果で200行がヒットするケースで、その FetchSize50に設定されている場合、データベースから200行を取得するために、一度に50行ずつ4回(200/50=4)のラウンドトリップを行います。 FetchSizeがデフォルトの 1に設定されている場合は、ビュー・オブジェクトは、これらの行をフェッチするために 200 回(200/1=200)のラウンドトリップを行います。

FetchSizeの値を 大きく設定しすぎると、JDBCの行バッファが毎回一杯にならない場合でも、フェッチ時に必要な量よりも多くのメモリーを使用することを意味します。 したがって、各行の属性が非常に少ないという場合を除いては、値を200以上には設定しません。 FetchSizeの値を小さく設定しすぎる、または適切ではないにもかかわらずデフォルトの 1のままにしておくと、データベース・サーバーからビュー・オブジェクト問合せ結果を取得するために余計なラウンドトリップが多数発生することになります。

こういった理由から、それぞれのビュー・オブジェクトで、 FetchSizeの値を考慮して、デフォルトの 1で適切かどうかを確認しておくことが必要です。 デフォルトの1が適切でない場合は、対象のビュー・オブジェクト問合せで、取得する行数として妥当な値を設定します。 toystore.model.dataaccessパッケージの FindProductsItemsForSaleLineItemsProductListProductsInCategoryReviewLineItemsなど、複数の行をフェッチするADF ToyStoreのビュー・オブジェクトは、すべて FetchSize10に設定しています。

注意

FetchSizeのデフォルト値は 1です。
一度に複数の行をフェッチして表示するビュー・オブジェクトの場合は、どのような値に設定したら適切かを常に考慮する必要があります。 経験からすると、一度に N 行を表示するようなページの場合、イテレータのRangeSize(レンジ・サイズ)はN に設定されているはずです。この場合、ベースとなるビュー・オブジェクトの FetchSize少なくとも N+1に設定されているほうが良いでしょう(N+1にしておくことで、次のN行分のデータがあるかどうかを、次のフェッチなしで認識できます) 。 このようにすると、各ページのすべての行を1回のラウンドトリップで確実にフェッチできます。


次に、もう一つのパラメータMaxFetchSizeについて説明します。このパラメータの名前は、今見た FetchSizeと混同される場合がよくあります。

データベースからフェッチする行数の制限

MaxFetchSizeプロパティは、ビュー・オブジェクトがデータベースからフェッチする行数についての上限を設定します。 デフォルトの値は -1で、これは上限を適用しないことを意味します。 つまりデフォルトでは、問合せからすべての行をフェッチします。 前述のビュー・オブジェクトの問合せで、結果に同じ200行があることがわかっていても、 MaxFetchSize10に設定されている場合は、最初に200行のうちの10行のみがフェッチされます。

注意

デフォルトでは、ビュー・オブジェクトの FetchModeFETCH_AS_NEEDED(必要に応じて)になっています。これは、ユーザーがビュー・オブジェクトの行を反復するときに、ビュー・オブジェクトの行が遅延して取得され、 -1に設定されていない場合は、フェッチできる行の最大数が MaxFetchSizeに制限されることを意味します。


MaxFetchSizeプロパティの値は、最後のセクションで見た FetchSizeプロパティと直行しています。 たとえば MaxFetchSize10FetchSize50に設定されており、サンプルの200行のビュー・オブジェクトについて問合せを実行すると、1回のラウンドトリップでデータベースに対して最初の10行が返されます。これは、最初の10行に対処するのに 50FetchSizeで十分であるためです。 FetchSize1(デフォルト)の場合は、想像どおり、データベースで最初の10行をフェッチするために10回のラウンドトリップが発生します。

実際には、デフォルト以外の MaxFetchSizeの値は、次の内容を示す場合に使用されます。

  1. 結果が1行であることが予想できるケース(MaxFetchSize=1
  2. 挿入のみでビュー・オブジェクトを使用するケース(MaxFetchSize=0

最大が1行であると予想できる場合に、MaxFetchSize1に設定すると、すぐれたパフォーマンスが得られます。これはビュー・オブジェクトが、結果の中にまだ行があるかどうかを確認するためにもう一度フェッチすることがないためです。 MaxFetchSize0に設定すると、ビュー・オブジェクトでは問合せが実行されません。 このようなビュー・オブジェクトでは、行を新しく作成したり、 findByKey()setCurrentRow()の組合せによる検索はできますが、通常の問合せでは行はフェッチされません。

ADF ToyStoreのサンプル・アプリケーションでは、 Accountsビュー・オブジェクトでこの手法が使用されています。 このビュー・オブジェクトの findAccountByUsernamePassword()メソッドのコードは、ユーザー名とパスワードを使用して問合せを実行する前に MaxFetchSize1に設定し、その後で設定を0に戻します。

// From: toystore.model.dataaccess.AccountsImpl
  public boolean findAccountByUsernamePassword(String username, String password) {
    /*
     * We're expecting either zero or 1 row here, so indicate that
     * by setting the max fetch size to 1.
     */
    setMaxFetchSize(1);
    setWhereClause("username = :0 and password = :1");
    setWhereClauseParam(0, username);
    setWhereClauseParam(1, password);
    executeQuery();
    boolean found = first() != null;
    setWhereClause(null);
    setWhereClauseParams(null);
    setMaxFetchSize(0);
    return found;
  }

ToyStoreServeiceImplアプリケーション・モジュール実装クラスのprepareToEditAccountInfoFor()および prepareToCreateNewAccount()のメソッドは、それぞれの処理(キーによって行を検索する、または、新しい行を作成する)の前に、 MaxFetchSize0に設定しています。

念のため、アプリケーション・モジュールの prepareSession()メソッドに次のコードを記述して、 AccountsMaxFetchSizeが必ず 0になるようにもしています。

// From toystore.model.services.ToyStoreServiceImpl
  protected void prepareSession(Session session) {
    super.prepareSession(session);
    getAccounts().setMaxFetchSize(0);
  }

このようにすると、リクエスト間で、アプリケーション・モジュール・プールから得られるモジュールのインスタンスが同じものではなくなってしまうケースでも、この設定(MaxFetchSize=1)がデフォルトになることが保証されます。 prepareSession()メソッドは、プールから ApplicationModuleインスタンスが使用されるたびに必ずコールされるため、このコードはここに記述するのが適切です。

バッチ・モードに関するいくつかの所見

ADFバインディングおよびデータ・コントロールの層の導入によって、フロントエンドのクライアント・テクノロジおよびバックエンドのビジネス・サービス実装を自由に選択できるようになります。どの選択をしても同じような開発手法で進めていくことができるというこの一貫性が、どんなタイプのデータ・コントロールも汎用JavaBeanと同レベルの機能しか利用できないという制限を生むことを意味するものではないことに注意してください。 イテレータ、バインディング、およびデータ・コントロールを首尾一貫した方法で扱うことができる一方で、特定のデータ・コントローラ・プロバイダが提供している固有な機能を利用することもできます。 

たとえば、 図61は、ADF Business Componentsのアプリケーション・モジュールに基づくデータ・コントロールは、他のデータ・コントロールは持たない 同期モード(Sync Mode)プロパティを持っていることを表しています。 JDeveloper 10gでは、新しいバッチ・モードを使用できるように、同期モード (Sync Mode)の値がデフォルトで「Batch」に設定されています。

アプリケーション・モジュールをベースとしたデータ・コントロールの同期モード・パラメータ
図61: アプリケーション・モジュールをベースとしたデータ・コントロールの同期モード・パラメータ

バッチ・モードはネットワークのラウンドトリップを減らすための機能で、その名前が示すように、データ関連の操作を、より大きな単位で実行するためにまとめて処理します。

クライアント層はビュー・オブジェクトやアプリケーション・モジュールと連携して、現行タスクに必要なデータを含むビュー・オブジェクトに対するデータ処理のためのセットアップ操作を行います。 これらの操作は、バッチ・モード用のクライアント側ApplicationModuleの実装クラス (同期モードBatchに設定されている場合に使用される ApplicationModuleインタフェースの実装クラス)を使用してまとめて実行されます。

アプリケーションは、次のメソッドをコールしてすべてのデータ処理を実行し、データソースから、期待するすべてのデータを1回のネットワーク・ラウンドトリップで取得します。

yourBindingContainer.refreshControl();

バインディング・コンテナに対して refreshControl()をコールすると、バインディング・コンテナで使用されている各データ・コントローラのデータ・プロバイダ上で、 sync()メソッドがコールされます。 ADFアプリケーション・モジュールをバッチ・モードで実行している場合は、このデータ・プロバイダはクライアント側のApplicationModuleオブジェクトになります。 この sync()の操作によって、保留中の処理およびクライアント側キャッシュ内のすべてのデータ変更が、サーバー側のビジネス・サービスに送信されて実行されます。 中間層のコンポーネントによって行われたすべてのデータ変更(問合せの実行、プログラム・コードによるデータ修正)は、同じラウンドトリップでクライアント・キャッシュに戻されます。

最終的には、処理をバッチ・モードで使用することで、クライアントがプールからアプリケーション・モジュールを使用する期間が短くなるため、クライアント層とビジネス・サービスが同じJ2EE Webコンテナ上にある場合でも、アプリケーションのスケーラビリティが向上する予定です。ただし、現行リリース(9.0.5)では、このように同じ場所に配置されて実行されている場合には、Immediateモードの方がパフォーマンスが良いという結果が出ています。

将来的には、バッチ・モードが最もスケーラビリティのある選択となるよう、研究開発に注力しており、段階的に目標に到達しつつあります。 ユーザーが、(Strutsのアクション・クラスのような)クライアント層とビジネス・サービス層を同じWebコンテナに配置するよう計画している場合、ネットワーク通信量に対する考慮は気にならなくなりますが、それでも、バッチ・モードによる開発時のメリットがあります。

ADFアプリケーションをバッチ・モードで実行およびテストすると、クライアント層では、ベースの実装クラスに対する操作ではなく、コンポーネント・インターフェースだけを操作するベスト・プラクティスのアプローチが保証されます。バッチ・モードの場合に、このベスト・プラクティスのアプローチに反したコーディングを実施していた場合(インターフェースに対するメソッド・コールではなく、ビジネス・サービス層の *Implクラス( ViewObjectImplViewRowImplEntityImplまたは ApplicationModuleImpl、あるいはこれらを拡張している自身のクラス)に対する型変換を実施していた場合など)は、ClassCastExceptionのエラーが発行されるからです。

このベスト・プラクティスのアプローチを維持しておくと、後で必要に応じて、アプリケーションを物理的に別のクライアント層とサーバー層に簡単に配置することができます。 また、今後のリリースで予定されている、バッチ・モードのスケーラビリティに関する新しい最適化の機能が実現された場合に、そのメリットを利用することも可能になります。 これらの理由により、JDeveloper 10gではバッチ・モードをデフォルトにしています。

現時点では、ADF ToyStoreサンプル・アプリケーションのようなWebベースのアプリケーションに対して、開発およびテストの際には Batchの同期モードを使用し、最終テストおよび本番環境へのデプロイの前に、Immediateの同期モードに切り替えることをお薦めしています。 同期モードの変更は、 図61に示すように、プロパティを変更するのと同じくらい簡単です。

バッチ・モードに関するもう一つの重要なポイントは、ADF DataAction は、ユーザーがバッチ・モードのアプリケーション・モジュールを同期化することを意識しなくても済むように設計されているということです。実際、デフォルトのDataActionページのライフサイクル処理では、ユーザーのバインディング・コンテナ上において適切なタイミングで refreshControl()処理を自動的に実行します。 具体的には、ライフサイクル処理の最初に prepareModel()フェーズで1回、リクエストの最後に refreshModel()フェーズで1回実行します(ビジネス・サービスから返されたデータに最初にアクセスするのがビュー層であるという仮定に基づいて設計されているため、バッチ・モードの同期処理を、Strutsの RequestProcessorが制御をページへ転送する前の、データ・アクションのライフサイクルの最後に実行しています)。 Strutsのアクション自身においてビジネス・サービスから取得されたデータを反復処理しなければならない場合は、データを反復するアクション・コードの直前に、 refreshModel()を特別にコールする必要があります。 この処理を実施しない場合、

JBO-25048: Operation XXXXXX is invalid for a working set view object
または、他の InvalidOperExceptionタイプのエラー

が発生します。なお、将来バージョンでは、通信が必要ないことがわかると、さらにネットワーク・ラウンドトリップをなくすような、よりスマートな処理を実現する、自動バッチ・モードによる同期処理が提供されます。 また、データ・コントロールがプールからアプリケーション・モジュール・インスタンスをチェックアウトする際の時間を短縮できるよう、さらなる改善も行われます。

注意

バッチ・モードの使用中に問題が発生した場合は、プロパティ・インスペクタを使用して 同期モードプロパティの値を Immediateに設定して、問題がバッチ・モードに特有のものであるか確認してください。


サーバー側でのモデル操作コードのカプセル化

ADF ToyStoreサンプル・アプリケーションでは、アプリケーション・ビジネス・ロジックに対してサービス中心のアプローチを使用しています。 ベスト・プラクティスに従い、ビジネス・オブジェクトの操作やビジネス・オブジェクト・データ上の問合せに対する設定管理のロジックを、ToyStoreServiceコンポーネントのサービス・メソッドに配置しました。 これによって、コントローラ層が非常にthinになり(小さくなり)、サービスの実装の詳細を極限までカプセル化しています。

他のアプローチとして、多くのデータ・モデルの操作コードをStrutsのアクション内に記述するという手法もありますが、このアプローチには、あまり賛成できません。それは、サービス中心のアプローチは、Web環境の外部でアプリケーションの機能が簡単にテストできるためです。 この理由から、できるだけサービス実装内で試行および実行することを目標として、このサンプル・アプリケーションは構成されています。 サービス中心のアプローチは、このようなカプセル化という明白な優位点以外にも、先に述べたバッチ・モードの将来的な改善という利益も最大限に教授できます。

ストアド・プロシージャへのアクセス例

ADF ToyStoreでストレス・テストを実行しているときに、同じ商品に対して、ほぼ同じ時刻に複数のWebユーザーが注文を完了した場合に、潜在的な問題があることに気が付きました。 問題は、ストレス・テストにおいて、負荷テスト・ツールによってランダムに発行された注文に、同じ商品IDが含まれている場合に発生しました。また、 ToyStoreServiceImplクラスの finalizeOrder()メソッドを使って、注文した商品の在庫数量を減らしたときも、「この在庫商品は別のユーザーにロックされています」というエラーを受け取ることがあります。

このため、買い物かごの中のすべての商品に対する在庫商品の行を「all or nothing」の方式で事前にロック するソリューションを実装するようにしました。その結果、複数のユーザーがまったく同時に同じ商品を注文するというケースでも、注文は競合しなくなりました。 このアプローチは、重複する商品が含まれていない場合でもすべての注文がシリアライズされるため、単にfinalizeOrder()メソッドをsynchronizedとしてマークするよりも、スケーラビリティが高いと感じています。

finalizeOrder()でコールされるヘルパー・メソッドは、 LockAllInventoryItemsHelperクラスのstaticメソッドで、 toystore.model.servicesパッケージの ToyStoreServiceImpl.javaファイルの中 にあります。 このメソッドは、具体的には次のようになっています。

  static void lockAllInventoryItemsInCart(ToyStoreServiceImpl am) {
    if (useViewObjectObjectForLockingAllItems(am)) {
      ensureLockAllInventoryItemsViewObjectExists(am);
      lockAllItemsInArrayUsingViewObject(am);
    }
    else {
      lockAllItemsInArrayUsingStoredProcedure(am);
    }
  }

このコードでは、学習目的で「all or nothingのロック」ソリューションを2つの方法で実装しています。 1つはストアド・プロシージャとして実装する方法で、もう1つは、アプリケーション・モジュール内のJavaコードで、ビュー・オブジェクトを使用して類似のタスクを実行する方法です。 useViewObjectObjectForLockingAllItems()メソッドの中で、 toystore.lockinventoryitems構成プロパティを読み込むために、ApplicationModuleのカスタマイズ実装クラス(ToyStoreApplicationModuleImpl)に追加した getConfigurationProperty()メソッドが使用されていることがわかります(この構成プロパティに対する有効値は、 ViewObjectStoredProcedure(デフォルト)です)。これらの2つのロックの方法では、ともに、最終的に承認しようとしている注文ですべての商品をロックできない場合に、数秒待ってからもう一度ロックを試行するように実装されています。

ストアド・プロシージャへのコールを行うメソッドは次のとおりです。

  private static void lockAllItemsInArrayUsingStoredProcedure(
                                     ToyStoreServiceImpl am) {
    PreparedStatement ps = null;
    try {
      ps = am.getDBTransaction().createPreparedStatement(STMT, 0);
      ps.setObject(1, arrayOfItemIdsInShoppingCart(am));
      ps.execute();
    }
    catch (SQLException s) {
      throw new AlreadyLockedException(s);
    }
    finally {
      if (ps != null) {
        try {
          ps.close();
        }
        catch (SQLException s) {}
      }
    }
  }

このメソッドは、アプリケーション・モジュールから取得したDBTransactionオブジェクトを使用して、JDBC PreparedStatementを作成し、 STMTというプライベート定数に格納されているPL/SQLコードのブロックを呼び出します。コールされるPL/SQLは、具体的には次のようなものです。

begin lock_all_items_ordered(?); end;

メソッドの最後には、確実にステートメントが閉じられています(ps.close)。 このメソッドで渡されるバインド変数は配列型であることは興味深い部分でしょう(この配列変数は、 arrayOfItemIdsInShoppingCart()メソッドによって取得されます)。 実行時には、この配列には、注文して買い物かごに入っているすべての商品のIDが含まれます。データベースの lock_all_items_orderedプロシージャでは、SQL文の中でこの配列値のバインド変数を使用して、行のすべてをロックする(または、処理に失敗した場合は何もロックしない)処理を1回の試行で実施します。

-- from lock_all_items_ordered
  CURSOR c(cp_items table_of_itemid) IS
     SELECT null
       FROM inventory
      WHERE itemid IN (
        SELECT *
          FROM TABLE(CAST(cp_items AS table_of_itemid))
      ) FOR UPDATE NOWAIT;

この配列パラメータは、table_of_itemid型になるよう定義されます。この型はOracleのオブジェクト型で、 TABLE OF VARCHAR(10)として定義されます。 TABLE()演算子と CAST(ArrayExpr AS ArrayType )構文を組み合せることによって、ネストされたSELECT文を使用して、配列要素の値を表として選択することができます。

注意

lock_all_items_orderedストアド・プロシージャは、 「サンプル・アプリケーションのインストールと設定」で説明した ToyStore.sqlスクリプトを実行すると作成されます。



実行時におけるビュー・オブジェクトの動的な構成

toystore.lockinventoryitemsというToyStoreServiceの構成プロパティの値を ViewObjectに設定しようとすると、「all or nothingのロック」ヘルパー・メソッドは、前述のストアド・プロシージャ・ベースのアプローチではなく、ビュー・オブジェクト・ベースの実装を使用します。 ユーザーは、 lockAllItemsInArrayUsingViewObject()メソッドのコードを調べて、どのように実装されているか確認することができます。

最初は、この実装で実行する必要があるSELECT FOR UPDATE NOWAIT問合せを使用したビュー・オブジェクトをビュー・オブジェクト設計ツールで作成しようとしましたが、FOR UPDATE NOWAITを指定したために問合せのSQL構文が設計時の検証で不適切であると認識されてしまう問題に直面しました。しかし、対応策はあります。 ビュー・オブジェクトを実行時に動的に作成できるのです。これを実現するために、(FwkExtensionsプロジェクトに含まれている) toystore.fwk.model.dataaccessパッケージの、 ViewDefHelperというヘルパー・メソッドを用意し、ビュー・オブジェクトの実行時の作成を容易に実施できるようにしています。

ApplicationModuleには、ビュー・オブジェクトの動的作成を支援する、便利なcreateViewObjectFromQueryStmt()メソッドがありますが、このメソッドは、与えられたSQL選択リストから各ビュー・オブジェクト属性の適切なデータ型を算出するために、データベースに対してラウンドトリップを強制的に実行します。このオーバーヘッドを回避するために、ensureLockAllInventoryItemsViewObjectExists()内で ViewDefImplによる新規ビュー・オブジェクト定義を動的に作成し、ViewDefHelperのいくつかのヘルパー・メソッドとViewDefImplのメソッドを使用して、ビュー・オブジェクトとその属性に関する定義情報を生成しています。作成されたビュー・オブジェクト定義を保持するオブジェクト(ViewDefImplのインスタンス)は、Java VMで実行中のすべてのコンポーネントで共有されるため、 synchronized修飾子を使用して、定義の作成と登録の際のマルチスレッドの問題を確実に回避しています。 一度ビュー・オブジェクト定義を解決および登録すると、これを使用して、この定義に基づいたビュー・オブジェクトのインスタンスを作成できます。 後続のコードでは、ビュー・オブジェクトのインスタンスを作成する前に、必ずインスタンスの検知作業をしているため、同じビュー・オブジェクト・インスタンスが何度も使用されることになります(実行のたびに、配列値のバインド変数のみが変わります)。


属性のデータ型によるデフォルトのフィールド・レンダリングの変更

前述の 「メタデータを使用した、より汎用的な手法によるデータ入力フォームの出力」の項で学習した、汎用的な formControl.jspページを開発した際に、問題に直面しました。 「Register New User」フォームに個人情報の詳細を入力する場合に、不正な電子メール・アドレスを入力するなどユーザーがミスをすると、ユーザーが修正しなければならない値が示されずに、不正な値が前の状態 (空白)に戻ります。 これは、 oracle.jdeveloper.htmlパッケージのデフォルトの HTMLFieldRendererImplクラスの問題で、フォーム・フィールドに表示するための属性値として、バインディング・オブジェクトから値を読み込むかわりに、ベースとなるモデル層の行から値を読み込んでいました。 ベースとなるモデル・オブジェクトに正しく設定できるまで、不正な値をキャッシュするのはバインディング・オブジェクトであるため、バインディング・オブジェクトの値を読み込むように手を加えると、期待どおりの動作にすることができます。

JDeveloperの今後のリリースでこの問題(Bug#3703925)が修正されるまでの回避策として、行の属性から直接値を読み込むのではなく、バインディングから値を読み込むカスタム・レンダラを作成することで、対処できます。このために作成した ControlBindingTextFieldRendererは、 例26のようなコードになります。 ここで、デフォルトのTextFieldレンダラを拡張し、 getHTMLValue()メソッドをオーバーライドして、データソースがBindingContainerベースである場合の処理方法を変更しています。 BindingContainerDataSourceを使用している場合は、出力されている現在のフィールドに対応するコントロール・バインディング(スーパークラスによってすでにセットアップされています)にアクセスし、 getInputValue()メソッドをコールすることによってバインディングの値を取得します。 BindingContainerを使用していない場合は、スーパークラスによって、そのまま処理されます。

例26: カスタムのTextFieldRenderer
package toystore.fwk.view;
// imports removed for brevity
public class ControlBindingTextFieldRenderer extends TextField  {
  protected String getHTMLValue(Row row) {
    if (getDatasource().isBindingContainerDataSource()) {
      BindingContainerDataSource ds = (BindingContainerDataSource)getDatasource();
      JUControlBinding b = ds.getControlBinding();
      if (b instanceof JUCtrlValueBinding) {
        Object value = ((JUCtrlValueBinding)b).getInputValue();
        return value != null ? value.toString() : null;
      }
    }
    return super.getHTMLValue(row);
  }   
}

<adf:inputrender>タグが使用する、フィールド・レンダラ・クラスのよりグローバルなオーバーライドを実行した別のテクニックを示すために、RegisterAction(新しいユーザーを登録するための /register DataPageに関連付けられているアクション)に対して、例27のようなヘルパー・メソッドを追加しました。

ADFでは、任意のJavaデータ型に対して、次のような名前のHTTPリクエスト属性を用意し、使用するデフォルトの編集用レンダラ・クラスの完全修飾名を指定することができます。

JavaTypeWithDotsConvertedToUnderscores_EditRenderer

したがって、 ControlBindingTextFieldRendererを、データ型 java.lang.Stringおよび toystore.model.datatypes.common.Emailの属性に対する編集用コントロールを出力するデフォルトの編集用レンダラ・クラスとして使用させるために、次の名前のHTTPリクエスト属性の値

ControlBindingTextFieldRendererクラスの完全修飾名に設定しました。

注意

<adf:render>タグを使用した通常の表示出力に対しても、同様の設定をすることもできます。この場合は、 _EditRendererのかわりに _Rendererの接尾辞を使用します。


例27: 型によるデフォルトの編集用レンダラのオーバーライド
//From: toystore.controller.strutsactions.RegisterAction
  private void setupDefaultFieldRenderers(DataActionContext ctx) {
    HttpServletRequest request = ctx.getHttpServletRequest();
    request.setAttribute(STRING,TEXTFIELD);
    request.setAttribute(EMAIL,TEXTFIELD);    
  }  
  private static final String STRING = "java_lang_String_EditRenderer";
  private static final String EMAIL = "toystore_model_datatypes_common_Email_EditRenderer";
  private static final String TEXTFIELD = "toystore.fwk.view.ControlBindingTextFieldRenderer";

RegisterActionでオーバーライドされた handleLifecycle()メソッドから、次のようにしてヘルパー・メソッドをコールしています。

// From: toystore.controller.strutsactions.RegisterAction
  protected void handleLifecycle(DataActionContext ctx) throws Exception {
    setupDefaultFieldRenderers(ctx);
    super.handleLifecycle(ctx);
  }

handleLifecycle()メソッドは、DataAction の組込みのライフサイクルを処理するメソッドで、これをオーバーライドすることにより、ライフサイクルが始まる前、またはそれが終了した後に実行したいカスタム・コードを記述できます。この対処策の根本原因であるレンダラの問題が修正されれば、このコードはコメントアウトしてかまいません。このように、十分に設計されたフレームワークを使用している場合は、発生した問題に対処することは新しい動作を追加するのと同じくらい簡単で、ともに、フレームワークのベース・クラスをカスタマイズすること、およびデフォルトのフレームワークの実装クラスのかわりに使用されるカスタマイズ・クラスを設定することで対応できます。

買い物かごの商品情報を調べるためのその他のアプローチ

ShoppingCartImplビュー・オブジェクト実装クラスのfillInCartItemDetails()メソッドは、商品情報を調べる処理を行いますが、ここには、ADF Business Components を使用した2つのアプローチが用意されています。 前述の例のように、実行時に使用される処理は、構成プロパティ(この場合は toystore.shoppingcartlookup)の値によって異なります。 このパラメータの値が ViewObjectの場合は、 ShoppingCartItemLookupビュー・オブジェクトのインスタンスを使用するアプローチが使用されます。 また、パラメータの値が EntityObjectの場合は、主キーによってエンティティ・キャッシュから在庫商品のエンティティ・オブジェクト・インスタンスを検索するアプローチが使用されます。

1文字のフラグ・フィールドをデコードするための別のアプローチ

データには、1文字の「フラグ」フィールドが含まれていることが多くあります。 ADF ToyStoreサンプル・アプリケーションでは、これに該当するものとして「InStock」フィールドがあります。これは、商品の在庫があるかどうかを表すフィールドです。 ( toystore.model.dataaccessパッケージの)ItemsForSaleビュー・オブジェクトでは、エキスパート・モードの問合せで DECODE()文を使用して、在庫の現在数量を表すINVENTORY.QTYの値によって、「Back Ordered」や「In Stock」などの文字列を返すのがわかります。

SELECT Item.ITEMID, 
       ITEM.ATTR1||' '||PRODUCT.NAME AS NAME, 
       Item.LISTPRICE, 
       Item.PRODUCTID, 
       DECODE(INVENTORY.QTY,NULL,'Back Ordered',
                               0,'Back Ordered',
                               'In Stock')
       AS IN_STOCK,
       /* etc */

また、 ShoppingCartビュー・オブジェクトには、 InStockという一時属性が含まれています。これは、商品の在庫があるかどうかを表す、YまたはNのどちらかの値をとります。 この値は、前の項で説明したfillInCartItemDetails()メソッドによって決定されます。 yourcart.jspページで InStockの情報を表示する場合、そのままのYまたはNの値が表示されるのではなく、Strutsメッセージ・リソース・ファイルに用意された翻訳文字列に対するキー名の一部としてYまたはNが使用されます。 最初に、JSTL <c:set>タグを使用して、 inStockMsgKeyという名前のページのローカル変数を使って、<c:forEach>ループにおける現在の RowInStockフィールドの値と"cart.instock."の値とを連結した値に設定し、次に、 <bean:message>を使用して、 inStockMsgKey の値 (cart.instock.Yまたはcart.instock.N)が表すキー値に対応する翻訳文字列が示されます。

<td>
  <%--
   | NOTE: Here we are using the value of the "InStock"
   | ----  attribute, which will be "Y" or "N", as part
   |       of the key to lookup a translated value to 
   |       show to the user like "In Stock" or "Back Ordered"
   +--%>
  <c:set var="inStockMsgKey" value="cart.instock.${Row.InStock}"/>
  <bean:message name="inStockMsgKey"/>
</td>

発生したエラーの確実な表示

すべてのWebページにはそれぞれ、最上部に <html:errors>タグがあることに気が付いたでしょうか? デフォルトでは、ADF DataActionは、リクエスト処理のライフサイクルの際に発生するすべての例外をまとめて管理し、リクエストの最後(ライフサイクルの reportErrors()フェーズ)に、StrutsのActionErrorオブジェクトに変換してStruts層に渡します。 これにより、ライフサイクル処理で発生した検証エラーが、ページ上の、 <html:errors>タグを配置した場所に表示されることになります。ページに <html:errors>タグがない場合、Struts層にはエラーがレポートされていても表示はされません。 JDeveloperのデータ・コントロール・パレットを使用してページを作成すると、必ずページに <html:errors>タグも追加されるようになっていますが、手動による方法でページを開発した場合には、タグを追加することを心がけておくべきでしょう。


前に戻る | 目次に戻る | 次へ進む