预览ExtJS 4.0的新功能(三):客户端数据层的革新:引入ActiveRecord模式

来源:互联网 发布:网络黑界是什么意思 编辑:程序博客网 时间:2024/05/19 01:13

转载请注明出处Ext中文网 (http://www.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/太阳提醒。

view plain
  1. Ext.regModel('Post', {  
  2.     fields: ['id''user_id'],  
  3.     belongsTo: 'User',  
  4.     hasMany  : {model: 'Comment', name: 'comments'}  
  5. });  
  6. Ext.regModel('Comment', {  
  7.     fields: ['id''user_id''post_id'],  
  8.     belongsTo: 'Post'  
  9. });  
  10. Ext.regModel('User', {  
  11.     fields: ['id'],  
  12.     hasMany: [  
  13.         'Post',  
  14.         {model: 'Comment', name: 'comments'}  
  15.     ]  
  16. });  

通过定义属性associations亦可:

view plain
  1. Ext.regModel('User', {  
  2.     fields: ['id'],  
  3.     associations: [  
  4.         {type: 'hasMany', model: 'Post',    name: 'posts'},  
  5.         {type: 'hasMany', model: 'Comment', name: 'comments'}  
  6.     ]  
  7. });  

在登记模型的过程中,主要执行的程序如下(详细见注释):

view plain
  1. /** 
  2.  * 登记一个模型的定义。所有模型的插件会被立即启动,插件的参数就来自于model的配置项。 
  3.  */  
  4. registerType: function(name, config) {  
  5.     …… ……  
  6.       
  7.     //if we're extending another model, inject its fields, associations and validations  
  8.     if (extendName) {  
  9.         // 现有的业务类,继承之  
  10.         extendModel       = this.types[extendName];  
  11.         extendModelProto  = extendModel.prototype;  
  12.         extendValidations = extendModelProto.validations;  
  13.           
  14.         proxy              = extendModel.proxy;  
  15.         fields             = extendModelProto.fields.items.concat(fields);  
  16.         associations       = extendModelProto.associations.items.concat(associations);  
  17.         config.validations = extendValidations ? extendValidations.concat(config.validations) : config.validations;  
  18.     } else {  
  19.         // 从Ext.data.Model继承,诞生全新的业务类。  
  20.         extendModel = Ext.data.Model;  
  21.         proxy = config.proxy;  
  22.     }  
  23.     // 创建新的业务类/  
  24.     model = Ext.extend(extendModel, config);  
  25.     // 初始化插件  
  26.     for (i = 0, length = modelPlugins.length; i < length; i++) {  
  27.         plugins.push(PluginMgr.create(modelPlugins[i]));  
  28.     }  
  29.     // 保存model到ModelMgr  
  30.     this.types[name] = model;  
  31.     // override方法修改prototype  
  32.     Ext.override(model, {  
  33.         plugins     : plugins,  
  34.         fields      : this.createFields(fields),  
  35.         associations: this.createAssociations(associations, name)  
  36.     });  
  37.       
  38.     model.modelName = name;  
  39.     // 注意call()用得巧妙!  
  40.     Ext.data.Model.setProxy.call(model, proxy || this.defaultProxyType);  
  41.     model.getProxy = model.prototype.getProxy;  
  42.     // 静态方法  
  43.     model.load = function() {  
  44.         Ext.data.Model.load.apply(this, arguments);  
  45.     };  
  46.     // 启动插件  
  47.     for (i = 0, length = plugins.length; i < length; i++) {  
  48.         plugins[i].bootstrap(model, config);  
  49.     }  
  50.       
  51.     model.defined = true;  
  52.     this.onModelDefined(model);  
  53.       
  54.     return model;  
  55. },  

三、Ext.data.BelongsToAssociation

Ext.data.Association表示一对一的关系模型。主模型(owner model)应该有一个外键(a foreign key)的设置,也就是与之关联模型的主键(the primary key)。

view plain
  1. var Category = Ext.regModel('Category', {  
  2.     fields: [  
  3.         {name: 'id',   type: 'int'},  
  4.         {name: 'name', type: 'string'}  
  5.     ]  
  6. });  
  7. var Product = Ext.regModel('Product', {  
  8.     fields: [  
  9.         {name: 'id',          type: 'int'},  
  10.         {name: 'category_id', type: 'int'},  
  11.         {name: 'name',        type: 'string'}  
  12.     ],  
  13.     associations: [  
  14.         {type: 'belongsTo', model: 'Category'}  
  15.     ]  
  16. });  

上面例子中我们分别创建了Products和Cattegory模型,然后将它们关联起来,此过程我们可以说产品Product是“属于”种类Category的。默认情况下,Product有一个category_id的字段,通过该字段,每个Product实体可以与Category关联在一起,并在Product模型身上产生新的函数。

获得新函数,其原理是通过反射得出的。第一个加入到主模型的函数是Getter函数。

view plain
  1. var product = new Product({  
  2.     id: 100,  
  3.     category_id: 20,  
  4.     name: 'Sneakers'  
  5. });  
  6. product.getCategory(function(category, operation) {  
  7.     //这里可以根据cateory对象来完成一些任务。do something with the category object  
  8.     alert(category.get('id')); //alerts 20  
  9. }, this);  

在定义关联关系的时候,就为Product模型创建了getCategory函数。另外一种getCategory函数的用法是送入一个包含success、failure和callback的对象,都是函数类型。其中,必然一定会调用callback,而success就是成功加载所关联的模型后,才会调用的success的函数;反之没有加载关联模型,就执行failure函数。

view plain
  1. product.getCategory({  
  2.     callback: function(category, operation), //一定会调用的函数。a function that will always be called  
  3.     success : function(category, operation), //成功时调用的函数。a function that will only be called if the load succeeded  
  4.     failure : function(category, operation), //失败时调用的函数。a function that will only be called if the load did not succeed  
  5.     scope   : this // 作用域对象是一个可选的参数,其决定了回调函数中的作用域。optionally pass in a scope object to execute the callbacks in  
  6. });  

以上的回调函数执行时带有两个参数:1、所关联的模型之实例;2、负责加载模型实例的Ext.data.Operation对象。当加载实例有问题时,Operation对象就非常有用。

第二个生成的函数设置了关联的模型实例。如果只传入一个参数到setter那么下面的两个调用是一致的:

view plain
  1. // this call  
  2. product.setCategory(10);  
  3. //is equivalent to this call:  
  4. product.set('category_id', 10);  

如果传入第二个参数,那么模型会自动保存并且将第二个参数传入到主模型的Ext.data.Model.save方法:

view plain
  1. product.setCategory(10, function(product, operation) {  
  2.     //商品已经保持了。the product has been saved  
  3.     alert(product.get('category_id')); //now alerts 10  
  4. });  
  5. //另外一种语法: alternative syntax:  
  6. product.setCategory(10, {  
  7.     callback: function(product, operation), //一定会调用的函数。a function that will always be called  
  8.     success : function(product, operation), //成功时调用的函数。a function that will only be called if the load succeeded  
  9.     failure : function(product, operation), //失败时调用的函数。a function that will only be called if the load did not succeed  
  10.     scope   : this //作用域对象是一个可选的参数,其决定了回调函数中的作用域。optionally pass in a scope object to execute the callbacks in  
  11. })  

Model可以让我们自定义字段参数。若不设置,关联模型的时候会自动根据primaryKey}和foreignKey属性设置。这里我们替换掉了默认的主键(默认为'id')和外键(默认为'category_id')。一般情况却是不需要的。

