Dorado 7 : 05. 实做主从维护界面(SEFC)

在前面实做单表CRUD的过程中,我们并没有使用的立体数据模型,不过在这个过程中我们对基于数据模型的开发有了一个初步的了解,下面我们基于一个主从表维护的工作来实际体验立体数据模型的开发过程。

访问如下的URL:  http://www.bsdn.org/projects/dorado7/deploy/sample-center/com.bstek.dorado.sample.data.MasterDetail.d

我们可以在浏览器中看到如下的视图:

这是一个主从表的维护界面,主表显示产品类别,从表表示对应产品类别的产品列表。当我们选择上面这个Grid的不同产品类别的时候,下面Grid会自动的显示相对产品类别的产品列表。如果我们用过Dorado5技术,就会知道做这么一个页面并不复杂,我们只要在页面上定义两个DataSet对象,并采用主从关系的MasterLink技术做好关联设定,就可以开发出这个页面。而在Dorado7中我们只要采用一个DataSet就可以完成开发,从技术上来说我们可以认为它就是一个立体数据,第一层是产品分类,每一个产品分类下有不同的产品。

下面我们根据SampleCenter中的范例了解立体数据的开发技术,首先打开MasterDetail.view.xml:

首先我们关注DataType的定义,其中的parent属性,在该处被定义为:"global:Category",parent属性是告诉Dorado该DataType的继承关系,其中global关键字是表明这个parent的DataType是一个全局的DataType。否则如果我们parnet属性只配置为"Category",它就会认为这是一个私有的DataType。全局DataType都是定义在系统默认的models下,我们找到这个全局的DataType:

在视图中我们可以看到Category的定义,在本例中为了说明DataType支持继承而专门做了复杂的继承关系(实际上并不一定需要设计这种继承关系),其中Category继承BaseCategory,BaseCategory继承CommonEntity,在本例中CommonEntity的作用是申明一个名称为id的propertyDef,并设置了其required属性为true,这样在继承的BaseCategory,Category等DataType中就不需要关心ID的设置,自动继承了这个特性:

前面我们说过,页面的逻辑是选择不同的产品分类,下面的Grid显示不同的产品列表,对于DomainObject来讲这是一个引用关系,是一个立体结构的数据。我们在DomainObject的定义中就能看到这个引用关系:

package com.bstek.dorado.sample.entity;

@Entity
@Table(name = "CATEGORIES")
public class Category implements Serializable {
	private static final long serialVersionUID = 6076304611179489256L;

	private long id;
	private Category parent;
	private String categoryName;
	private String description;
	private Collection<Category> categories;
	private Collection<Product> products;

	...省略
	@OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
	@JoinColumn(name = "CATEGORY_ID")
	public Collection<Product> getProducts() {
		return products;
	}

	public void setProducts(Collection<Product> products) {
		this.products = products;
	}
}

其中的products就是Category的产品列表。那么我们就容易理解Category中的中名称为products的PropertyDef的定义了,此处的products中我们注意到未设定dataType属性,这是用到了DataType的一个特性,这个特性我们在DataType的说明文档中的其他特性中提到DataType与JavaBean的一一映射关系,而在Demo.model.xml中定义了名称为Product的DataType对象:

可以看到Product对象设定了matchType,这样com.bstek.dorado.sample.entity.Product就与Product这个全局的DataType建立了一一对应的映射关系,在Category中虽然我们没有定义products的DataType属性,但Category对象根据自身的引用关系:

private Collection<Product> products;

知道这是一个Product的集合,那么根据前面的一一映射关系,就很容易的知道这个默认的DataType为Demo.model.xml中定义的那个全局的Product。

为什么不将这个products定义在全局的Category中呢?

如果没有特殊的必要,不要在全局的Model文件中建立对象之间的关系,而是应该在自身的View中去建立,为什么呢?这是因为如果我们在Model中就直接建立了这个关联关系,但是我们不知道最终有多少个视图会需要这种关系,这样导致任何一个视图引用这个全局DataType的时候都得到一个立体的数据对象,而这对很多视图来说可能并不是必须的。另外这种立体视图也可能会导致持久层不必要的数据加载从而性能损耗。

了解了DataType的基本设定之后,我们再来看DataSet的设定,如下图:

有了前面单表CRUD的了解,对其中dataType和dataProvider的设定都比较熟悉了,根据dataProvider的设定,也很容易的可以找到对应的Java代码:

package com.bstek.dorado.sample.interceptor;

@Component
public class CategoryInterceptor {

	@Resource
	private CategoryDao categoryDao;

	@DataProvider
	public Collection<Category> getAll() {
		return categoryDao.getAll();
	}
...省略

其中的getAll方法用以向外返回一个产品分类列表。应该注意到这儿我们没有再返回product对象,这是因为我们以及在DataType中添加的products属性了,这样Dorado试图解析Category数据的时候就会自动的读取products属性,同时我们注意的Category.java对products的装载设定:

@OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
@JoinColumn(name = "CATEGORY_ID")
public Collection<Product> getProducts() {
	return products;
}

这儿采用了LAZY的装载方式,一旦试图读取category的products属性的时候,hibernate就会帮助我们自动的装载对应的产品列表。

好了到目前为止我们已经完成了最重要的工作:通过定义一个DataSet我们可以拿到一个立体数据模型的Category列表了。接下来的工作就比较简单了,只要在页面上放置一些数据敏感控件,把它关联到立体数据模型上的不同节点下就可以。下面我们来了解一下数据敏感控件的使用,我们来看一下View的定义,在SplitPanel控件中我们看到有两个Grid,分别用来展示产品分类和产品列表,其中展示Category的Grid的定义比较简单,我们已经比较熟悉了,设定一下dataSet属性就可以:

我们主要关注Product的Grid的定义:

这个Grid除了指定dataSet属性之外,还定义了dataPath属性"#.products"。这个表达式的含义是:"#":表示当前记录,连起来的意思是"当前Category记录下的products"。
当前记录是什么意思呢?
就是当我们在Category那个Grid选择不同行的时候,当前行的背景色会变绿,选中的这一行就是当前记录,当我们在浏览中选择不同的行操作时,这个#代表的记录会实时的变化,而这样"#.products"就代表了当前选中行的产品列表,它也是动态变化的。好了这就是这两个Grid的重要属性设定。

通过这个例子我们了解了立体数据模型的使用,另外我们还接触到了一个新的技术:DataPath,这个我们将在下一章中再详细介绍。

性能优化

刚才的范例,如果我们分析其HTTP请求,通过Chrome的F12打开Developer Tools,并重新刷新页面后,查看Developer Tools中的AJAX请求,找到其中view-service的AJAX请求,并切换标签也到Response:

仔细分析Response的代码:

<?xml version="1.0" encoding="UTF-8"?>
<result>
<request>
<response type="json"><![CDATA[
{
	"data":[
		{
			"id":1,
			"categoryName":"Beverages",
			"products":[
				{
					"id":1,
					"productName":"Chai",
					"categoryId":1,
					"discontinued":false,
					"quantityPerUnit":"10 boxes x 20 bags",
					"reorderLevel":12,
					"unitPrice":18.0,
					"unitsInStock":39,
					"unitsOnOrder":0
				},
				{
					"id":2,
					...
				},
				...省略多个Product
			],
			"description":"Soft drinks, coffees, teas, beers, and ales"
		},
		{
			"id":2,
			"categoryName":"Condiments",
			"products":[
				{
					"id":3,
					"productName":"Aniseed Syrup",
					"categoryId":2,
					"discontinued":false,
					"quantityPerUnit":"12 - 550 ml bottles",
					"reorderLevel":66,
					"unitPrice":10.0,
					"unitsInStock":13,
					"unitsOnOrder":70
				},
				{
					"id":4,
					...
				},
				...省略多个Product
			],
			"description":"Sweet and savory sauces, relishes, spreads, and seasonings"
		},
		...省略多个Category
	],
	"$dataTypeDefinitions":[
	],
	"$context":{
	}
}
]]></response>
</request>
</result>

简单的阅读一下代码,我们不难发现这个AJAX请求将立体数据模型中的产品分类和产品列表的所有信息都下载到客户端了。那么这儿存在一个问题,如果产品分类特别多,比如1000个产品分类,同时每个产品分类下有1000个产品,如果按这种机制处理的话这一个AJAX请求会产生100W个产品信息的数据下载,这是不可想象的。虽然我们列举了一种比较极端的情况,但这也说明了一个问题,对于立体数据模型的下载,我们还是有性能上的考虑的,这也对软件开发人员提出了要求:就是开发的初期软件开发人员要大概预期可能的数据量,再匹配相对合理的开发模式,如该分页的时候分页,该懒加载的时候懒加载。下面我们看针对本例中的功能实现懒加载实现的一个处理,在SampleCenter中已经实现了这个范例,我们大概预览一下这个页面:
页面链接:http://www.bsdn.org/projects/dorado7/deploy/sample-center/com.bstek.dorado.sample.data.MasterDetailLazy.d
页面效果:

界面效果完全一样,唯一不同之处是在我们选择不同的Category的时候,可以在界面的右上角看到一个提示框。当我们切换不同的Category的时候都可以看到这么一个提示框,很容易就看出这就是懒加载的一种效果,多次单击不同的Category后,我们再将浏览器切换到Developer Tools中就可以看到很多的View Service请求,打开其中的一个查看Response信息:

分析其中的数据不难发现,现在每一次都只是下载当前Category对应的产品列表。通过这种懒加载处理机制就能很好的解决我们之前说的1000个产品分类,每个产品分类有1000个产品的性能问题。

下面再来看看实现懒加载的开发与之前有什么不同之处,首先打开对应的视图配置文件:MasterDetailLazy.view.xml

其不同之处在于其中Category这个DataType下的products为橙色,而原来的MasterDetail.view.xml下为绿色:

他们之间的差别是什么呢?我们选择Category这个DataType,注意看IDE中的工具栏,其中DataType下可以添加三个元素:PropertyDef, Lookup, Reference


上图我们看到的绿色的代表PropertyDef, 橙色的代表Reference。
接下来我们就介绍这两种对象之间的差别。

PropertyDef可以认为就是一个bean的引用,但是Reference相对来说就是一种松散的关系,它不要求Bean内部包含这种逻辑关联关系,而可以直接在View中的Dorado层面建立这种关系。我们来看看Reference的基本属性设定:

其中可以定义dataProvider属性,由于Reference不要求通过Bean本身的引用建立关系,可以直接利用Reference建立两个Bean之间的关联。它允许通过Reference本身定义当前属性的数据来源。数据的获取是通过DataProvider得到的,这个dataProvider属性我们已经很熟了,我们找到相关的Java方法:

package com.bstek.dorado.sample.interceptor;

@Component
public class ProductInterceptor {
	..省略
	
