摘要
关系数据库访问层可以帮助您设计使用关系型数据库的应用程序,并且在业务对象级别反映关系运算,这种应用程序被称作数据驱动或representational[Mar95]。这种系统并不需要是面向对象的,您完全可以使用第三代语言(3GL)来实现,因此,本文的模式语言将忽略映射继承和多态的特性。
本文包含框架模式和几个关键的实现模式。PLoP 学报[Kel+96b]包含了完整的模式语言,包括适配器和优化模式。
简介
很多业务信息系统拥有一个简单的数据模型,即使他们可能会有30或更多个实体表,但是很少发现继承或者复杂的关联。对于复杂的情况,通常是封装在应用程序的核心。用关系运算来为这些系统建模是一个很好的主意。
来看看下图1,从一个订单管理系统中摘录的片断,在左上角,有处理发货单的实体,这个片断遵循第三范式(3NF),在因数分解级别,数据分析家经常这么用。假设你使用这种逻辑数据模型来定义你的物理数据库表,系统当然可以正常工作,但是性能却是不敢恭维。
剖析这个系统,你将发现很多多余的数据库操作,同时,也会发现数据库操作过于缓慢的原因是大量的表连接或者是不必要地移动大量数据。为了提高性能,你可以在物理数据模型中不用太过范式化。对数据库内容的统计分析表明90%的订单拥有不超过5个订单项(OrderItems),因此可以将前5个OrderItems放在Order表中,为包含剩余10%,可以创建一个OrderItemOverflow表,如图1右上角所示,此外,你还可以将物品的属性ArtPrice和ArtName集成到Order表中。这样,最后的数据库设计使得访问两个表,一个Order表,一个Customer表,就可以读取90%的发货单,而其他的连接都可以去掉了。
图1:订单处理系统的部分
现在假设你在应用程序的核心代码中嵌入了SQL语句,为适应新的表结构,你将不得不重写其中的某个部分,此外,处理这些溢出表(OverflowTable),将使SQL代码暴露出来,最坏的是,你还不得不在每次数据库结构更新时重复这些过程。
模式语言图
关系数据库访问层框架模式定义了它的部件的角色和职责,同时也阐述了三个关键抽象,层次视图、物理视图和查询代理。
图2:该模式语言的映射图。白框中的模式表示在本文描述的模式,灰框中的模式只在PLoP 学报中描述[Kel+96b]。
数据库访问层的相关工作
这个框架为那些将数据库以关系型方式使用的应用程序提供了一个数据库视图接口,其它的持久化框架和模式语言都是关于对象的持久化,例如对象-关系的访问层[Bro+96, Col+96, Kel+96a],它使用一个关系型数据库;还有对象访问层,它使用一个面向对象数据库[Col97]。图3显示了这些不同访问层的区别。
图3:三种不同的数据库访问层
符号约定
我们使用OMT[Rum+91]来表示对象图,带下划线的其他模式参考一个相关的模式。如果一个模式参考后面跟着一个引用,例如[GOF95],你可以从相应参考论文中找到它。
框架模式
模式:关系数据库访问层
环境
你如果正在编写一个类似上面订单处理系统的业务信息系统,关系型运算比较适合这个域逻辑,最后的数据模型非常简单,很少使用继承,而将关系模型映射到面向对象表示的工作巨大,得不偿失。
问题
如何访问关系型数据库?
先决条件
Ÿ
业务分离与成本:数据库编程很复杂,应用逻辑也是如此,都需要有灵活的解决方案。将它们混合起来的复杂程度要高于它们两者复杂度简单的组合。最容易的方法是将应用编程和数据库编程分离开,两者都更易于实现和测试。另一方面,引入新的层,将增加类的数量,并加重设计和实现的工作,需要化大力气在提高维护性和性能调优上。
Ÿ
易用性与功能:如果你决定要封装数据库,最后的接口必须易于使用。而另一方面,数据库接口的复杂度将影响它的功能,因此,接口的封装既要易于使用,功能也要足够强大以满足你的项目。
Ÿ
性能:要想业务信息系统达到一个可接受的性能,数据库优化是关键的。因为数据库比一个主处理过程要慢几个数量级,调优的操作将集中在数据库访问上。调优是一个迭代的过程,为优化数据库访问,你可以改变存储的物理参数,以及表的分布或访问数据库的API。
Ÿ
灵活性与复杂性:因为数据库调优非常关键,需要对数据库有一层封装,允许在应用核心不变的情况下,频繁改变底层的数据模型。因此,系统的灵活性越高,复杂性就越大。
Ÿ
旧系统与优化设计:很少从零开始设计一个业务信息系统,而是需要联接到一个旧系统,并且根本就不允许碰它。通常你不可能替代所有的遗留代码,因为这种方式有很高的风险并代价不菲。但是,遗留数据的结构很少符合你的需要――如果还有结构的话?你也许不得不将不同年代的数据库技术桥接起来。为保证应用的可维护性,你不得不封装遗留的访问接口,这在尤其在重建工程中是一个强烈的先决条件。
解决方案
使用一个分层的架构,包含两层。逻辑访问层提供稳定的应用核心接口,而物理访问层访问数据库系统,后者可以适应修改性能的需要,在两者之间使用一个查询代理将他们耦合起来。
结构
图4显示了关系数据库访问层的类。逻辑访问层提供缓冲和事务管理的类,物理访问层提供访问数据库系统的接口。后者细分成表示数据访问地物理视图和Database类,Database类用来封装数据库管理的调用。这两层可以直接硬连接,或采取更好的方式——用一个查询代理居于逻辑和物理访问层之间。
图4:关系数据库访问层框架的结构。客户端只访问逻辑访问层,而逻辑访问层使用物理访问层联接到数据库。
参与角色
事务(Transcation)
Ÿ
提供一个允许事务开始、提交和回滚的接口;
Ÿ
它在每个事务开始前创建,并在提交或回滚后销毁。这种用法类似于定义在[ODMG96, chapter2.8]中的事务对象。
视图工厂(View Factory)
Ÿ
转送由键值标识的数据。它提供createView()和getView()方法,分别来创建新视图和激活已有视图。客户端只有通过这两个方法才能得到视图的引用;
Ÿ
使用一个视图缓冲(View Cache)[Kel+96b]来避免创建在当前事务环境中已经存在的视图;
Ÿ
允许标识ConcreteViews的判定,用一个抽象的键值类ViewKey为所有的键提供标准接口;
Ÿ
它是一个Singleton对象[GOF95]。
视图缓冲(ViewCache)
Ÿ
避免数据装入两次,它是一个键值索引的视图包容器,形成访问层的缓冲;
Ÿ
提供writeAndFlush方法,可以将所有修改过的视图使用write2DB方法写入数据库。事务对象在提交时,会调用writeAndFlush,而中止事务将调用flush()来清空ViewCache。
具体视图(ConcreteView)
Ÿ
它是逻辑数据库模型的层次视图(Hierarchical View),有若干个具体视图类。每个类适应于应用核心某个用例,ConcreteView的成员的类型是应用程序数据类型,而非数据库类型。
Ÿ
知道如何通过具体物理视图(ConcretePhysicalViews)将它们自己写入数据库。具体视图将监视它的内部状态,如果它从视图缓冲接收到一个write2DB消息,将调用私有的update()、insert()或delete()方法。这些方法中即可以强制编码调用一个相应的具体物理视图,也可以调用一个查询代理(Query Broker)。
视图(View)
Ÿ
为具体视图(ConcreteView)定义抽象的协议,参见层次视图模式;
Ÿ
提供一个markModified()方法,在当前事务提交时触发数据库更新;
Ÿ
提供requestDelete()方法,在事务结束时触发数据库删除。不要将这个方法和析构函数混淆起来。析构函数只是将对象从内存清除,而requestDelete()是在事务结束后将对象从数据库(同时也从内存中)删除。为了避免悬空引用,requestDelete()方法应该是唯一删除数据库记录的方法。
物理视图(PhysicalView)
Ÿ
为具体物理视图(ConcretePhysicalViews)定义统一的协议;
Ÿ
捆绑数据库访问函数,封装数据库行为并将数据库错误码转换成应用级别错误码;
具体物理视图(ConcretePhysicalView)
Ÿ
包装一个物理数据表。它也可以包装数据库视图,如果数据库不支持视图的直接更新,具体物理视图还要提供适当的写命令;
Ÿ
包装数据库优化。如果使用非范式化、受控冗余或溢出表,这种情况下,具体物理视图可以映射多个表;
Ÿ
可以从元数据信息生成,如数据库的表结构。
数据库(Database)
Ÿ
封装数据库管理系统。提供开始数据库联接的方法,以及处理数据库命令,接收结果集等方法。
动态行为
我们将讨论这个模式的动态行为,实现框架的不同方面。
实现
Ÿ
对待大量更新(Mess Update)。大量更新语句的形式诸如“update..where”,使用一条查询操纵一组记录。很难将这些语句集成到视图缓冲中。合并大量更新的意思是:读入大量数据到视图缓冲,单条操纵记录并在写回数据库时,每次写一条,这个方法比直接在数据库处理要慢的多。
Ÿ
批处理需要特别对待。有一些模式描述批处理方式访问数据库,不过还有待挖掘;
Ÿ
多重查询:我们忽略了多重查询,可以在短视图(ShortView)和窄视图(Narrow View)中找到更多信息。
Ÿ
游标稳定性:将大量数据读取操作提交到BFIM(前映像)一致性检查是具有理论可行性的。这将提供二级事务一致性(游标稳定性[Gra+93]),而非一级(浏览一致性)。大量数据读取通常用来填充列表框(参见短视图)。它们具有诸如“select <fileds> from … where”的形式。在提交时检查它们的一致性意味着在事务中,重新读取所有已读记录,并和它们先前的映象比较。如果有一条记录不同,将不得不终止事务。这不仅仅对性能是一个严重的威胁,同时对一致性也没有什么价值。大多数情况下,用来填充列表框的记录不会扮演破坏任务一致性的角色。因此,在事务中,对于不参与计算的数据,通常使用浏览一致性就足够了。
Ÿ
具体物理视图和动态SQL:如果数据库系统支持动态SQL而没有什么运行时的负担,你可以跳过具体物理视图而使用查询代理来生成相应的SQL语句。静态的SQL由具体物理视图提供。
Ÿ
数据库联接尽可能长时间地保留。为每个事务建立一个新联接将导致槽糕的性能。
Ÿ
在这个架构下,强烈建议不使用触发器和包含业务逻辑的存储过程。一个视图缓冲将无法被通知到数据库中自治的变化,因此存储过程可能会导致缓冲的一致性问题。同样,触发器也有类似问题。因为它们工作于物理数据模型,很难转换到应用核心的逻辑层,不过也可以为了提高数据访问速度(见物理视图)使用受限的存储过程。
结论
Ÿ
业务分离:访问层对事务、数据库访问、缓冲形成了封装良好的子系统,应用核心使用逻辑层接口,无需对数据库的访问有了解。
Ÿ
工作强度:实现一个关系数据库访问层根据包含的特性不同,需要0.5到35个人年,使用生成器和依赖硬编码比构建维护工具代价和查询代理的代价要低,在你决定不同的选择前,充分考虑预期的变化、市场推广和软件的生命周期。
Ÿ
易用:这个访问层并没有将关系模型转换到一个面向对象视图,因此应用核心必须工作于关系视图上。你应该仔细考虑是否这种数据驱动的方式适合你的应用逻辑,看看是否你的工程更适合使用对象-关系的访问层。当应用核心确实需要一个隐式的映射是,避免构建一个更复杂的访问层并不是一个好的想法。
Ÿ
灵活性:如果你使用一个查询代理,你可以通过增加新的具体物理视图来维护并调优数据库,而不用修改应用核心代码。在底层物理数据库改变时,应用代码保持稳定。
Ÿ
复杂性:这个访问层包含最简单的类。查询代理是成本最高的部分,因为它包含一个复杂的树匹配算法。忽略它可以导致一个简单的适配器层,但是灵活性降低了。
Ÿ
性能:你要为映射付出一点运行时性能的牺牲,不过访问层可以使用缓冲来优化并易于调优,它将使用快速的处理器周期,而避免缓慢的IO处理。
Ÿ
遗留数据:你也许会使用访问层来降低现有应用的物理、逻辑数据模型间的耦合,这对重构遗留系统非常有用。首先,在代码中插入一个数据访问层,这是一个具有可控风险的单一步骤,接着,开始在不同的项目中重写数据库和应用核心。
变种
Ÿ
舍去视图缓冲:如果你不需要长事务,可以舍去视图缓冲。这对简单的对话框系统是可行的,这种系统在每个事务中一般之处理一条记录。但是如果应用核心有一些影响多条记录的事务时,就应该使用视图缓冲。
Ÿ
对于在事务监控器之上实现用户事务,一个缓冲机制是非常自然的选择,例如IMS、CICS或UTM。当用户事务包含多步对话框才完成,事务监控器为每一步开始一个新的事务,使用视图缓冲可以让你收集到一个用户事务中所有的写数据库操作,接着,它们可以作为一个技术上实际的事务执行,可以对这些多步的对话框操作保持事务一致性。
Ÿ
使用非关系型数据库:物理访问层可以封装非关系型数据库和文件格式,例如IMS-DB,CODASYL或VSAM。甚至可以将它改写到若干不同的数据库技术,隐藏对遗留数据的访问。
已知应用
VAA数据管理器规范使用这个模式,并带有元数据编辑器和对层次数据库系统复杂的映射[VAA95]。VAA数据管理器是从Württembergische Versicherung[Würt96]的数据管理器派生而来的。
Denert在[Den91,pp.230-239]中简单描述了这个模式语言的一些基本思想,sd&m的许多项目都使用这个模式的各种变种,包括Thyssen、Deutshche Bahn和HYPO银行[Kel+96a]。
CORBA持久对象服务(POS)[Ses96]指定持久对象使用一个代理(持久对象管理器)来将数据写入任意数据存储中(持久数据服务)。
相关模式
这个模式一个分层(Layer)的应用[Bus+96,pp.31]。视图工厂一个抽象构造器[Lan96]的应用。
[Bro+96]和[Col+96]阐述了如何扩展这个模式,向应用核心提供面向对象的视图。Brown和Whitenack[Bro+96]使用一个代理来降低层之间的耦合,而[Col+96]描述了一种直接连接的方法。
一些实现模式
模式:层次视图
示例
考虑图5中,订单处理系统的细节。其中的发货单可能具有右图所描述的情况,注意这个发货单有两级间接的层次结构。一个用例可能就是从一个订单号开始并浏览它各种订单项和它们的物品。
图5:我们订单处理系统数据模型详细。左边用第三范式表示的 ER图,右边是发货单的结构,由这些实体构成。
环境
你已经决定使用关系数据库访问层来降低物理数据库和应用核心的逻辑数据模型之间的耦合。
问题
数据库访问层向应用核心提供什么接口?
先决条件
Ÿ
复杂度与易用性和开发成本:接口应该简单易用而且要有足够的功能来满足必要的数据库操作。因为应用核心反映域问题,你不会想用数据库特定的代码来打乱它,所以这个接口应该在某种程度上反映适当域级别的数据抽象。提供一个包含所用数据库功能的接口意味着重新实现大部分数据库管理系统,这个代价太高了,所以,SQL风格的接口不大可行。
Ÿ
灵活性与开发速度:在数据库访问时,使用逻辑数据模型作为指导对于短期基本功能实现可能是最简单的解决方案。但是,性能调优和维护迫使你要经常改动物理数据库的布局,同时我们也不想改变应用核心,所以我们需要一种和物理数据模型无关的接口。
Ÿ
性能:理论上,第三范式对于关系运算是最好的,但是在物理模型中用它将导致很差的性能。
Ÿ
大量问题:一个大型的数据模型包含上百甚至更多的实体,手工地为数百个实体编写包装函数或嵌入的SQL代码是一个无聊而又代价昂贵的任务,枯燥的工作还总是伴随错误发生。一般的解决方法是在数据库编程中使用宏、生成器或模板。
解决方案
根据域问题空间来表示接口,那就成为一个关系数据模型。从数据模型的一点(或实体)开始,并使用外键关系来行走到其他感兴趣的点。在行走过程中形成形成了一个有向无环图(DAG),每个节点标记上实体名,相关属性和选择谓词,每条边标记上所使用的外键以及它的粒度(一对一或一对多)。
结构
图6显示了相当于左边发货单的图表示
要将这个图转换成一个层次视图,从View派生一个ConcreteView,这个ConcreteView是DAG的根,再为任何一个节点定义一个域级别的类,使用聚集来实现图中to one的关系,使用包容器来实现图中to many的边。一个ConcreteView通过这种方式构建,并填充以来自ConcretePlysicalView域级别属性,适当的ConcretePhysicalView可以使用硬编码方式调用或使用查询代理。
数据库访问层应该能够对所有ConcreteViews统一对待,因此,View定义了它们共通的接口去访问访问层中其他的类。
实现
你可以使用文本文件或者一个特定的工具[Wurt96]来定义ConcreteViews的结构,这允许为静态类型语言自动生成ConcreteViews类,而对于动态类型语言可以进行运行时定义。
结论
Ÿ
继承和多态性:访问层没有内嵌继承和多态性的处理,看看这是否适合你的问题域。
Ÿ
接口复杂性:这个接口是最小化的,因为它仅提供应用核心所需的基本特性。然而,你不得不花点努力在生成器或模板上,这在调优是会对你有所回报,但同时,在你达到真正目的前,需要经过几个维护周期。一旦你完成了生成器,那么你定义新的ConcreteViews将会是几分钟的事情。
Ÿ
接口风格:一个使用层次视图的应用程序依据逻辑数据模型的结构,逻辑数据模型决定了使用它的代码的结构。而对于一个对象-关系访问层中,对象模型依照域的内部结构。
Ÿ
易用性和应用核心的需求:层次视图只反应特定域逻辑,它们是用例中所需确实支持的,因为层次视图封装了数据库特定功能,所以调用访问层是非常简单的。
Ÿ
性能和灵活性:层次模型完全降低了应用核心和物理数据模型的耦合度,这允许你任意调整数据库而无需影响应用核心代码。最后的性能总体所得要高于引入层次视图这一间接层的性能损失。
Ÿ
大量问题:最坏的情况是每个用例都需要一个或更多的ConcreteView,这会导致大量的类产生:大量代码,大量符号表等等。不过,使用模板、宏或代码生成器,ConcreteViews是足够一般的类。
示例方案
程序段1显示了发货单示例的声明,程序段2包含处理发货单的代码。
struct
Customer {
CustomerKeyType
iCustNumber;
...
// 逻辑数据模型种客户其他属性
};
struct
Article {
ArticleNumberType
iArticleNumber;
...
// 其他属性
};
struct
OrderItem {
Article
iArticle;
QuantityType
iQuantity;
};
class
OrderInvoiceView : public View {
public:
OrderKeyType
iOrder;
Customer
iCustomer;
Vector<OrderItem> iItems; // 其他包容器也可以
Money
iSumOfInvoice;
private:
//
私有方法用以读取和写入数据
//
到PhysicalView
virtual
void update ( void );
virtual
void insert ( void );
virtual
void remove ( void );
virtual
void read ( void );
};
程序段2的代码和数据库无关,它是依据逻辑数据模型的,非范式化的物理数据模型对于应用代码是不可见的。这里只有两行代码涉及到持久化:ViewFactory::getView()命令从访问层得到数据。pos->markModified()方法为SumOfInvoice作上标记,以让它写回数据库。
void
Order::processInvoice ( OrderKeyType anOrder){
//
从数据库得到数据,我们只指定主键,剩下的让访问层去做
OrderInvoiceView * pInvoice
=
( OrderInvoiceView*)ViewFactory::getView( anOrder );
//
处理发货单项
ItemIterator itemIter = pInvoice->iItems.begin();
for(;
itemIter != iItems.end(); itemIter ++){
itemIter->iSumOfInvoice +=
(itemIter->iQuantity *
itemIter->iArticle.iArticlePrice);
}
//
视图改变了,作标记
pInvoice->markModified();
}
程序段2:processInvoice的实现。这个例子演示了订单中订单项的遍历和所有项目iSumOfInvoice属性的求和。注意,我们遍历了逻辑数据模型中的两个间接层。为简便起见,我们舍去包围在Order::processInvoice外面的事务处理和一些明显的类型定义。
变种
Ÿ
许多应用大多是一些简单用例的集合,它们只需要单一级别的间接(就像实体和它依赖的实体)。这种情况,ConcretePhysicalViews封装了数据库访问代码并向应用提供足够的简洁接口,省掉查询代理和层次视图。不过这对那些可能设计到上10个实体的复杂用例不太适合,例如,保险应用等。
Ÿ
一个更复杂的变种是允许获取历史数据,当你不仅仅对一个合同的当前状态感兴趣,而且对它在某一个指定时间的状态感兴趣时,需要这种变种,为浏览数据模型,你不得不为表示基于时间的浏览而增加条件和浏览边。保险公司经常需要这样。
已知应用
VAA,应用于德国保险公司的一个标准构架,结合时间浏览使用这个模式[VAA95]。相应的数据管理部件目前在构建中,Württembergische Versicherung[Würt96]开发了一个使用层次视图的数据管理器以及一个定义它们的工具。
许多sd&m的项目使用层次视图模式的简单变种(1:n views),生成这些视图[Den91]。
相关模式
你可以使用一个查询代理来降低层次视图和底层物理数据库的耦合度。使用一个视图缓冲来避免对同一物理数据的重复数据库访问。
模式:物理视图
环境
你已经决定使用关系型数据库访问层。你使用层次视图作为应用核心的接口,并且你已决定,不将数据库的访问放在ConcreteViews。
问题
如何提供一个易用的访问物理数据库表的接口?
先决条件
Ÿ
简单化与性能:为达到一个良好的性能,你不得不使用非范式化、可控冗余或溢出表来优化物理表的布局。但是,对于这些技术如果没有限制地使用会打乱处理物理数据结构代码并使数据库访问变的复杂,尤其是溢出表将导致错综复杂的代码。除了这些复杂的优化,你还想有一个易用的接口和一些可维护的类。
Ÿ
灵活性:大多数据库向你提供了使用静态还是动态SQL的选择,因为数据库预编译、优化静态SQL查询,这通常减少服务器复杂以得到较好性能。一些数据库管理员只允许在服务器上使用静态SQL。另一方面,动态SQL更加灵活并且更适应数据库结构的变化,很容易在开发中使用。为满足高性能的需求,你也许想使用底层数据库API,访问层高层API应该不知道这些细节。
Ÿ
大量问题和开发成本:因为大型数据库可能有上百个表,你也许想有一个构建这些接口的自动过程,宏、生成器或模板都可以减轻这个工作。然而,一个通用的接口开发要花费更多设计努力,因为它必须要考虑到各种情况。
解决方案
用一个ConcretePhysicalView封装每个表和视图,同样用这些类来封装溢出表。为提供一个统一的接口,从PhysicalViews派生出ConcretePhysicalViews,它使用类似PhysicalViews的记录结构来存储它们的实例数据,主要区别是封装一个物理表或视图,还是封装多个表、视图。
结构
实现
封装什么?每个ConcretePhysicalView应该封装一个简单的SQL语句和它对应的溢出表。因为层次视图指向多个ConcretePhysicalView,你可以选择是使用SQL来连接数据库的两个表,还是在访问层连接它们。一个比较好的入手点是为每个“根表”,例如客户表和物品表,定义一个ConcretePhysicalView,,另外,为每个“复合实体”定义一个ConcretePhysicalView,你已为它们定义了数据库视图,例如订单/订单项关系。如果你使用一个查询代理,你可以分析这些决策,寻求更多的候选实体。
封装只读视图:为保持物理视图尽可能简单,你应该考虑哪些ConcretePhysicalViews有权限更新它们已读取的数据。表示数据库视图的物理视图,以及大多数数据库都不支持写视图操作。因此,如果涉及到几张表,一个只读访问的物理视图要比读写访问来得简单。一个好的主意是完全让一个物理视图拥有对某个表的写权限。
编程工具:ConcretePhysicalViews是一般性的类,使用一个生成器或宏技术去实现它们,当然也可以考虑模板。
使用存储过程和其他API:大多数数据库提供存储过程以进行数据库服务器上的运算。因为物理视图是直接工作在数据库上的,因此可以使用存储过程或是其他任何访问数据库元组的API来实现。使用这个方案,你可以写出灵活的优化,例如溢出表,它是数据库代码形式的,而非某种主机语言加上嵌入SQL。但是,这将增加数据库服务器的负载,并且你要确定,所有的应用都要遵从这样的架构。
结论
Ÿ
简单性:物理视图隐藏了优化和数据库编程的复杂性。因为它没有其他责任,也很容易实现。尽管如此,附加的层增加了附加的类。如果你计划舍去查询代理,而直接硬联接到ConcreteView的话,你应该考虑清楚,是否增加一个层更简单或者是是否ConcreteView本身应该访问数据库。后者导致较少的类,但是相应的,灵活性也降低了。
Ÿ
灵活性:因为物理视图封装了数据库代码,使用何种API去访问数据库是它们的选择,。你可以将不同数据库访问API分成几组类。如果你想在运行时试验不同的数据库访问技术,你就可以使用桥(Bridge)[GOF95]来动态切换访问模式。
Ÿ
封装:物理视图使你无需影响上层而优化物理数据库结构。简化了调优过程并导致更佳的性能。大量的附加间接级在这里可以微不足道的。
Ÿ
大量问题:很容易定义一个生成器来构建一个粗略版本的ConcretePhysicalView。只要你不使用溢出表,你只需要将相应的SQL语句包装起来,更复杂的生成器可能需要处理溢出表等问题。
变种
如果你选择直接联接ConcreteViews和ConcretePhysicalViews,你可能将ConcretePhysicalViews作为ConcreteViews的方法。不过这个方法灵活性较低,因为你无法重复使用同一个ConcretePhysicalView。
你也可以使用物理视图来封装非SQL数据库和文件系统,例如ADABAS、IMS-DB、CODASYL和VSAM等。我们前面提到了,你可以使用这些变种来在遗留系统上构建关系型的应用。
示例解决
图7显示了两个物理视图的DAG定义,这是我们的发货单例子所需的。它们对应到物理数据库结构而且也解析溢出表(见图1)。为了简化OrderPhysicalView,应该只对Order和OrderItem数据赋以更新访问权限,有其他的物理视图改变Ariticle表,
图7:两个ConcretePhysicalViews的DAG定义。注意OrderPhysicalView封装Order表和它的溢出表OrderItemOverflow,而CustomerPhysicalView只封装了一个Customer表,参见图1的物理表结构。
相关模式
对优化更深入的讨论,参照[Kel+96b]:溢出表模式的详细描述,如何部分合并表。受控的冗余包含了关于何时赋写权限的讨论。窄视图给出物理视图如以选择数据的提示。
模式:查询代理
示例
考虑前面发货单的例子,OrderInvoiceView描述了逻辑数据结构,而OrderPhysicalView和CustomerPhysicalView描述了相应的物理表。
环境
你已经决定使用关系数据库访问层了,使用层次视图作为应用核心的接口,并使用物理视图来封装数据库访问。
问题
如何连接层次视图和物理视图以进行读取和写入?
先决条件
Ÿ
成本与灵活性:最节省成本的方式是通过函数调用直接硬连接:一个ConcreteView知道它必须调用哪个PhysicalViews,你能够使用约定的表描述来生成相应的调用,这在所有的层都非常稳定的情况下会工作得很好,但是,如果有一层不是很稳定,你就需要使用某种方式来降低耦合。在访问层中,我们有比较稳定的层次视图,位于不太稳定的物理视图之上。如果系统足够小,你可以使用一个程序生成器来耦合这两层。不过,如果系统存在的时间将有好几年,这种方式会因为编译和软件发布而产生昂贵的成本,想象一下你将不得不在每当物理数据模型发生变化时,将几兆的数据库访问软件发布到数千个客户端。
Ÿ
重用性:虽然物理视图可以迅速改变,它反映数据库的物理结构。因此有可能是若干个应用程序使用相同的物理视图却拥有不同的层次视图。为每个独立的应用编写一个独立的耦合机制使你从重用物理视图中得到的好处无处体现。
Ÿ
复杂性:因为硬编码的解决方法不够灵活,你需要一个更复杂的方法。然而,附加的复杂性使你的系统成本增加且难以二次维护。
解决方案
使用一个代理[Bus+96]来联接两个层,层次视图形成查询代理的客户端,物理视图作为代理的服务端。使用DAG来描述服务,并且使用一个树匹配算法来寻找最优匹配。让查询代理装配物理视图并将结果集放在查询结果集包容器中。
结构
代理是降低耦合的一种标准技术,以它的标准结构应用于数据库访问层框架:
Ÿ
查询代理和一个标准代理最重要的区别是它通常有多个服务端来处理一个请求,到服务端的映射不是一对一而是一对多的关系。
图8:查询代理是一种应用到数据库访问层框架的代理。
Ÿ
代理通常使用符号名称来标识服务。但我们这里在服务请求和服务端之间是一种1:n的关系,这不大适合。因此,查询代理使用语义描述(DAGs)来表述请求视图。看看图9,左边表示OrderInvoiceView的请求,右边表示相应的服务。为组装ConcretePhysicalViews,查询代理匹配关键字,标记在白色椭圆中。
图9:树匹配解析查询代理收到的请求。左边显示了我们订单处理系统的OrderInvoiceView,右边显示了相应的两个物理视图。如果查询代理得到左边表示的请求,那么右边的物理视图将是满足请求的最优方式。
动态行为
在下面的场景中,一个应用核心对象创建一个OrderInvoiceView,产生一个数据库调用read(),查询代理处理read(),并且通过一个matchServices()方法依据已有的服务匹配一个视图描述。QueryBroker将这个请求转发到两个不同的ConcretePhysicalViews:OrderPhysicalView和CustomerPhyscialView,这两个read()从数据库读取数据并将数据打包到结果集包容器中发布。代理必须合并结果集包容器,向OrderInvoiceView发布一个结果。OrderInvoiceView再将数据解包到它的实例变量中。
图10:由查询代理获取数据
实现
Ÿ
服务端注册:所有的ConcretePhyscialViews必须在第一次数据库访问前注册到QueryBroker中,在系统初始化时要特别注意这一点。你可以使用一个运行时字典、其他一些形式的注册表或者是语言特定的初始化技术。
Ÿ
转换数据类型的职责:原始数据库类型和应用数据类型的互相转换有两种选择。例如,你不得不将一个CHAR(20)类型转换到一个OrderKeyType类型,反之亦然。你可以转换ConcreteViews,也可以转换PhysicalViews。如果由一个运行时字典,它包含逻辑数据模型和哪些属性转换到哪些应用数据类型的信息,就可以同时支持这两种选择。
Ÿ
树匹配:匹配DAG类似于编译器的代码生成,它必须为程序寻找一个理想的汇编码,所以,你可以使用类似的算法[Aho+86,9.2章]。代理甚至可以为一个请求寻找若干具有不同速度的查询计划,匹配算法必须处理任意多种情况,以得到一个最快的方案。对编译器优化也有类似问题,为代码生成的目的,处理任意语法。
Ÿ
查询描述和结果集包容器:你必须找到一个理想的DAG表示法,用以描述查询。一方面,视图应该能够很容易指定它们的请求和服务,另一方面这个表示法应该顺应匹配算法的需要,一个易于解析的文本表示是一个很好的选择。
结论
Ÿ
灵活性:查询代理完全降低了层次视图和物理视图的耦合度,在运行时,新的物理视图可能被注册,而且和层次视图的关联也会被改变。
Ÿ
复杂度:树结构的结果集包容器、请求描述、树匹配算法使查询代理很难设计,但是代理封装性很高,限制了单一子系统的复杂性。
Ÿ
重用性:因为查询代理独立于它联接的视图,你可以将它作为框架的一部分,这比仅仅重用物理视图或生成器更好。
Ÿ
成本:查询代理的复杂度使之实现的代价很高,一个运行时字典更增加了成本。不过作为补偿,它实现成一个可重用的框架并应用于多个应用程序中。硬连接耦合的构建代价较低但是使优化更昂贵,并且当你想将软件发布到数千个客户端时,简直是个恶梦。
变种
如果你只使用动态SQL,查询代理可以组装SQL语句而无需使用物理视图,这对于干净的数据库模型工作起来不错,但是对于溢出表的处理就很难。你可以在开发的早期使用这种方式,接着,增加越来越多的使用静态SQL或其他API的物理视图服务器,代理向应用核心隐藏了这些变化,它总是负责在它的注册表中寻找最快的服务。
已知应用
查询代理汇集了多种最好的实际应用:
VAA数据管理器[VAA95]根据逻辑数据模型定义视图,它使用一个自动生成的硬连接来耦合两层。我们在HYPO-Bank中的经验告诉我们无论在哪儿都尽可能使用动态描述。在sd&m的两个项目使用了这种方式其他重要的部分。
sd&m的LSM项目使用动态SQL,移植到静态SQL并最终形成一个元组接口。这个想法来自工作于一个缓慢的数据库服务器上的实际经验,这个项目完全使用一个运行时数据字典来桥接动态查询和预编译查询。
德国警局的Fall/OK项目使用树匹配,这个软件通过在一个大型数据模型上的示例处理查询,数据模型的改变非常频繁。
CORBA持久对象服务[Ses96]也使用了一个代理,应用核心对象将它们的实例数据写入流,然后一个代理(持久对象管理器)将流转发到某个持久对象服务(数据库或其他什么)。持久对象服务可以是任何数据库,对象无需知道它。这也是一种简单的请求和服务端之间一对一的情况。
相关模式
[Bus+96]包含了一个关于代理的更广泛的讨论。
Brown和Whitenack描述了一个代理模式[Bro+96]的基本类,注意,查询代理是更一般的情况。
参考文献
[Aho+86] |
Alfred
V. Aho, Ravi
Sethi, Jeffrey
D. Ullman:
Compilers: Principles, Techniques, and Tools, Addison-Wesley
1986. |
[Bro+96] |
Kyle
Brown, Bruce G. Whitenack:
Crossing Chasms, A Pattern Language for Object-RDBMS
Integration, White Paper, Knowledge Systems Corp. 1995.
A shortened is contained in: John M. Vlissides, James O. Coplien, and Norman L. Kerth (Eds.): Pattern Languages of Program
Design 2, Addison-Wesley 1996. |
[Bus+96] |
Frank
Buschmann, Regine Meunier, Hans
Rohnert, Peter Sommerlad, Michael Stal: Pattern
Oriented Software Architecture, A System of Patterns,
Wiley 1996. |
[Col+96] |
Jens
Coldewey, Wolfgang Keller: Objektorientierte
Datenintegration – ein Migrationsweg zur
Objekttechnologie,
Objektspektrum Juli/August 1996, pp. 20-28. |
[Col97] |
Jens
Coldewey: A
Database Access Layer for ODBMS,
In Akmal Chaudri, Mary Loomis (Eds.), Experiences
with ODBMS [working title], Prentice
Hall 1997 (to appear). |
[Dat94] |
Chris
J. Date:
An Introduction to Database Systems, Sixth Edition;
Addison-
Wesley
1994 |
[Den91] |
Ernst
Denert:
Software-Engineering, Springer Verlag 1991. |
[GOF95] |
Erich
Gamma, Richard Helm, Ralph Johnson, John Vlissides:
Design Patterns, Elements of Reusable Object-oriented
Software, Addison-Wesley 1995. |
[Gra+93] |
Jim
Gray, Andreas Reuter:
Transaction Processing, Concepts and Techniques, Morgan
Kaufmann Publishers 1993. |
[Kel91] |
Wolfgang
Keller: Automated
Generation of Code using Backtracking Parsers for Attribute
Grammars,
ACM Sigplan Notices, Vol. 26(2),
1991. |
[Kel+96a] |
Wolfgang
Keller, Christian Mitterbauer,
Klaus Wagner:
Objektorientierte Datenintegration über
mehrere Technologiegenerationen, Proceedings ONLINE,
Kongress VI, Hamburg
1996. |
[Kel+96b] |
Wolfgang
Keller, Jens Coldewey:
Relational Database Access Layers: A Pattern Language,
Proceedings PLoP ‘96, Allerton
Park
1996. |
[Lan96] |
Manfred
Lange: Abstract
Constructor, Preliminary
Conference Proceedings EuroPLoP,
First European Conference on Pattern Languages of Programming,
Irrsee,
Germany,
1996. |
[Mar95] |
Robert
C. Martin: Designing
Object-Oriented Applications Using the Booch Method;
Prentice-Hall International, London,
1996 |
[ODMG96] |
Rick
G. G. Cattell (Ed.) et. al.: Object
Database Standard: ODMG-93 -Release 1.2 Morgan
Kaufmann Publishers, San
Mateo,
California,
1996. |
[Rum+91] |
James
Rumbaugh, Michael Blaha, William Premerlani, Frederick Eddy, William Lorensen: Object-Oriented
Modelling and Design,
Prentice Hall, 1991. |
[Sch96] |
Johannes
Schlattmann: Die
Anwendungsarchitektur der Versicherungswirtschaft, Historienkonzept,
Entwurf, GDV Bonn, 1996. |
[Ses96] |
Roger
Sessions: Object
Persistence, Beyond Object Oriented Databases,
Prentice Hall 1996 |
[VAA95] |
GDV:
VAA
- Die Versicherungs-Anwendungs-Architektur,
1.
Auflage, GDV, Bonn
1995 |
[Würt96] |
Württembergische
Versicherung: Projekt
Datenmanager,
private
communications 1995 - 1996. |
[Wit96] |
Andreas
Wittkowski;
Datenbankdesign &
Performance, slide presentation, sd&m Internal Lecture Series, 1996. |
|