Delphi 2005 Eco II之王者归来
作者 陈省
新的开始
从borland收购模型驱动开发的领先厂商Bold,以及双向建模工具Together之后,borland开始整合两者的资源,于C#Builder和Delphi8中推出了强大的可视化MDA模型驱动的Enterprise Core Objects(以下简称ECO) OR Mapping企业应用开发框架,可以用来轻松地完成从可视化的平台无关UML模型创建=>自动平台相关模型转换=>代码模型双向同步及模型-DBSchema自动映射=>快速原型系统生成及运行这一系列复杂烦琐的工作。
虽然Eco平台从一开始推出就引起了人们的普遍关注,但是由于时间的关系,Eco 1.0仍然存在着很多问题。
l 首先,采用Eco创建企业应用,有一个很大的限制就是DBSchema完全由UML模型生成,我们只能对其做限度很小的调整,由此产生的问题就是对于一个现有的企业应用系统,我们无法把它整个或者部分平滑的移植到Eco平台上来。而完全采用Eco重造轮子,则风险太大,成本太高,这无疑限制Eco了的应用范围。
l 据统计,在.Net平台更多的是用来开发基于Asp.Net的B/S应用,但Eco 1.0对于多线程的并发性能不是很好,无法有效应用于B/S应用的开发。
l 其次,Eco 1.0可以很好的应用于传统的C/S开发,但是由于不支持.Net Remoting技术,无法适应越来越普遍的分布式三层开发的需要。
l 此外,Eco 1.0中体系架构相对封闭,没有提供很好的开放式接口,难于满足用户扩展的需要。
l 最后,Eco 1.0是一个全新的产品,因此产品质量也有很多的问题,比如基于Together的代码-模型双向同步技术的设计器就很不稳定,经常会将生成的代码和模型搞乱,这些小的瑕疵比较影响用户的使用体验。
事实上,Eco研发队伍也非常清楚用户反映的这些问题,因此当拿到Delphi 2005的时候,我欣喜的发现Eco II中实现了很多用户企盼已久的改变:
l 在Eco II中用户可以自己编写OR Mapping映射XML文件,实现模型同DBSchema的映射,用户除了可以手工编写映射文件外,Eco Surface Designer中提供了强大自动化的工具可以智能的分析DBSchema之间的外键关联,生成对应的模型定义。在Eco的Demos目录下,我们可以看到用这个工具可以轻松的将我们常用的Sql Server中的pub、NorthWind等示例数据库转换为对象模型。事实上,对于符合标准设计范式的DBSchema我们用这个工具基本就可以搞定了。
l Eco II中终于提供了开发基于Eco的Asp.Net WebForm和WebService应用的支持。
l 在Eco II中,引入了Bold For Win32中的对象空间同步技术。这使得用户可以开发出可伸缩的分布式应用。
l Eco对于整个体系架构进行了重构,新的架构更具扩展性,几乎Eco体系架构中的绝大部分都可以由用户进行扩展。同时,提供了许多的新的Service接口,如IExternalIdService等,可以用来实现查询对象主键等功能。
l 最后,Eco II的可视化设计器所使用的Together内核也由1.0升级到了2.0,不仅仅是稳定性大幅度提高,而且功能也有所增强。
自定义的OR Mapping映射
接下来,我们就来体验一下Eco II的强大智能Database模型转换工具的功能。首先,我们先来创建一个自定义的Sql Server数据库test,库中包含两张表,一个是Parent,它有两个字段ParentId和ParentName,其中ParentId是主键。另外Parent表对应于一个子表Child,它有三个字段,主键ChildId,外键ParentId和名称ChildName,建好的表的示意如下:

第二步是将数据库Test转换为UML模型。首先,创建一个C#版本的Eco WinForms Application,然后在Database Explorer中建立Test数据库连接的Connection对象,将其拖放到Eco Designer Surface创建Database Connection。然后添加一个persistenceMapperBdp组件,设定Connection属性为bdpConnection1,然后设定Mapper组件的配置为Sql Server。
第三步创建DBSchema对应的模型定义,在Eco Surface Designer中执行右健菜单命令Wrap Existing Database with Eco,将创建一个TestClasses UML Package,里面包含两个类Parent和Child类,同时它们的关联也被自动识别并建立。下图就是Eco自动生成的模型示意图:

在Eco自动创建类定义的同时,自动工具还将创建一个testMapping.xml,内容如下:
<Classes>
<ClassDef Name="Child">
<AliasDef Name="Child_1" Table="Child" ExtentRequiresDiscriminator="False" IsMainId="True">
<KeyImpl Name="EcoKey" IsAutoInc="False">
<KeyColumn Name="ChildId" />
</KeyImpl>
</AliasDef>
<KeyDef Name="EcoKey" Signature="System.Int32" IsId="True" KeyMapper="Attribute"/>
<AttributeDef Name="Id" Alias="Child_1" Columns="ChildId" AttributeMapper="" AllowNULL="False" Length="0" />
<AttributeDef Name="ChildName" Alias="Child_1" Columns="ChildName" AttributeMapper="" AllowNULL="False" Length="50" />
<SingleLinkDef Name="Parent" Alias="Child_1" Columns="ParentId" Key="Parent.EcoKey" IsConstrained="True"/>
</ClassDef>
<ClassDef Name="Parent">
<AliasDef Name="Parent_1" Table="Parent" ExtentRequiresDiscriminator="False" IsMainId="True">
<KeyImpl Name="EcoKey" IsAutoInc="False">
<KeyColumn Name="ParentId" />
</KeyImpl>
</AliasDef>
<KeyDef Name="EcoKey" Signature="System.Int32" IsId="True" KeyMapper="Attribute"/>
<AttributeDef Name="Id" Alias="Parent_1" Columns="ParentId" AttributeMapper="" AllowNULL="False" Length="0" />
<AttributeDef Name="ParentName" Alias="Parent_1" Columns="ParentName" AttributeMapper="" AllowNULL="False" Length="50" />
</ClassDef>
</Classes>
Xml文件中定义了对象的属性及其相互之间的关联,接下来新建一个FileMappingProvider组件,然后设定其FileName属性为testMapping.xml。最后,设定persistenceMapperBdp组件的RunTimeMappingProvider属性指定为FileMappingProvider组件,程序运行时,将使用testMapping.xml作为OR Mapping的映射依据。同时,别忘了设定testClasses.testClassesPackage作为运行时UML Package,否则默认是以向导创建的UML Package作为运行时Package,见下图:

最后,我们要创建一个可以编辑展示数据库表内容的界面,在主窗体上创建一个DataGrid,设定EcoAutoForm属性为True,这样Eco在运行时,会在用户双击DataGrid中的数据对象时,自动创建快速原型的编辑界面。建好了数据展示控件后,我们还需要添加一个ExpressionHandle组件,作为数据源,设定ExpressionHandle的RootHandle为向导创建的rhRoot,并设定rhRoot组件的EcoSpaceType属性为TestDB.TestDBEcoSpace。然后,设定ExpressionHandle的Expression属性为ocl查询语句“Parent.allInstancess”,表示显示所有的Parent元素,将ExpressionHandle设定为DataGrid的Datasource后,我们就可以运行程序,看看效果了(别忘了手工添加一些测试数据)。运行效果示意如下:

当双击DataGrid的指示箭头时,会弹出上图示意的Eco自动生成的编辑界面。可以看到新的数据库映射工具简单而且强大,如果默认的XML Schema不符合实际的情况,我们还可以手工修改 Mapping XML 的定义,实现更灵活的定制(具体可参考帮助文档)。
基于Eco的Asp.Net程序开发
下面我们就来创建一个最简单的留言簿Web程序,它可以让用户添加、删除和修改留言欣喜。
第一步调用Eco Asp.Net Web Application Form向导创建一个Eco Asp.net项目,起名为TestWeb。然后,使用Together可视化设计器创建信息模型,模型非常简单,只有两个字段GuestName, Message表示访客名称和消息内容,模型示意如下:

设计完模型,使用Eco Space Designer的右健命令Generate DBSchema来创建对应的数据库表。
设定数据访问层
开发Eco的WinForm程序时,我们需要在EcoSpace Designer中设定PersistenceMapperProvider和DBConnection的属性实现数据库访问层。但是在Eco Asp.Net项目中,数据访问层的设定不太一样。在Eco II中,我们现在可以创建多个Eco Space了,每一个Eco Space可以理解为一个对领域对象生命周期进行Session管理的对象,它负责维护数据库在内存中的缓存,确保实现正确的容错的数据更新,删除等动作,可以创建多个Eco Space的好处在于,由于Eco Space维护对象当前状态的缓存,每次加载对象实例时系统开销都会很大,对于Web程序来说,每次请求都创建一个Eco Space的话,会非常影响性能。因此在Eco II 中提供了对Eco Space实例的缓冲池管理,以及内容的进程内和进程外的同步机制。在Eco II中,PersistenceMapperProvider组件为修改为了线程安全并支持Remoting的组件,这使得它可以被多个进程内或进程外的Eco Space公用来实现缓存信息的同步。
因此,在Eco Space Designer中,我们要添加的一个persistenceMapperSharer组件,这个组件的用途是用来提供PersistenceMapperProvider的共享的。同时在Eco Asp.Net向导创建了一个EcoPersistenceMapperProvider组件,拖放PersistenceMapperBdp和bdpConnection组件到EcoPersistenceMapperProvider设计器上,除了要设定数据库连接外,还要设定PersistenceMapperBdp组件的SyncActive属性为True,这样Eco会在数据库操作时自动进行EcoSpace的同步处理。设定完成后事先编译一下。这时,persistenceMapperSharer组件的MapperProviderType属性的下拉列表中就出现了TestWeb.EcoPersistenceMapperProvider。
最后,我们还需要调整Web.Config中appSettings的设置,将MaxPool值由默认的0改成10,如下示意:
<appSettings>
<add key = "Borland.Eco.Web.MaxPool" value = "10" />
<add key = "Borland.Eco.Web.MaxAge" value = "600" />
</appSettings>
否则Eco Space的缓冲池不会被启动。
数据表示层设计
为了尽可能简化留言簿的设计,界面上只使用一个DataGrid来实现留言信息的列表展示,以及一个按钮用来添加留言信息。为了获取留言信息,还需要在界面上添加一个ExpressionHandle,设定它的RootHandle属性为向导默认创建的rhRoot,设定rhRoot的EcoSpaceType为TestWeb.TestWebEcoSpace(注意,该属性必须预编译一遍Eco程序才能被设定),设定ExpressionHandle的Expression属性为Ocl查询语句GuestMsg.allInstances,翻译成Sql 语句就是Select * from GuestMsg。然后,将DataGrid的DataSource属性指定为ExpressionHandle。
为了实现DataGrid中的原位编辑功能,使用DataGrid的Columns编辑器添加编辑、更新、取消和删除按钮列。为了实现原位编辑,我们还需要为DataGrid指定关键字段属性,但是我们前面在设计信息模型时虽然没有设计关键字段,但是Eco提供了外部的关键字段支持,设定ExpressionHandle的AddExternalId属性为True时,Eco会自动添加一个ExternalId的属性,它可以保证是唯一的,将DataGrid的DataKeyField属性设定为ExternalId属性即可。
接下来添加DataGrid的编辑操作事件CancelCommand, DeleteCommand等的处理代码,代码示意如下:
private void dataGrid1_CancelCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
//取消编辑
(source as DataGrid).EditItemIndex = -1;
DataBind();
}
private void dataGrid1_DeleteCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
//删除记录
DataGrid SourceGrid = source as DataGrid;
string Id = SourceGrid.DataKeys[e.Item.ItemIndex].ToString();
IObject ToDelete = ObjectForId(Id);
if (ToDelete!=null)
ToDelete.Delete();
UpdateDatabase();
DataBind();
}
private void dataGrid1_EditCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
//编辑操作
(source as DataGrid).EditItemIndex = e.Item.ItemIndex;
DataBind();
}
private void dataGrid1_UpdateCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
//更新操作
DataGrid SourceGrid = source as DataGrid;
string Id = SourceGrid.DataKeys[e.Item.ItemIndex].ToString();
SourceGrid.DataBind(); // To set DataSource
ElementHandle Eh = SourceGrid.DataSource as ElementHandle;
Object Row = Eh.RenderElement(ObjectForId(Id));
for (int colIndex = 0; colIndex< SourceGrid.Columns.Count;colIndex++)
{
if (!(SourceGrid.Columns[colIndex] is BoundColumn))
continue;
BoundColumn Col = (BoundColumn)SourceGrid.Columns[colIndex];
if (Col!=null && !Col.ReadOnly)
Eh.SetRenderedElementProperty(Row, Col.DataField, (e.Item.Cells[colIndex].Controls[0] as TextBox).Text, true);
}
UpdateDatabase();
SourceGrid.EditItemIndex = -1;
DataBind();
}
主要的业务逻辑就是获取ID,基于IObject接口或者ElementHandle的方法操作领域对象,执行UpdateDatabase()方法更新数据库,然后调用DataBind方法重新进行数据绑定,展现更新后的数据。需要说明的是在关键字段ID是通过ObjectForId方法来获取的,这个方法是由Eco 的向导自动创建的,代码很简单就是获取Eco Space的IExternalIdService接口来获取关键字段ID。
protected IObject ObjectForId(string id)
{
IExternalIdService idService = EcoSpace.GetEcoService(typeof(IExternalIdService)) as IExternalIdService;
return idService.ObjectForId(id);
}
最后,添加一个按钮,用来添加测试数据,代码如下:
private void btnTest_Click(object sender, System.EventArgs e)
{
//生成测试数据
//GuestMsg Msg=new
GuestMsg Msg=new GuestMsg(EcoSpace);
Msg.GuestName="哈巴狗";
Msg.Message="测试";
UpdateDatabase();
DataBind();
}
同时开两个TestWeb程序,先对一条记录进行修改,将消息信息改成其它值,更新后,再用另一个实例进行原位编辑时,可以看到更新后的值被自动同步到了编辑框中,表明Eco Space可以对数据库操作进行正确的同步。运行示意图如下:

可灵活扩展的Eco框架
EcoII中,Borland的R&D Team对体系结构进行了重构,几乎Eco框架中每一个组成部分都可以由用户自己编写的组件进行替换,比如我们可以自己实现一个高效的对象缓冲机制,对默认的Eco Cache实现进行替换。在\Demos\ CSharp\ECO\ChangeLogging目录下提供了一个通过自定义的链式缓存类实现对象属性变更审计的功能,方法非常简单,就是在EcoSpace的初始化方法中将标准的Cache对象换成自定义的Cache对象
public ChangeTrackingCache m_ChangeTrackingCache = new ChangeTrackingCache();
public override void Initialize()
{
base.Initialize();
m_ChangeTrackingCache.NextCache = FrontsidePolicy.Cache;
FrontsidePolicy.Cache = m_ChangeTrackingCache;
}
同时自定义的Cache对象中,重载SetCurrentValue,当对象属性被变更时,激发MemberChanged代理事件,实现属性变更审计。
public class ChangeTrackingCache: ChainedCacheBase
{
public event ChangeDelegate MemberChanged;
public override void SetCurrentValue(Locator locator, int memberIndex, object value)
{
IClass c = NextCache.GetUmlClass(locator);
IStructuralFeature f = c.EcoClass.AllStructuralFeatures[memberIndex];
object oldValue = NextCache.GetCurrentValue(locator, memberIndex);
string message = "Member " + c.Name + "." + f.Name +
" changes value from " + oldValue.ToString() + " to " + value.ToString();
MemberChanged(message);
NextCache.SetCurrentValue(locator, memberIndex, value);
}
}
增强的服务接口
在Eco II中,又提供了很多新的接口服务,比如上面提到的IExternalIdService,其它如ITypeSystemService, IVariableFactoryService,在Delpphi Eco Demos目录下有一个Services的项目,演示了绝大部分的Eco的服务接口的使用方法,如使用ITypeSystemService接口获取UML元数据。
改进的Eco可视化设计器
这一版本中不仅是Together的可视化设计器的稳定性有了极大的提高,EcoSpace的设计器也有很多体贴的设计,比如PersistenceMapperProvider组件的配置是一个比较烦琐的事情,为了防止用户设错属性,Eco Space Designer中专门为PersistenceMapperProvider组件提供了一个提示ToolTip,可以告诉用户已经做了哪些设置,还需要做哪些设置。

另外,生成数据库Schema的对话框也有了改进,可以让用户选择更新哪些表,删除哪些表,虽然功能不复杂,但是却可以节省用户的大量时间,并减低出错的可能。

此外,Eco Space Designer还提供了编写插件进行扩展的接口,在CSharp的Eco Demos目录下有一个EcoSpaceDesignerPlugIn的示例项目,演示了如何获取当前UML模型的元数据,并由此生成描述性的HTML文件。
总结
从Delphi 2005 Eco II的表现来看,Eco 开发框架同现有各种OR Mapping框架相比,功能上已领先了很多,同时稳定性和操作简便性也另我刮目相看,在这一领域已经成长为一个重量级选手,相信必然会在未来企业应用领域有着越来越广泛的应用。