	@Resource
	private ProductDao productDao;

	@DataProvider
	public Collection<Product> getProductsByCategoryId(Long parameter) {
		return productDao.find("from Product where category.id=" + parameter);
	}
	..省略
}

这个方法通过一个category的id获取对应产品分类中的产品列表。那么其中的parameter参数怎么传进来的呢,我们看一下Reference中的parameter属性设定:"$${this.id}"这个双$的表达式,我们之前在EL表达式介绍的时候说明过,这时使用时动态计算的一种表达式,每一次使用都会被重新计算。这种表达式是在浏览器端执行的,因此其中的this就比较容易理解了,就是指当前使用中的实体对象(Entity),在本例就是当前的Category。这个参数的整体含义就是获取当前Category的id属性作为Reference的parameter参数的值。在范例中,当每一次我们将当前的Category切换为别的Category的时候,当前Category对象都会发生变化,也就是说this所指的实体对象就会发生变化,由于采用的是动态表达式,这样绑定产品列表的Grid在每一次Category发生变化的时候都会尝试着访问其中的products属性,由于products是Reference类型,它就会激活其中的dataProvider机制获取数据,并返回自身的parameter属性,获取parameter属性的时候就会激活动态EL表达式的计算规则,得到当前Category的id,并作为DataProvider的参数发出获取数据的请求。这样我们之前在ProductInterceptor.java中的getProductsByCategoryId方法就能得到对应的categoryId的值了。

Reference这个对象是懒装载的,它只是把这个关系告诉浏览器,只有在浏览器中我们试图访问Reference的时候才会触发数据加载工作,并通过一系列的机制调用到DataProvider获取相关的数据。通过这种处理机制,我们可以提高某些类型页面的性能,并极大的降低网络传输的数据量。