转载请注明出处Ext中文网 (http://ajaxjs.com)。
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 关系中。
二、Model 实例管理器:ModelMgr
例如,假设一个博客的管理程序,有用户 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 亦可:
Ext.regModel('User', {
fields: ['id'],
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.BelongsToAssociation
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', {
fields: [
{name: 'id', type: 'int'},
{name: 'category_id', type: 'int'},
{name: 'name', type: 'string'}
],
associations: [
{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')。一般情况却是不需要的。
var Product = Ext.regModel('Product', {
fields: [...],
associations: [
{type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id'}
]
});
四、Ext.data.HasManyAssociation
HasManyAssociation 表示一对多的关系模型。如下例:
Ext.regModel('Product', {
fields: [
{name: 'id', type: 'int'},
{name: 'user_id', type: 'int'},
{name: 'name', type: 'string'}
]
});
Ext.regModel('User', {
fields: [
{name: 'id', type: 'int'},
{name: 'name', type: 'string'}
],
associations: [
{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', {
fields: [
'id', 'query'
],
hasMany: {
model: 'Tweet',
name : 'tweets',
filterProperty: 'query'
}
});
Ext.regModel('Tweet', {
fields: [
'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({
model: 'Tweet',
filters: [
{
property: 'query',
value : 'Sencha Touch'
}
]
});
数据可能来源于各个地方,但对于我们来说常见的途径是关系型数据库。本节大部分的内容都是基于关系型数据库进行展开的。
五、Ext.data.PolymorphicAssociation
多对多关系(鉴于文档信息不足……略……)
六、小结
相比于服务端的 ActiveRecord 方案,Ext 只是继承,自然没有太多高级的特性。也许客户端的话,仅此而已便足够……但是有没有人想把 Ext.data放到后台跑呢?天啊~难得不是为了……
从技术评估上看,动态语言比较适合实现所谓的 ActiveRecord,4.0 采用 ActiveRecord 的概念也是处于客户端当中的,生成的不是 SQL,而是通过 AJAX 请求之类的请求,可见这一思路丰富了既 ActiveRecord 的内涵,也从一侧面提升了动态语言的价值。
ORM 几乎是所有企业开发项目的标配,但可实现 ORM 的方案和思路却多种多样。虽不能说百花齐放,但也可以说繁荣到可以说争奇斗艳。
(……ORM 讨论若干字……略……见下面补充的链接)
既然选择了 ActiveRecord 去实现,想必也有一定的理由。JS 是动态语言,动态语言的确很容易做出 ActiveRecrod,这是无疑的,起码比静态语言好做。然而是否一定只选择AcitveRecord 呢?也不见得,例如微软的 JSLinq 也是一种思路,我见过有 JavaScript 方案的(虽然都是对 JSON 查询的,却缺少 JS2SQL 的),说明动态语言的优势还是很明显的,语言包袱没那么重。呵呵,不啰嗦,否则又容易扯起“语言之争”。
实际上模型 ActiveRecord 早已久负盛名,可能这就是 ExtJS 开发团队考量的因素之一。Ruby on Rails、Gails 上的 ActiveRecord 已经热闹非凡,JS 或 Ext 至今才实现的话算晚的了……原来我也写过 ActiveRecord 的 JS,当然差得远了,不过这一切都是没有实际项目的“纸上谈兵”有关概念、理论说明等的内容……还须见企业级开发导师马大叔的为准:http://www.martinfowler.com/eaaCatalog/activeRecord.html……上面权且为草草笔记。