view plain
  1. var Product = Ext.regModel('Product', {  
  2.     fields: [...],  
  3.     associations: [  
  4.         {type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id'}  
  5.     ]  
  6. });  

四、Ext.data.HasManyAssociation

HasManyAssociation表示一对多的关系模型。如下例:

view plain
  1. Ext.regModel('Product', {  
  2.     fields: [  
  3.         {name: 'id',      type: 'int'},  
  4.         {name: 'user_id', type: 'int'},  
  5.         {name: 'name',    type: 'string'}  
  6.     ]  
  7. });  
  8. Ext.regModel('User', {  
  9.     fields: [  
  10.         {name: 'id',   type: 'int'},  
  11.         {name: 'name', type: 'string'}  
  12.     ],  
  13.     associations: [  
  14.         {type: 'hasMany', model: 'Product', name: 'products'}  
  15.     ]  
  16. });  

 

以上我们创建了Products和model模型,我们可以说用户有许多商品。每一个User实例都有一个新的函数,此时此刻具体这个函数就是“product”,这正是我们在name配置项中所指定的名字。新的函数返回一个特殊的Ext.data.Store,自动根据模型实例建立产品。

 

view plain
  1. // 首先,为user创建一笔新的纪录1。  
  2. var user = Ext.ModelMgr.create({id: 1, name: 'Ed'}, 'User');  
  3. // 根据既定的关系,创建user.products方法,该方法返回Store对象。  
  4. // 创建的Store的作用域自动定义为User的id等于1的产品。  
  5. var products = user.products();  
  6. // products是一个普通的Store,可以加入轻松地通过add()纪录  
  7. products.add({  
  8.     name: 'Another Product'  
  9. });  
  10. // 执行Store的保存命令。保存之前都自动哦你将产品的user_id为1。  
  11. products.sync();  

所述的Store只在头一次执行product()时实例化,持久化在内存中不会反复创建。

由于Store的API中自带filter过滤器的功能,所以默认下过滤器告诉Store只返回关联模型其外键所匹配主模型其主键。例如,用户User是ID=100拥有的产品Products,那么过滤器只会返回那些符合user_id=100的产品。

但是有些时间必须指定任意字段来过滤,例如Twitter搜索的应用程序,我们就需要Search和Tweet模型:

view plain
  1. var Search = Ext.regModel('Search', {  
  2.     fields: [  
  3.         'id''query'  
  4.     ],  
  5.     hasMany: {  
  6.         model: 'Tweet',  
  7.         name : 'tweets',  
  8.         filterProperty: 'query'  
  9.     }  
  10. });  
  11. Ext.regModel('Tweet', {  
  12.     fields: [  
  13.         'id''text''from_user'  
  14.     ]  
  15. });  
  16. // 返回filterProperty指定的过程字段。returns a Store filtered by the filterProperty  
  17. var store = new Search({query: 'Sencha Touch'}).tweets();  

例子中的tweets关系等价于下面,也就是通过Ext.data.HasManyAssociation.filterProperty定义过滤器。

view plain
  1. var store = new Ext.data.Store({  
  2.     model: 'Tweet',  
  3.     filters: [  
  4.         {  
  5.             property: 'query',  
  6.             value   : 'Sencha Touch'  
  7.         }  
  8.     ]  
  9. });  

数据可能来源于各个地方,但对于我们来说常见的途径是关系型数据库。本节大部分的内容都是基于关系型数据库进行展开的。

五、Ext.data.PolymorphicAssociation

多对多关系(鉴于文档信息不足……略……)

六、小结

相比于服务端的ActiveRecord方案,Ext的没有太多高级的特性。也许,客户端的话,仅此而已便足够……但是有没有人想把Ext.data.放到后台跑呢?天啊~难得不是为了……

从技术评估上看,4.0运用ActiveRecord的概念是处于客户端当中的,生成的不是SQL,而极有可能是AJAX请求之类的请求,可见这一思路丰富了ActiveRecord的内涵,从一侧面提升了动态语言的价值。

ORM几乎是所有企业开发项目的标配,但可实现ORM的方案和思路却多种多样。虽不能说百花齐放,但也可以说繁荣到可以说争奇斗艳。

(……ORM讨论若干字……略……)

既然选择了ActiveRecord去实现,想必也有一定的理由。JS是动态语言,动态语言的确很容易做出ActiveRecrod,这是无疑的,起码比静态语言好做。然而是否一定只选择AcitveRecord呢?也不见得,例如微软的JSLinq也是一种思路,我见过有JavaScript方案的(虽然都是对JSON查询的,却缺少JS2SQL的),说明动态语言的优势还是很明显的,语言包袱没那么重。呵呵,不啰嗦,否则又容易扯起“语言之争”。

实际上模型ActiveRecord早已久负盛名,可能这就是Ext开发团队考量的因素之一。Ruby on Rails、Gails上的ActiveRecord已经热闹非凡,JS或Ext至今才实现的话算晚的了……原来我也写过ActiveRecord的JS,当然差得远了,不过这一切都是没有实际项目的“纸上谈兵”有关概念、理论说明等的内容……还须见企业级开发导师马大叔的为准:http://www.martinfowler.com/eaaCatalog/activeRecord.html 。上面权且为草草笔记。

原创粉丝点击