コメント行: Scott Johnson: Dojo Dijit ツリー・ウィジェットを遅延ロードすることによりパフォーマンスを改善する

ツリー・ウィジェットのノードへのデータのロードをすべて最初から行うのではなく、遅延させることによって、ツリーを高速に描画することができ、またツリーのパフォーマンスを高めることができます。この記事では実際に近い例をとおして、Dojo の Dijit ツリー・ウィジェットにロードする JSON データを遅延ロードするために REST 呼び出しを使う方法について説明します。

Scott Johnson (scottjoh@us.ibm.com), Software Engineer, WebSphere Business Monitor, EMC

Author photoScott Johnson はこの 25 年間、ソフトウェア開発者として活躍しています。IBM に入社前には、銀行や資金管理、調査研究、そして医療業界のスケジューリングや人員管理などの領域で業務を行っていました。彼は 2000 年に WebSphere Application Server の JSP コンポーネントのリーダーとして IBM に入社しました。彼は現在、WebSphere Business Monitor のためのダッシュボードを開発するために Dojo を使っています。



2008年 3月 21日

遅延させることによる高速化

Dojo ツリー用のデータを取得するためにブラウザー・クライアントから REST 呼び出しを使用する方法はパフォーマンスの点で難がある可能性があります。ツリー・ウィジェットのノードへのデータのロードを遅延させると、最初からツリーの全ノードにデータをロードする場合よりもツリーのパフォーマンスを高めることができ、またツリーの描画も高速になります。つまり展開されたノードのみが、そのノードにデータをロードするために REST 呼び出しを要求するのです。

この記事で紹介する Dojo データ・ストアのサンプルでは、REST 呼び出しを使うことで、Dojo Dijit ツリー・ウィジェットにロードする JSON データの遅延ロードを行います。このサンプルでは REST のレスポンスに使用する JSON データの構造として、Dojo データ・ストアに関する他のサンプルに見られるものよりも複雑な構造を使用しています。REST のレスポンスはツリーのノード構造に直接対応しておらず、その意味で他のサンプルよりも「実際に近い」形になっています。またこのサンプルは Dojo 1.0 でも Dojo 1.1 でも同じように適切に動作します。ここでは、AccountTreeWidget.js の _createStoreAndTree() 関数を見て行き、このサンプルがどのように実現されているかを調べます。

サンプルに対して作業を行う

このツリーを図 1 と図 2 に示します。

図 1. ツリーの最上位レベルにある 2 つのノード
ツリーの最上位レベルにある 2 つのノード
図 2. 一部のノードを展開したツリー
一部のノードを展開したツリー

ファイルとデータ

