データパッケージ
データパッケージはアプリケーション内で全てのデータを保存するものです。複数のクラスでできていますが、中でも、特に重要な3つのクラスがあります。
それはつぎの3つです。
上記のクラスはほとんど全てのアプリケーションが利用します。いくつかのサテライトクラスでサポートされています。
モデル
データパッケージの中心となるのは Ext.data.Model
です。モデルはアプリケーション内のエンティティを表現します。例えば、eコマースアプリケーションでは、Users ( ユーザー )、Products (商品)、Orders (注文) などのモデルがあるでしょう。最もシンプルなレベルで、モデルはフィールドのセットと関係するビジネスロジックを定義します。
さて、モデルの主な部分をご覧ください:
モデルの生成
通常は共通ベースクラスを作ってから、モデルを定義するのが一番です。このベースクラスがあると、一カ所で全てのモデルのある部分を簡単に構成できるようになります。スキーマを構成する場所としても適しています。スキーマは、アプリケーションの全てのモデルのマネージャーです。この時点では、最も役立つコンフィグオプション二つに焦点を当てます。
Ext.define('MyApp.model.Base', { extend: 'Ext.data.Model', fields: [{ name: 'id', type: 'int' }], schema: { namespace: 'MyApp.model', // generate auto entityName proxy: { // Ext.util.ObjectTemplate type: ajax, url: '{entityName}.json', reader: { type: 'json', rootProperty: '{entityName:lowercase}' } } } }); |
モデルのベースクラスの適切な内容はアプリケーション毎に違うでしょう。特に fields
についてはそうでしょう。
プロキシ
プロキシは、モデルデータの読込と保存を管理するためにモデルとストアが利用します。プロキシには二つの種類があります。クライアントとサーバーです。
プロキシは、(上記の通り)モデルのベースクラスのスキーマに直接定義する事ができます。
クライアントプロキシ
クライアントプロキシには、Memory プロキシ や HTML5 localStorage 機能を利用する LocalStrage プロキシなどがあります。過去のブラウザは、この新しい HTML5 API をサポートしていませんが、とても便利で、多くのアプリケーションが多大な恩恵に浴することができます。
サーバープロキシ
サーバープロキシは、リモートサーバーへのデータの管理をします。 AJAX、 JSONP、 REST、 があります。
スキーマ
スキーマは互いに関連している要素の集まりです。モデルに schema
コンフィグを指定すると、そのスキーマは、派生モデル全てに継承されます。上記のサンプルでは、そのスキーマの全モデルのデフォルトを確立する二つの値で構成されています。
コンフィグの最初は “namespace” です。この名前空間を指定すると、全てのモデルは “entityName” と呼ばれる短縮された名前を取得します。この短縮名はまずモデルの間の関連を定義する時に役に立ちますが、後に説明します。
サンプルの schema
では、proxy
コンフィグも定義しています。これはオブジェクトテンプレートというもので、Ext.XTemplate
に基づいたテキストテンプレートによく似ています。違いは、オブジェクトテンプレートにデータが渡されたら、オブジェクトを生成します。この場合、プロキシを明示的に定義していないモデル全てに自動的に proxy
を定義します。
この機能は、各モデルのインスタンスにおいて、データを同じように読み込む必要があり、ほんの少しだけ値が違うという場合に便利です。これにより、各モデルに同じプロキシ定義を繰り返すことを避ける事ができます。
URLでは
url: '{entityName}.json'
と指定していますので、User モデルの場合には、User.json
となり、それは JSON 文字列を返します。
例えば、次の様な感じです。
{ "success": "true", "user": [ { "id": 1, "name": "Philip J. Fry" }, { "id": 2, "name": "Hubert Farnsworth" }, { "id": 3, "name": "Turanga Leela" }, { "id": 4, "name": "Amy Wong" } ] } |
ストア
モデルはよくストアと共に利用されます。ストアは基本的にレコード(モデルの派生クラスのインスタンス)の集まりです。ストアの生成したり、データをロードしたりするのは次の様に簡単にできます。
var store = new Ext.data.Store ({ model: 'MyApp.model.User' }); store.load({ callback:function(){ var first_name = this.first().get('name'); console.log(first_name); } }); |
MyApp.model.User
レコードのセットを取得するために、手動でストアをロードしています。ストアのロードが完了したら load
メソッドに渡したの callback
関数が発火し、レコードの内容がコンソールに出力されます。
インライン データ
ストアにはインラインでデータを読み込む事もできます。内部的には、ストアは、 data コンフィグに指定したオブジェクトを 適切なタイプのモデルのレコードに変換します。
new Ext.data.Store({ model: 'MyApp.model.User', data: [{ id: 1, name: "Philip J. Fry" },{ id: 2, name: "Hubert Farnsworth" },{ id: 3, name: "Turanga Leela" },{ id: 4, name: "Amy Wong" }] }); |
ソートとグループ
ストアはソート、フィルタ、グルーピングをローカルとリモートで行う事ができます。
new Ext.data.Store({ model: 'MyApp.model.User', sorters: ['name','id'], filters: { property: 'name', value : 'Philip J. Fry' } }); |
このストアでは、データはまず name
で、その後に id
の順番でソートされます。データは、’Philip J. Fry’ という名前があるユーザーだけを含むようにフィルタリングされます。ストアの API を通して、この属性をいつでも簡単に変更する事ができます。
アソシエーション
モデルは、Associations API によって、互いにリンクする事ができます。ほとんどのアプリケーションでは、様々なモデルを扱い、ほとんどのモデルは互いに関連しています。ブログ作成アプリケーションでは User(ユーザー)と Post (記事)のモデルがあるでしょう。各 User は、Post を生成します。この場合は、User は複数の Post をする可能性がありますが、各 Post は、一人の User が生成します。これは ManyToOne リレーションシップというものです。この関係は次のように表す事ができます。
Ext.define('MyApp.model.User', { extend: 'MyApp.model.Base', fields: [{ name: 'name', type: 'string' }] }); Ext.define('MyApp.model.Post', { extend: 'MyApp.model.Base', fields: [{ name: 'userId', reference: 'User', // MyApp.model.User の entityName type: 'int' }, { name: 'title', type: 'string' }] }); |
アプリケーション内での、様々なモデル間の関係を簡単に表現できます。各モデルは、他のモデルといくつものアソシエーションを持つことができます。さらに、モデルはどの順番で定義してもかまいません。このタイプのモデルのレコードがあると、関連付けられているデータを追いかけるのが簡単になります。例えば、もしある User の Post を全て取得したいと思うなら、次のようにします。
// id が 1 の User をロードし、関連する Post を取得する MyApp.model.User.load(1, { callback: function(user) { console.log('User: ' + user.get('name')); user.posts(function(posts){ posts.each(function(post) { console.log('Post: ' + post.get('title')); }); }); } }); |
上記のアソシエーションを指定すると、モデルに新しい関数が追加されます。各 User モデルには複数の Post があり、user.posts()
関数が追加されました。上記ではそれを使っています。user.posts()
を呼び出すと、Post モデルのインスタンスが格納された Store を返します。
関連付けは、データのロードの時だけに使えるわけではありません。新しいレコードを生成する時にも便利です。
user.posts().add({ userId: 1, title: ‘Post 10’ });
user.posts().sync();
これは、新しい Post をインスタンス化し、userId
フィールドに User の ID が自動的にセットされます。sync() を呼び出すと、新しい Post をプロキシ(これは最終的にスキーマのプロキシコンフィグで定義されます)経由で保存します。これは非同期の操作で、操作が完了したら通知されるようにコールバックを渡す事もできます。
アソシエーションの「反対側」でも Post モデルに新しいメソッドを生成します。
MyApp.model.Post.load(1, { callback: function(post) { post.getUser(function(user) { console.log('Got user from post: ' + user.get('name')); }); } }); MyApp.model.Post.load(2, { callback: function(post) { post.setUser(100); } }); |
ロードする関数 getUser()
は非同期で、User インスタンスを取得するための、コールバックの関数が必要です。 setUser()
メソッドは、単に userId
(外部キーともいいます)を 100 にアップデートし、Post モデルを保存します。保存の処理が完了したら(成功でも失敗でも)呼び出されるコールバックも渡されています。
ネストしたデータの読み込み
アソシエーションが定義されると、レコードのロードの際に、単一のリクエストで関連するレコードも読み込む事ができます。例えば、次のようなサーバーレスポンスをご覧ください。
{ "success": true, "user": [{ "id": 1, "name": "Philip J. Fry", "posts": [{ "title": "Post 1" },{ "title": "Post 2" },{ "title": "Post 3" }] }] } |
フレームワークは上記のようなネストしたデータの一つのレスポンスを自動的に解析できます。User データと別に Post データ用にリクエストをするのではなく、全てのデータを一つのサーバーレスポンスにまとめる事ができます。
バリデーション
モデルには、様々なデータのバリデーションをサポートします。上記のサンプルに追加して説明します。まず、User モデルにいくつかのバリデーションを追加しましょう。
Ext.define('MyApp.model.User', { extend: 'Ext.data.Model', fields: ..., validators: { name: [ 'presence', { type: 'length', min: 7 }, { type: 'exclusion', list: ['Bender'] } ] } }); |
validators
は、フィールド名をキーとして、バリデーションのルールを値としたオブジェクトとして定義します。
このルールは、validator
オブジェクトのコンフィグまたは、コンフィグの配列になります。
サンプルの validators
では、name
フィールドは、最低 7文字で、値は “Bender” 以外なら大丈夫ということになります。
いくつかのバリデーションには、オプションのコンフィグがあります。例えば、length
のバリデーションには min
と max
のプロパティを指定する事ができ、format
には matcher
を指定する事ができます。Ext JS には ビルトインのバリデーションが 5つあり、カスタムルールを追加するのも簡単にできます。
まず、ビルトインのものを紹介しましょう。
- Presence – フィールドに値がある事を確保します。ゼロも有効な値ですが、空文字列は無効です。
- Length – 文字列が
min
とmax
の範囲の長さである事を確保します。min
も max も省略可能です。 - Format – 文字列が正規表現に一致する事を確保します。
- Inclusion – 指定した値リストのどれかの値になる事を確保します(例えば、性別は男か女のどちらかであることを確保する)
- Exclusion – 値が指定された値リストのどれにも一致していない事を確保します(例えば、admin などのユーザー名を作成するのは無効にするなど)
さて、様々なバリデーションの種類が理解できたので、ある User インスタンスに対して利用してみましょう。User を生成し、それに対してバリデーションを動作させて、失敗をログします。
// さて、できるだけ多くの検証エラーがある新しいユーザーを作成しましょう var newUser = new MyApp.model.User({ id: 10, name: 'Bender' }); // 生成したユーザーに検証を実行して下さい console.log('Is User valid?', newUser.isValid()); // 検証のエラーがあったため、 'false' が返されます var errors = newUser.getValidation(), error = errors.get('name'); console.log("Error is: " + error); |
ここでキーとなる関数は getValidation()
です。これは、全ての構成されたバリデーションを動作させ、エラーがあった場合は各フィールドでの最初のエラーが格納されたレコードを、エラーがなかった場合は論理値の true
を返します。バリデーションレコードは必要な時に生成され、リクエストされた時だけ更新されます。
この場合は、name フィールドの最初のエラーは “Length must be greater than 7” となります。
それでは、7文字以上の名前を設定しましょう。
newUser.set('name', 'Bender Bending Rodriguez'); errors = newUser.getValidation(); |
このユーザーレコードは、全てのバリデーション条件を満たします。レコードは、存在していて、7文字以上あり、name は Bender と一致しません。
newUser.isValid()
は true を返します。getValidation()
を呼び出すと、バリデーションのレコードが更新され、もうダーティな状態にはなっていません。それで、全てのフィールドは true
にセットされます。