Ext JS 5 のデータパッケージ詳説
こんにちは、ゼノフィnakamuraです。
はじめに
Ext JS 5 の公開で、元々 Ext JS 4 や Sencha Touch 2 で導入されていた Sencha データパッケージの機能を拡大しました。データパッケージは、アプリケーション開発の要であり、MVC と MVVM の世界でよく軽視される “M” (モデルのM) です。この新しい機能は、データモデルの宣言、データの表示とユーザー入力のバリデーション、最後にサーバーへの保存など、アプリケーション開発の過程全体で現れます。
この記事で、各機能について説明します。では始めましょう。
詳しく学びたい人は、ウェブセミナーに参加して下さい:Deep Dive into Ext JS 5 Data 7月17日、木曜日、10:00 PDT
新しい宣言的な機能
通常のアプリケーションでは、データモデルというものにたくさんのクラスが含まれています。Ext JS 5 の場合、このモデルクラスに必要となっている繰返しでてくる定型的なコードの量を削減しました。多くの改善は、 What’s New In Ext JS 5 で説明されていますので、ここでは余り深くは書きません。
フィールド
矛盾しているように感じられるかもしれませんが、Ext JS 5 では、モデルのフィールドを宣言する必要はありません。この利点は、レコード生成の前に特別な処理が必要ない限り、データをより簡単に拡張できます。単に宣言するものが減ったので、より簡単になったという事だけではなく、より速くもなりました。この最適化により、必要がない限り、各フィールドごとにデータ処理されないからです。
フィールド宣言を省略する方法は時に便利で、各フィールドごとの処理を省く簡単な方法ですが、これはいつも可能とは限りません。
例えば、
defaultValue
や
convert
コンフィグは、宣言されたフィールドでの処理が必要となります。
フィールドに
validators
や
reference
を追加する場合は
デフォルトのフィールドタイプ “auto” を利用すれば、この処理を避ける(そしてさきほどのコンフィグを避ける)ことができます。
Custom Field Types and Validators カスタムフィールドタイプと検証コントロール
フィールドを宣言するほうが都合が良いことの一つは、モデルにおける最も重複の多いコード (バリデーション) です。
Ext JS 5 以前では、バリデーションロジックの宣言は、Ext.data.Model
から派生したクラスが行う操作でした。これらのバリデーションは、よく似たフィールドを扱っているクラスから再利用できるものではありませんでした。同時に、このバリデーションは、フィールドの内容の種類に関連していました。例えば、メールアドレス、電話番号、性別、生年月日など。
カスタムフィールドタイプの導入で、このロジックを複数のモデルから再利用できます。例えば、「性別」のフィールドタイプを次のように宣言します。
Ext.define('App.fields.Gender', { extend: 'Ext.data.field.String', alias: 'data.field.gender', validators: { type: 'inclusion', list: [ 'female', 'male' ] } }); |
そして、いくつかのモデルで利用します。
Ext.define('App.model.Person', { extend: 'Ext.data.Model', fields: [{ name: 'name', type: 'string' },{ name: 'gender', type: 'gender' }] }); |
上のサンプルを利用した Sencha Fiddle の実験をご覧ください。実際のアプリケーション上で、フィールドタイプとその Validators を再利用する機会は多いことでしょう。
参照フィールドとアソシエーション
関連付けを宣言する事に対して、またExt JS 5で、定型的なコードの必要条件を削減しました。
アソシエーションの定義は、Ext JS 5 によって定型的なコードの必要性を減少させたもう一つの部分です。
以前のリリースでの、hasMany
, hasOne
, belongsTo
コンフィグは、アソシエーションの「両側」に対称な宣言を手動で管理する必要がありました。これが変更されました。関連しているクラスのどちらかで (通常は “Many” 側で) アソシエーションを宣言できます。
例えば、一つのモデルに hasMany
使うと、対向の belongsTo
は必要ありません。次のこの宣言のセットをご覧ください。
Ext.define('App.model.Base', { extend: 'Ext.data.Model', schema: { namespace: 'App.model' } }); Ext.define('App.model.Person', { extend: 'App.model.Base', fields: [ 'name' ], hasMany: 'Order' // the "entityName" for "App.model.Order" (see below) }); Ext.define('App.model.Order', { extend: 'App.model.Base', fields: [ 'name' ], hasMany: 'OrderItem' }); Ext.define('App.model.OrderItem', { extend: 'App.model.Base', fields: [ 'name' ] }); |
上記のサンプルでは、Ext JS 5 に導入された新しい機能を二つ利用しています。
- 一つの関連しているモデルの一方だけにアソシエーションが宣言されます。
- (Schema の namespace をセットすることにより) 自動的に生成される entityName 値。 これにより、アプリケーションで推奨されている名前空間の構造に従いつつ、モデルやそのモデルの生成された getter / setter メソッドに分かりやすい名前をつけられます。Schema も Ext JS 5 で新しく導入されました。いくつかの便利なオプション (例えばその proxy config ) もあります。 ただ、通常はSchema と直接やりとりする必要はないため、ここでは説明しません。
しかし、上記の例は、現実のアプリケーションからかけ離れて単純化しすぎています。
実際のアプリケーションでは、このレコードは適切なフィールドに ID 値を保持することによってお互いにリンクしています。
例えば、OrderItem には orderId
フィールドがあって、Order には personId
フィールドがあります。「外部キー」と呼ばれる、これらのフィールドの整合性を保つことはとても重要です。
このようなフィールドがある場合、Model の種類に特定のフィールド参照を示すために、新しい
reference
コンフィグが利用できます。上記のサンプルを再び整理すると。
Ext.define('App.model.Person', { extend: 'App.model.Base', fields: [ 'name' ] }); Ext.define('App.model.Order', { extend: 'App.model.Base', fields: [{ name: 'name', type: 'string' },{ name: 'personId', reference: 'Person' }] }); Ext.define('App.model.OrderItem', { extend: 'App.model.Base', fields: [{ name: 'name', type: 'string' },{ name: 'orderId', reference: 'Order' }] }); |
ここに表示されている新しい
reference
コンフィグによって、
hasMany
のサンプルと同じアソシエーションが生成されます。
さらに、Ext JS 5 はこのフィールドを管理します。
例えば、Order の orderItems
アソシエーションに OrderItem を追加すると、orderId
フィールドを自動的にセットします。
Many-to-Many アソシエーション
参照フィールドを使ってアソシエーションを効率化するのに加え、Ext JS 5 では、新しいアソシエーション (many-to-many) のサポートも追加しました。よくあるシナリオでいいますとユーザーとグループのモデリングです。ユーザーは複数のグループのメンバーとなれますし、グループにも複数のメンバーがいる事ができます。次の簡単な宣言を見てみましょう。
Ext.define('App.model.User', { extend: 'App.model.Base', fields: [ 'name' ], manyToMany: 'Group' }); Ext.define('App.model.Group', { extend: 'App.model.Base', fields: [ 'name' ] }); |
ここでも片方だけのモデルで
(manyToMany
コンフィグを使って)
アソシエーションを定義しています。
これは、hasMany
と同様に各クラスでメソッドを生成します。
上記の場合には、User クラスには groups
メソッドができ、Group クラスには users
メソッドができます。
hasMany
と同じく、これらは関連したレコードを保持するストアを返します。しかし、内部的には、この種類の関連付けを保つためには、慎重な管理が必要です。
モデルがお互いにキーを格納する場合は、その関係のメンテナンスは解り易いです。しかし、多対多のアソシエーションの場合には、その関係を関連しているレコードのフィールドとして表すことはできません。また、User の groups
ストアに Group を追加すると、その Group の users
(が存在するなら) にその User を追加する必要があります。
この動作を確認するために、このサンプルをご覧ください。 このサンプルには、一つの非常に新しい違いがあります。
var session = new Ext.data.Session(); |
後で詳しく説明しますが、この時点で説明すると、このセッションインスタンスが多対多のアソシエーションに必要な追加のメンテナンスを行っています。後ほどセッションを利用して、これらのアソシエーションでの保留中の編集を見る方法を説明します。
ピースをつなげて
我々のデータモデルを宣言しましたので、アプリケーション内での新しい使用方法を説明します。Ext JS 5 のデータバインディングを利用し、より簡単に我々のデータをユーザーに表示できますが、ユーザーがそのデータの編集すると、色々楽しい事が起こります。
フォームバリデーション
ViewModels
の導入により Ext JS 5は、フォームフィールドの値とその元になるデータストレージとの接続を知る事ができます。フォームフィールドのデータバインディングは、次の論理的な段階に進み、新しいコンフィグ modelValidation
をセットするとモデルフィールドからの validators とも接続します。
このサンプルをご覧ください。
Ext.create({ xtype: 'window', title: 'Validation', width: 350, layout: 'form', autoShow: true, viewModel: { data: { person: new App.model.Person({ name: 'Bob', gender: 'mal' // typo }) } }, defaultType: 'textfield', modelValidation: true, items: [{ fieldLabel: 'Name', bind: '{person.name}' },{ fieldLabel: 'Gender', bind: '{person.gender}' }] }); |
上記のフォームは、ViewModel の Person レコードを利用した、基本的なデータバインディングフォームです。しかし、modelValidation: true
を追加すると、子供のフォームフィールドは、value
バインディングによりバリデーション情報に自動的に連携します。この場合、ViewModel の person
レコードの gender
フィールドは不正ということになり、それは Gender
テキストフィールドのエラーインジケーターに反影されます。
データセッション
パズルの最後のピースは、サーバーに保存するために、バリデーションされたデータを集めます。 以前のバージョンでは、関係しているレコードや ストア を手動で追跡し、 その save メソッドや sync メソッドを呼び出していました。複数のレコードとストアを扱う時には、管理、シーケンス、コールバックのチェーンの複雑な処理になる可能性がありました。
ここで、 Ext.data.Session がコードを大きくシンプルにさせます。セッションの主な作業は(そのタイプとIDで)レコードを追跡する事で、そのため、関連するものが同じレコードオブジェクトへの参照を取得できます。それぞれのレコードを追跡すると、編集が発生したときに、セッションがアソシエーションの内容を更新でき、維持できます。
セッションの生成
セッションは session session コンフィグを利用して生成されます。 決めなければならないのは、ビューの階層のどの位置で生成するかです。 アプリケーションにおいて、一定したレコードのセットをずっと使い続けるのであれば、最上位のレベル(viewport)でセッションを生成するのが正解です。 そうでなければ、 モーダル window や閉じることができるタブなどの子ビューのほうがいいでしょう。 なぜならセッションはセッションオーナーのビューと同時に破棄されるからです。
lookupSession メソッドを呼び出すと、どのコンポーネントでも、適切なセッションインスタンスを取得できます。指定されていれば、このメソッドはそのコンポーネントの session 構成を参照しますし、そうでなければビュー階層上で session が設定されている直近の先祖からセッションを検索します。つまり “session” は子供コンポーネントに継承されます
レコードの取得
レコードは、 getRecord を呼び出して手動でセッションから取得できますし、 createRecord を呼び出せば、セッション内に生成できます。 このメソッドは、ViewModel を利用している時は、直接に呼び出すことは滅多にありません。ViewModel は、 links コンフィグを使って、その操作を自動化します。 直接呼び出すか、ViewModel で自動化するとしても、このメソッドがあるおかげで、セッションが追跡するレコードのインスタンスの一貫性を確保できます。
セッションがレコードを読み込む時、そのアソシエーションに触れると、関連するレコードが同じセッションに読み込まれます。そうすると、サーバーに保存する時に、セッションが全ての変更を集めることができます。
セッションがロード、あるいは生成したレコードはそのセッションのものとなりますので、別のセッションと共有することは出来ませんので、気をつけてください。
セッションの検査
セッションの getChanges メソッドや visitData メソッドを呼び出すと、セッションが全てのレコードの現在の状態を出力するようにリクエストできます。このメソッドが返す情報には次のような便利な使い道があります。
- 一つのトランザクションで全ての変更をサーバーへ保存する
- カスタム Ajax リクエストで、クライアントのレコードの状態をサーバーに転送する
- 一般的な診断
セッションのアップデート
getChanges の逆をするのが update です。このメソッドは、getChanges が返す形式と同じものを使って、レコード変更の種類 (create, update, drop) と数を注入できます。内部的には、このメカニズムは子セッション(次に説明します)をサポートするために使われていますが他の技術も可能となります。
- ページロード時に、セッションと一緒に関係あるレコードを事前に読み込む。 これで、レコードやストアを個別に読み込む際の Ajax リクエストを削減できます。
- 保存 (あるいは破棄) の際にサーバーサイドのレコード変更結果をクライアントに (カスタム Ajax リクエストのレスポンスとして) 返します。 保存(または破棄)するために、サーバー側のレコード変更をクライアントに転送する(もしかしたら、カスタムのAjaxのリクエストに従って)。 つまり、クライアントからサーバー側のメソッドにレコードの状態をシームレスで転送でき、変更をサーバーに同時にコミットすることなく、そのレコードの変更をクライアントにマージできます。これは “what-if” ユーザー体験を構築している時に役に立ちます。
Child Sessions 子供セッション
ユーザーが変更して、その変更を実際に適用するかやめるかという UI を提供することはよくあります。例えば、通常の “OK/Cancel” のウィンドウです。新しい 「子」 セッションを生成して、そのようなウィンドウの ViewModel を分離させることができます。この子セッションは親セッションと通信し、レコードデータを取得しますが、すぐにそのレコードを変更しません。その代わりに、変更を保持できるように独自のレコードを生成します。ユーザーが変更を適用させると、子セッションの save メソッドを呼び出します。
Kitchen Sinkのサンプルでこの動作をご覧になれます。
セッションの保存
サーバーに変更を保存する時には、二つの選択肢があります。上記で説明した getChanges を使って、カスタム Ajax リクエストでデータを送信できます。または、各モデルに定義された標準の proxy プロキシ を使うこともできます。
標準のプロキシを使うには、セッションの
getSaveBatch
getSaveBatch
を呼び出します。
これは
Ext.data.Batch
Ext.data.Batch
のオブジェクトを返し、これの
start
を呼び出します。
サーバーが扱えないレコードが渡されないように、このバッチの一つ一つの操作は順序が決められます。この順序はモデルの参照フィールドからきています。例えば、OrderItem は orderId
フィールドがあるため、生成された Order レコードは、orderId の OrderItems の前にサーバーに送信されます。
もしサーバーが ID 生成を管理している場合、この時にクライアント側で生成したID を修正され、関連するレコードの対向する参照フィールドもアップデートされます。
注意事項
とても便利ですが、セッションには制限もあります。それは、セッションが役割を果たすため、関連しているレコードの参照を全て保つ必要があり、レコードが 削除(dropped) された場合のみ参照も削除できます。もしあるセッションを使い過ぎると、メモリーを沢山食うことになります。セッションを限られたライフサイクルがあるコンポーネントに連携し、セッションの生成と破棄ができるように、慎重に選択する必要があります。
まとめ
データレイヤー全体に渡って、それぞれの改善があると、皆様のアプリケーションの開発と保守がより簡単になると思いますし、以前にはできなかった技術まで提供されています。この新しい機能をどのようにアプリケーションで使ったかという報告や質問がある方は、是非 Sencha フォーラムに投稿して下さい。