Ext JS 4最強大的機能之一就是将模型的關系映射連結到一起。在 Ext 4 資料模型中,這種連結關系是通過關聯操作(associations)來完成的。在應用中定義不同物件的關系是非常自然的。比如說,在一個食譜資料庫中,一條食譜可能會有多條評論,多條評論又可能為同一作者所寫,而作者又可以創造多條食譜。透過定義這種連結關系可以讓你以更直覺地更強大的方式去操縱資料。
首先是 belongTo 管關聯。何謂“belongTo”,我們不妨這樣說(“屬于”這裡表示特定的關系):
公司資料庫中,賬單 accout 屬于 公司 company;
論壇程式中,文章 thread 屬于論壇 forum,也屬于分類 cateory;
一個畫冊中,縮略圖 thumbnail 屬于picture。
如果 bar 屬于foo,也就是說它們之間的關系 belongTo,那麼一般情況下,關系型資料庫實體表中,bar 表會有一稱作 foo_id 的字段,作為外鍵(foreign_key)出現。
相對地,與 belongs_to 關聯相對應地就是 hasMany 關聯,也就是“多對一” v.s “一對多”之間的差別;
也許我們可以借助“父-子”的概念去了解,例如父母可有多個孩子,孩子隻有一對父母。賬單相當于公司是“子級别”,而公司相當于賬單是“父級别”。
目前 Ext.data 可支援 belongTo(多對一)、hasMany(一對多)、多對多。而 hasOne 關系實際上包含在 belongTo 關系中。
例如,假設一個部落格的管理程式,有使用者 Users、貼子 Posts 和評論 Comments 三大業務對象。我們可以用以下文法表達它們之間的關系:
Edit 2011-9-22 示例代碼可能與最新版本的 Ext Model 有差別,但不影響主幹意思——感謝 Thanks to QiuQiu/太陽提醒。
Ext.regModel('Post', {
fields: ['id', 'user_id'],
belongsTo: 'User',
hasMany : {model: 'Comment', name: 'comments'}
});
Ext.regModel('Comment', {
fields: ['id', 'user_id', 'post_id'],
belongsTo: 'Post'
Ext.regModel('User', {
fields: ['id'],
hasMany: [
'Post',
{model: 'Comment', name: 'comments'}
]
通過定義屬性 associations 亦可:
associations: [
{type: 'hasMany', model: 'Post', name: 'posts'},
{type: 'hasMany', model: 'Comment', name: 'comments'}
在登記模型的過程中,主要執行的程式如下(詳細見注釋):
/**
* 登記一個模型的定義。所有模型的插件會被立即啟動,插件的參數就來自于model的配置項。
*/
registerType: function(name, config) {
…… ……
//if we're extending another model, inject its fields, associations and validations
if (extendName) {
// 現有的業務類,繼承之
extendModel = this.types[extendName];
extendModelProto = extendModel.prototype;
extendValidations = extendModelProto.validations;
proxy = extendModel.proxy;
fields = extendModelProto.fields.items.concat(fields);
associations = extendModelProto.associations.items.concat(associations);
config.validations = extendValidations ? extendValidations.concat(config.validations) : config.validations;
} else {
// 從Ext.data.Model繼承,誕生全新的業務類。
extendModel = Ext.data.Model;
proxy = config.proxy;
}
// 建立新的業務類/
model = Ext.extend(extendModel, config);
// 初始化插件
for (i = 0, length = modelPlugins.length; i < length; i++) {
plugins.push(PluginMgr.create(modelPlugins[i]));
// 儲存model到ModelMgr
this.types[name] = model;
// override方法修改prototype
Ext.override(model, {
plugins : plugins,
fields : this.createFields(fields),
associations: this.createAssociations(associations, name)
model.modelName = name;
// 注意call()用得巧妙!
Ext.data.Model.setProxy.call(model, proxy || this.defaultProxyType);
model.getProxy = model.prototype.getProxy;
// 靜态方法
model.load = function() {
Ext.data.Model.load.apply(this, arguments);
};
// 啟動插件
for (i = 0, length = plugins.length; i < length; i++) {
plugins[i].bootstrap(model, config);
model.defined = true;
this.onModelDefined(model);
return model;
},
Ext.data.Association 表示一對一的關系模型。主模型(owner model)應該有一個外鍵(a foreign key)的設定,也就是與之關聯模型的主鍵(the primary key)。
var Category = Ext.regModel('Category', {
fields: [
{name: 'id', type: 'int'},
{name: 'name', type: 'string'}
var Product = Ext.regModel('Product', {
{name: 'id', type: 'int'},
{name: 'category_id', type: 'int'},
{name: 'name', type: 'string'}
],
{type: 'belongsTo', model: 'Category'}
上面例子中我們分别建立了 Products 和 Cattegory 模型,然後将它們關聯起來,此過程我們可以說産品 Product 是“屬于”種類Category的。預設情況下,Product 有一個 category_id 的字段,通過該字段,每個 Product 實體可以與 Category 關聯在一起,并在 Product 模型身上産生新的函數。
獲得新函數,其原理是通過反射得出的。第一個加入到主模型的函數是 Getter 函數。
var product = new Product({
id: 100,
category_id: 20,
name: 'Sneakers'
product.getCategory(function(category, operation) {
//這裡可以根據cateory對象來完成一些任務。do something with the category object
alert(category.get('id')); //alerts 20
}, this);
在定義關聯關系的時候,就為 Product 模型建立了 getCategory 函數。另外一種 getCategory 函數的用法是送入一個包含 success、failure 和 callback 的對象,都是函數類型。其中,必然一定會調用 callback,而 success 就是成功加載所關聯的模型後,才會調用的 success 的函數;反之沒有加載關聯模型,就執行 failure 函數。
product.getCategory({
callback: function(category, operation), //一定會調用的函數。a function that will always be called
success : function(category, operation), //成功時調用的函數。a function that will only be called if the load succeeded
failure : function(category, operation), //失敗時調用的函數。a function that will only be called if the load did not succeed
scope : this // 作用域對象是一個可選的參數,其決定了回調函數中的作用域。optionally pass in a scope object to execute the callbacks in
以上的回調函數執行時帶有兩個參數:1、所關聯的模型之執行個體;2、負責加載模型執行個體的Ext.data.Operation對象。當加載執行個體有問題時,Operation對象就非常有用。
第二個生成的函數設定了關聯的模型執行個體。如果隻傳入一個參數到setter那麼下面的兩個調用是一緻的:
// this call
product.setCategory(10);
//is equivalent to this call:
product.set('category_id', 10);
如果傳入第二個參數,那麼模型會自動儲存并且将第二個參數傳入到主模型的 Ext.data.Model.save 方法:
product.setCategory(10, function(product, operation) {
//商品已經保持了。the product has been saved
alert(product.get('category_id')); //now alerts 10
//另外一種文法: alternative syntax:
product.setCategory(10, {
callback: function(product, operation), //一定會調用的函數。a function that will always be called
success : function(product, operation), //成功時調用的函數。a function that will only be called if the load succeeded
failure : function(product, operation), //失敗時調用的函數。a function that will only be called if the load did not succeed
scope : this //作用域對象是一個可選的參數,其決定了回調函數中的作用域。optionally pass in a scope object to execute the callbacks in
})
Model 可以讓我們自定義字段參數。若不設定,關聯模型的時候會自動根據 primaryKey 和 foreignKey 屬性設定。這裡我們替換掉了預設的主鍵(預設為'id')和外鍵(預設為'category_id')。一般情況卻是不需要的。
fields: [...],
{type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id'}
HasManyAssociation 表示一對多的關系模型。如下例:
Ext.regModel('Product', {
{name: 'id', type: 'int'},
{name: 'user_id', type: 'int'},
{name: 'name', type: 'string'}
{type: 'hasMany', model: 'Product', name: 'products'}
以上我們建立了 Products 和model 模型,我們可以說使用者有許多商品。每一個 User 執行個體都有一個新的函數,此時此刻具體這個函數就是“product”,這正是我們在 name 配置項中所指定的名字。新的函數傳回一個特殊的 Ext.data.Store,自動根據模型執行個體建立産品。
// 首先,為user建立一筆新的紀錄1。
var user = Ext.ModelMgr.create({id: 1, name: 'Ed'}, 'User');
// 根據既定的關系,建立user.products方法,該方法傳回Store對象。
// 建立的Store的作用域自動定義為User的id等于1的産品。
var products = user.products();
// products是一個普通的Store,可以加入輕松地通過add()紀錄
products.add({
name: 'Another Product'
// 執行Store的儲存指令。儲存之前都自動哦你将産品的user_id為1。
products.sync();
所述的 Store 隻在頭一次執行 product() 時執行個體化,持久化在記憶體中不會反複建立。
由于 Store 的 API 中自帶 filter 過濾器的功能,是以預設下過濾器告訴 Store 隻傳回關聯模型其外鍵所比對主模型其主鍵。例如,使用者 User 是 ID=100 擁有的産品 Products,那麼過濾器隻會傳回那些符合 user_id=100 的産品。
但是有些時間必須指定任意字段來過濾,例如 Twitter 搜尋的應用程式,我們就需要 Search 和 Tweet 模型:
var Search = Ext.regModel('Search', {
'id', 'query'
hasMany: {
model: 'Tweet',
name : 'tweets',
filterProperty: 'query'
Ext.regModel('Tweet', {
'id', 'text', 'from_user'
// 傳回filterProperty指定的過程字段。returns a Store filtered by the filterProperty
var store = new Search({query: 'Sencha Touch'}).tweets();
例子中的 tweets 關系約等價于下面代碼所示,也就是通過 Ext.data.HasManyAssociation.filterProperty 定義過濾器。
var store = new Ext.data.Store({
filters: [
{
property: 'query',
value : 'Sencha Touch'
資料可能來源于各個地方,但對于我們來說常見的途徑是關系型資料庫。本節大部分的内容都是基于關系型資料庫進行展開的。
多對多關系(鑒于文檔資訊不足……略……)
相比于服務端的 ActiveRecord 方案,Ext 隻是繼承,自然沒有太多進階的特性。也許用戶端的話,僅此而已便足夠……但是有沒有人想把 Ext.data放到背景跑呢?天啊~難得不是為了……
從技術評估上看,動态語言比較适合實作所謂的 ActiveRecord,4.0 采用 ActiveRecord 的概念也是處于用戶端當中的,生成的不是 SQL,而是通過 AJAX 請求之類的請求,可見這一思路豐富了既 ActiveRecord 的内涵,也從一側面提升了動态語言的價值。
ORM 幾乎是所有企業開發項目的标配,但可實作 ORM 的方案和思路卻多種多樣。雖不能說百花齊放,但也可以說繁榮到可以說争奇鬥豔。
(……ORM 讨論若幹字……略……見下面補充的連結)
既然選擇了 ActiveRecord 去實作,想必也有一定的理由。JS 是動态語言,動态語言的确很容易做出 ActiveRecrod,這是無疑的,起碼比靜态語言好做。然而是否一定隻選擇AcitveRecord 呢?也不見得,例如微軟的 JSLinq 也是一種思路,我見過有 JavaScript 方案的(雖然都是對 JSON 查詢的,卻缺少 JS2SQL 的),說明動态語言的優勢還是很明顯的,語言包袱沒那麼重。呵呵,不啰嗦,否則又容易扯起“語言之争”。