データパッケージとは、アプリケーション内のあらゆるデータのロード・セーブを行うパッケージです。多数のクラスで構成されますが、他のすべてよりも重要なクラスが3つあります。
以下がこれらのクラスです。
上記のクラスはほとんどすべてのアプリケーションで使用されます。これらは多くのサテライトクラスでもサポートされています。
モデル
データパッケージの中心となるのは Ext.data.Model です。モデルはアプリケーション内のエンティティを示します。例えば、eコマースアプリでは、Users(ユーザー)、Products(商品)、Orders(注文)などのモデルがあります。最もシンプルなレベルで、モデルはフィールドのセットと関連ビジネスロジックを定義します。
それでは、モデルの主な部分を見ていきましょう。
モデルの作成
通常は共通ベースクラスを作ってから、モデルを定義するのが一番です。このベースクラスがあると、1ヶ所ですべてのモデルの特定の要素を簡単に設定できるようになります。スキーマを設定する場所としても適しています。スキーマは、アプリケーション内のすべてのモデルのマネージャーです。それでは、最も役立つ設定オプション2つに焦点を当てていきます。
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” についてはアプリケーションごとに違うでしょう。
プロキシ
プロキシは、モデルデータのロードや保存を行うためにモデルとストアによって使用されます。プロキシには、クライアントとサーバーの2種類があります。
プロキシは、(上記の通り)モデルのベースクラスのスキーマに直接定義することができます。
クライアントプロキシ
クライアントプロキシには、Memory や HTML5 localstorage 機能を利用する Local Storage プロキシなどがあります。古いブラウザはこれらの新しいHTML5 APIをサポートしていませんが、その存在によってたくさんのアプリケーションが非常に役立つようになり、とても便利です。
サーバープロキシ
サーバープロキシは、リモートサーバーへのデータの管理を処理します。このタイプのプロキシには、AJAX、JSONP、REST などがあります。
スキーマ
スキーマは互いに関連している要素の集まりです。モデルが “schema” コンフィグを指定すると、そのスキーマは派生モデルすべてに継承されます。上記の例では、そのスキーマの全モデルのデフォルトを確立する2つの値で設定されています。
コンフィグの最初は “namespace” です。この名前空間を指定すると、すべてのモデルは “entityName” と呼ばれる短縮された名前を取得します。この短縮名はまずモデルの間の関連を定義する場合に役に立ちますが、これについては後述します。
この例の schema では、“proxy” コンフィグも定義しています。これはオブジェクトテンプレートで、Ext.XTemplate に基づいたテキストテンプレートに似ています。違いは、オブジェクトテンプレートにデータが渡されたら、オブジェクトを生成します。この場合、そのデータを使用して、プロキシを明示的に定義していないモデルすべてに自動的に “proxy” コンフィグを定義します。
この機能は、各モデルのインスタンスにおいて、データを同じようにロードする必要があり、ほんの少しだけ値が違うという場合に便利です。これにより、各モデルに同じプロキシ定義を繰り返すことを避けられます。
URL では User.json
を url:'{entityName}.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
レコードのセットを取得するために、手動でストアをロードしています。ストアのロードが完了してコールバック関数が発火すると、レコードがログされます。
インラインデータ
ストアはデータをインラインでロードすることもできます。内部的には、ストアは、データとして該当する Model タイプのレコードに渡す各オブジェクトを変換します。
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 を通じて、いつでも簡単にこれらのアイテムの属性を変更できます。
アソシエーション
モデルは、アソシエーションのAPIと一緒にリンクすることができます。ほとんどのアプリケーションでは、多くの異なるモデルを扱い、そしてモデルはたいてい関連があります。ブログ作成アプリケーションには、User と Post のモデルがあります。各 User は、Post を作成します。この場合は、User は複数の Post をする可能性がありますが、各 Post は1人の 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', // the entityName for MyApp.model.User
type: 'int'
}, {
name: 'title',
type: 'string'
}]
});
アプリケーションの異なるモデルにおける豊富な関係を表すのは簡単です。各モデルは、他のモデルといくつものアソシエーションを持つことができます。さらに、モデルはどの順序で定義してもかまいません。このタイプのモデルのレコードがあると、関連付けられているデータを追いかけることが簡単になります。例えば、ある User の Post を全て取得したい場合、次のようにします。
// Loads User with ID 1 and related posts and comments
// using User's Proxy
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"
}]
}]
}
フレームワークは上記のようなネストしたデータの1つのレスポンスを自動的に解析できます。User データと別に Post データ用にリクエストをするのではなく、全てのデータを1つのサーバーレスポンスにまとめて返すことができます。
バリデーション
さらに、モデルは様々なデータのバリデーションをサポートします。上記で使用した例に追加して説明します。まず、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 の範囲内の長さとなるように確認します。両方の制約はオプションです。
- Format - 文字列が正規表現フォーマットに一致することを確認します。上の例では、age フィールドが数字のみで構成されていることを確認します。
- Inclusion - 値が特定の値のセット(例:性別の場合、男性か女性のどちらかであることを保証)の範囲内であることを確認します。
- Exclusion - 値が特定の値のセット(例:‘admin’ のような username のブラックリスト化)に含まれないことを確認します。
バリデーションの機能を把握したので、User インスタンスに対して利用してみましょう。User を作成し、それに対してバリデーションを実行し、失敗をログします。
// now lets try to create a new user with as many validation
// errors as we can
var newUser = new MyApp.model.User({
id: 10,
name: 'Bender'
});
// run some validation on the new user we just created
console.log('Is User valid?', newUser.isValid());
//returns 'false' as there were validation errors
var errors = newUser.getValidation(),
error = errors.get('name');
console.log("Error is: " + error);
ここでキーとなる関数は getValidation() です。これは、すべての設定されたバリデーションを動作させ、エラーがあった場合は各フィールドでの最初のエラーが格納されたレコードを、有効なフィールドに対しては Boolean 値の 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 に設定されます。