このサンプルを構成するファイルは次のとおりです。

  • AccountTreeDemo.html: Dijit ツリーをロードする対象となるノード (_treeWidgetAttach) を含み、AccountTreeWidget を実行します。このファイルをブラウザーの中で開くと、このデモを見ることができます。
  • AccountTreeWidget.js: AccountTreeStore を Dijit ツリーに接続するための役割を果たします。AccountTreeStore の初期データ構造は、AccountTreeStoreUtil が繰り返し REST 呼び出しを行うことで作成されます。AccountTreeStoreUtil がこの JSON データ構造の作成を完了すると、AccountTreeWidget は初めてデータ・ストアとツリーを作成します。
  • AccountTreeStoreUtil.js: 最初のデータ・ストアの構造を作成するために繰り返し REST 呼び出しを行います。
  • AccountTreeStore.js: カスタムのデータ・ストアです。このデータ・ストアはツリーにデータを渡し、ノードが展開されるにつれ、それらのノードにデータを遅延ロードします。
  • Accounts/Accounts.json: このサンプルのインストール先ファイルの直下に来るディレクトリーです。Accounts.json は Accounts とネストされた子 Accounts とを記述する JSON 構造を含みます。
  • Transactions/*.json: Accounts と同じレベルにあるディレクトリーです。このディレクトリーは、transaction の配列を表現した JSON 構造を含むファイルを複数含んでいます。

これらのファイルとディレクトリーを (Dijit、Dojo、DojoX と同じレベルにある)「ibmtreedemo」というディレクトリーにインストールします。言い換えると、このデモが Dojo のディレクトリー構造の中で Dojo の他のディレクトリー群と同列であるように、このデモを配置します。

サンプル用のデータは、US National Bank という架空の銀行の account (口座) と transaction (取引) で構成されています。ツリーにデータをロードするためには、明確に異なる 2 つの JSON 構造 (account と transaction) を解析し、組み合わせる必要があります。

  • account データ: account データは 1 回の REST 呼び出しによって取得され、この REST 呼び出しによって複雑な JSON 構造が返されます (リスト 1)。account データの構成は、最上位レベルの account の下に深くネストされた子 account を持つことのできる構成になっています。このサンプル・ファイルの最初の半分は Joe Smith の account で構成され、残りの半分は Mary Bradley の account で構成されています。
    リスト 1. account データ
    {bankId: "NationalBank", bankName:"NC National Bank", location: "US_NC", 
    "accountArray": [
        {accountName: "Joe Smith's US National Bank Account", 
            accountId: "JoeSmith_US_NC", dataType: "Account",
            childAccounts: 
                [{accountName: "Joe Smith's Bahamas Account", 
                  accountId: "JoeSmith_BS",  dataType: "Account",
                  childAccounts: 
                      [{accountName: "Joe Smith's Turks and Caicos Account", 
                        accountId: "JoeSmith_TC",  dataType: "Account", 
                        childAccounts: []}]},
                 {accountName: "Joe Smith's Swiss Account", 
                  accountId: "JoeSmith_SZ",  dataType: "Account",
                  childAccounts: []}]},
        {accountName: "Mary Bradley's US National Bank Account", 
            accountId: "MaryBradley_US_NC", dataType: "Account",
            childAccounts: 
                [{accountName: "Mary Bradley's Bahamas Account", 
                  accountId: "MaryBradley_BS",  dataType: "Account",
                  childAccounts: 
                      [{accountName: "Mary Bradley's Turks and Caicos Account", 
                        accountId: "MaryBradley_TC",  dataType: "Account", 
                        childAccounts: []}]}]}
    ]}
  • transaction データ: それぞれの account の transaction データは、口座名義人と口座 ID を示す名前を持つファイルの中にあります。リスト 2 は、Transactions/JoeSmith_SZ.json というファイルの中の、Joe Smith の Swiss (スイス) の account の transaction を簡略化して表示したものです。これらの transaction は transactionArray 配列の中にあります。
    リスト 2. transaction データ
    {bankId:"NationalBank",location:"US_NC",accountId:"JoeSmith_SZ",
    transactionArray:[
    {transactionAmount:"4050.00",  
       dataType: "Transaction", 
       currency:"USD",
       transactionDate:"2006/10/24"},
    {transactionAmount:"2050.00",  
       dataType: "Transaction", 
       currency:"EUR",
       transactionDate:"2006/10/25"},
    {transactionAmount:"404.00",  
       dataType: "Transaction", 
       currency:"GBP",
       transactionDate:"2006/10/26"},
    {transactionAmount:"98300.00",  
       dataType: "Transaction", 
       currency:"EUR",
       transactionDate:"2006/10/27"}
    ]}

データ・ストアのための初期データ構造

このデータ・ストアは AccountTreeStore と呼ばれ、dojo.data.ItemFileReadStore を展開したカスタムのデータ・ストアです。

ItemFileReadStore は、url パラメーターまたは data パラメーターのいずれかを使って作成されます。このサンプルでは初期データ構造を複数の URL から作成する必要があるため url パラメーターを使うことはできず、またツリーにデータをロードする前にそれらのデータを解析する必要があります。従って、データ・ストアを作成するために data パラメーターが使われています。

このデータ・ストアの初期データ構造は AccountTreeStoreUtil というウィジェットによって動的に作成されます。このデータ構造を簡略化して示したものがリスト 3 です。

リスト 3. データ・ストアのための初期データ構造
{
identifier: 'name',  
label: 'label' 
items: [
{ name:"Joe Smith's US National Bank Account", 
type:'topLevelAccount',
  accountId: 'JoeSmith_US_NC', 
  label:"Joe Smith's US National Bank Account", 
      children:[
{_reference:"Joe Smith's Bahamas Account",
            accountId:"JoeSmith_BS", 
            label:"Joe Smith's Bahamas Account", 
            parent: "JoeSmith_US_NC", type: "childAccount"}, 
{_reference:"Joe Smith's Swiss Account",
            accountId:"JoeSmith_SZ", 
            label:"Joe Smith's Swiss Account", 
            parent: "JoeSmith_US_NC", type: "childAccount"},
{_reference:"JoeSmith_US_NC1",
            parent: "JoeSmith_US_NC", 
            type: "transaction"},
{_reference:"JoeSmith_US_NC2",
            parent: "JoeSmith_US_NC", 
            type: "transaction"}]},
{ type: 'stub', 
        name: "Joe Smith's Bahamas Account",
        accountId:"JoeSmith_BS", 
        dataType:"Account", 
        label: "Joe Smith's Bahamas Account", 
        parent:"JoeSmith_US_NC"},
{ type: 'stub', 
        name: "Joe Smith's Swiss Account",
        accountId:"JoeSmith_SZ", 
        dataType:"Account", 
        label: "Joe Smith's Swiss Account", 
        parent:"JoeSmith_US_NC"},
{ type: 'stub', 
        name:"JoeSmith_US_NC1",
        label:"2006/10/24 USD 4050.00", 
        dataType: "Transaction", 
        parent:"JoeSmith_US_NC", 
        transactionAmount: "4050.00", 
        currency: "USD", 
        transactionDate:"2006/10/24"},
{ type: 'stub', 
        name:"JoeSmith_US_NC2",
        label:"2006/10/25 EUR 2050.00", 
        dataType: "Transaction", 
        parent:"JoeSmith_US_NC", 
        transactionAmount: "2050.00", 
        currency: "EUR", 
        transactionDate:"2006/10/25"},
// end of top-level item  -   name:"Joe Smith's US National Bank Account
    
//  Mary Bradley's account data would follow the same pattern as Joe Smith's.

  { name:"Mary Bradley's US National Bank Account", 
type:'topLevelAccount', dataType: 'Account',........................}
    // end of top-level item  -   name:"Mary Bradley's US National Bank Account
]};

この構造はメモリーの中でのみ存在し、ディスク上に永続化されることはありません。

このサンプルで遅延ロードを実現するための鍵は、上記の構造で太字になっている要素です。これらの要素を説明しましょう。

  • type:'topLevelAccount'
    'topLevelAccount' は、AccountTreeWidget.js: query:{type:'topLevelAccount'} の中で Dijit Tree を作成する際に使われるクエリーと突き合わせを行います。この突き合わせによって、topLevelAccount 型の項目がツリーの最上位レベルのノードであることをツリーに対して指示します。他の型はすべてサブノードです。図 1 を見ると Joe と Mary の US National の account のみが最上位ノードであることがわかります。これは、データ・ストアに対する初期構造の中で、これらの account のみが type:'topLevelAccount' と判断されるためです (リスト 3)。

  • _reference:
    最上位レベルの account の子配列の中で、それぞれの子は _referenceと判断されます。_reference の値 (例えば "Joe Smith's Bahamas Account" (Joe Smith のバハマの口座) など) は、スタブ構造の中の name 要素と突き合わせが行われます。

  • type: 'stub'
    name: 'somevalue'

    最上位レベルの account の _reference は type: 'stub' として認識される構造の中で定義されます。stub 構造の中の name の値は、子配列の中の 1 つの _reference の値と突き合わせが行われます。例えば Joe Smith の account の子配列の中の _reference:"JoeSmith_US_NC1" は、stub 構造のうちの 1 つである name:"JoeSmith_US_NC1" と突き合わせが行われます。

この初期構造を AccountTreeStoreUtil が作成すると、データ・ストアとツリーを作成することができます。AccountTreeWidget がリスト 4 のようにデータ・ストアとツリーを作成します。

リスト 4. データ・ストアとツリーを作成する
_createStoreAndTree: function(data){
    if (dojo.version.major == 1 && dojo.version.minor == 0) {
        this.store = new ibmtreedemo.AccountTreeStore({
            data: data["treeStructure"], 
            cachedTransactionObjects: data["cachedTransactionObjects"],
            cachedChildAccounts: data["cachedChildAccounts"]});
        this.tree = new dijit.Tree({store: this.store, 
            label: &Accounts and Transactions for US National Bank&, 
            labelAttr: &name&, 
            typeAttr: &type&, 
            id:&accounttree&, 
            query:{type:&topLevelAccount&}});
        
        this.treeAttachPoint.appendChild(this.tree.domNode);
        this.tree.startup();
    } else if (dojo.version.major == 1 && dojo.version.minor >= 1) {
        this.store = new ibmtreedemo.AccountTreeStore({
            data: data["treeStructure"], cachedTransactionObjects: 
            data["cachedTransactionObjects"],
            cachedChildAccounts: data["cachedChildAccounts"]});
        var myModel = new dijit.tree.ForestStoreModel({
            store: this.store,
            query:{type:&topLevelAccount&},
            rootId: "accts",
            rootLabel: &Accounts and Transactions for US National Bank&});                
        this.tree = new dijit.Tree({model: myModel});
        this.treeAttachPoint.appendChild(this.tree.domNode);
        this.tree.startup();
    } else {
        alert("Unsupported dojo version.  dojo 1.0 or greater required");
    }   
}

このデータ・ストアに対する data パラメーターは data["treeStructure"] です。これは上で説明した初期データ構造であり、AccountTreeStoreUtil.js によって作成されたものです。

初期データと共にデータ・ストアがロードされ、上記の呼び出しが実行されると、ツリーが描画されます。(初期データは、最上位レベルのすべての account と、それらの account の transaction を含んでいます。データ・ストアは、常にツリーよりも 1 ステップ先行しています。) あるツリー・ノードが展開された場合、データ・ストアは子ノードを調べ、それから初めて、それらのノードの transaction をロードします。

ツリーの中に子ノードを遅延ロードする

ノードが展開されると、Tree ウィジェットがデータ・ストアの isItemLoaded() 関数を呼び出します。例えばツリーが最初に表示された時に最上位レベルの 2 つのノード (Joe と Mary の最上位レベルの銀行口座) が展開されていない場合には、Joe の最上位レベルの account の展開アイコン (+) をクリックすると、データ・ストアの最初のデータ構造の中の stub を解決するために、データ・ストアの isItemLoaded() がツリーによって呼び出されます。これらの stub は先ほど説明したものです。

データ・ストアがデータの取得に関してツリーよりも常に 1 ステップ先行していることを思い出してください。データ・ストアには account に関するキャッシュが渡されるため、データ・ストアはその account の REST クエリーを再度行う必要がありません。もっと重要な点として、データ・ストアには最上位レベルの account に対する transaction のキャッシュも渡されます。これは効率的です。なぜならツリーの初期データ構造は、(transaction を含む) 最上位レベルの account のすべての子に対する stub を含む必要があるからです。従って AccountTreeStoreUtil は、Joe と Mary の両方の最上位レベル account に対する transaction を取得するためのクエリーを行う必要があります。そしてこれらの account オブジェクトは、データ・ストアが作成される際にキャッシュの中でデータ・ストアに渡されます。これはつまり、Joe の US National Bank の account ノードが展開されると、この account の transaction をキャッシュから取得できるということです。そして、(バハマのノードはまだ展開されていないにもかかわらず) キャッシュには Joe のバハマの account の transaction が追加されます。バハマのノードが展開されると、バハマの account の transaction がキャッシュから取得され、そしてキャッシュには Joe の Turks and Caicos Islands (タークス・カイコス諸島) の account の transaction が追加されます。このパターンは、account が子 account を持たなくなるまで、従って transaction もなくなるまで繰り返されます。

この isItemLoaded() は、次に示すような簡単な関数です。

リスト 5. isItemLoaded() 関数
isItemLoaded: function(item) {
    //  summary:
    //      Overload of the isItemLoaded function to
	 //      look for items of type 'stub', which indicate
    //      the data hasn't been loaded in yet.
    //
    //  item:
    //      The item to examine.
    // For this store, if it has the value of 'stub' for its type attribute,
    // then the item hasn't been fully loaded yet.  It's just a placeholder.
    if (this.getValue(item, "type") == "stub") {
        return false;
    }
    return true;
},

Joe の最上位レベルの account ノードを展開する際に最初に解決する必要のある項目は、Joe のバハマの account です。バハマの account は Joe の最上位レベルの account の子として定義されており、また初期データ構造の中で stub 型を持っています。バハマの account は stub 型なので、isItemLoaded() は false を返します。

ツリーは Joe の最上位レベル account の中にある他の stub に対して isItemLoaded() を呼び出します。これらの stub は、Joe のスイスの account であり、また最上位レベルの account に対する transaction です。これらの stub はすべて、データ・ストアの初期データ構造の中にある、Joe のアメリカの銀行口座に対する子配列の中にリストされています。しかしこれらはどれも、最初の子であるバハマの account が解決されるまで解決されることはありません。

account の stub を処理する

バハマの account の stub を解決するために、次にデータ・ストアの loadItem() 関数がツリーによって呼び出されます。loadItem() は、今度は以下の処理を行います。

  1. データ・ストアの loadItem() 関数は、解決が必要な項目が transaction ではなく account であると判断し、データ・ストアの addAccountNodes() 関数を呼び出します。
  2. addAccountNodes() は次の 3 つの処理を行います。
    1. Joe のバハマの account を表す新しいノードをメモリーの中に作成します。このノードの型は stub です。このノードの完全な構造をリスト 6 に示します。
    2. 新しいノードの中に、バハマの account の子に対する配列を作成します。これらの子は、バハマの account の直接の子 account と、バハマの account に対する transaction です。この配列に追加される最初の子は直接の子 account であり、データ・ストアが作成されたときにデータ・ストアに渡されるキャッシュの中で、これらの account が検索されます。(account 群に対する REST クエリーは 1 回のクエリーだったので、その結果をキャッシュした方が効率的です。)
    3. REST クエリーを実行してバハマの account の transaction を取得する getTransactionsForAccount() 関数を呼び出します (データ・ストアがツリーよりも必ずワンステップ先行することを思い出してください)。getTransactionsForAccount() はバハマの新しい account ノードの子配列に transaction を追加し、またこれらの transaction を transaction のキャッシュに追加します (バハマのノードが展開されると、これらの transaction をキャッシュの中で取得することができます)。

データ・ストアの getTransactionsForAccount() 関数は、バハマの account に対する transaction の取得を完了するとデータ・ストアの gotData() 関数を呼び出し、バハマの新しい account のノードを渡します。メモリーの中では、この構造は下記のリスト 6 のようなものです。(スペースの関係から、すべての transaction のうちの 1 つの例のみを示してあります。)

リスト 6. account ノードに対する stub の構造 (account ノードの transaction と子 account を含んでいます)
{
   "name":["Joe Smith's Bahamas Account"],
   "label":["Joe Smith's Bahamas Account"],
   "type":["stub"],
   "accountId":["JoeSmith_BS"],
   "dataType":["Account"],
   "parent":["JoeSmith_US_NC"],
   "children":[
      {
         "name":"Joe Smith's Turks and Caicos Account",
         "label":"Joe Smith's Turks and Caicos Account",
         "stub":"Joe Smith's Turks and Caicos Account",
         "type":"childAccount",
         "dataType":"Account",
         "accountId":"JoeSmith_TC",
         "parent":["JoeSmith_BS"]
      },
      {
         "stub":"JoeSmith_BS1",
         "type":"transaction",
         "parent":"JoeSmith_BS",
         "dataType":"Transaction"
      },
      {
         "stub":"JoeSmith_BS2",
         "type":"transaction",
         "parent":"JoeSmith_BS",
         "dataType":"Transaction"
      }]}

注意する点として、account は stub 型を持ちますが、子 account は stub という要素を持ち、子 account の型は「childAccount」または「transaction」のいずれかです。これは重要です。なぜなら gotData() は stub と呼ばれる要素を探し、その構造をスタブ要素に変更し、後で解決に使用するからです。

transaction オブジェクトを処理する

データ・ストアの loadItem() 関数は、ある項目が transaction であると検出すると、単純にキャッシュの中で transaction オブジェクトを検索し、そのオブジェクトを gotData() に渡します。この構造はキャッシュされていたので、stub ではありません。この構造は次のようなものです。

リスト 7. account ノードを完全に解決する構造
{
   "name":"JoeSmith_US_NC6",
   "label":"2006/10/29 GBP 100000.00",
   "parent":"JoeSmith_US_NC",
   "dataType":"Transaction",
   "currency":"GBP",
   "transactionAmount":"100000.00",
   "transactionDate":"2006/10/29",
   "type":"transaction"
}

まとめ

この記事で説明した方法を使って子 account と transaction のデータの遅延ロードを実現するために重要なことは、ツリーにデータをロードするために使われる JSON データ構造をメモリー内に適切に作成することです。JavaScript コードのファイルをよく調べ、このサンプルがそうした目標を実現するためにどのようなことをしているのか理解してください。このサンプルの中心になっているのは AccountTreeStore.js です。

データ・ストアの初期データ構造は REST 呼び出しを繰り返し行うことで動的に作成されますが、この構造は最上位レベルの account の stub を表現しています。この初期データ構造は子 account と transaction に対して type: 'stub' という表記を使っています。ツリーのノードが展開される際の実行時に作成される JSON 構造は、別の方法で stub を表現します。これらの構造では "stub":"Joe Smith's Turks and Caicos Account" という表記方法が使われます。"stub": "値" という表記はデータ・ストアの gotData() 関数に対するフラグになっています。gotData() 関数はこの表記方法を使用する項目を認識し、それらの項目を type: 'stub' という表記の項目に変換し、これらの新しいスタブ項目をツリーの中にロードするために onItem() を呼び出します。


ダウンロード

内容ファイル名サイズ
Code samplelazyloadtreedemo.zip14 KB

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=WebSphere, Web development
ArticleID=320238
ArticleTitle=コメント行: Scott Johnson: Dojo Dijit ツリー・ウィジェットを遅延ロードすることによりパフォーマンスを改善する
publish-date=03212008