UML软件工程组织

关系数据库访问层 一种模式语言

关键模式

 

日期:4/2003

译者:HappyLiu

 

摘要

关系数据库访问层可以帮助您设计使用关系型数据库的应用程序,并且在业务对象级别反映关系运算,这种应用程序被称作数据驱动或representational[Mar95]。这种系统并不需要是面向对象的,您完全可以使用第三代语言(3GL)来实现,因此,本文的模式语言将忽略映射继承和多态的特性。

本文包含框架模式和几个关键的实现模式。PLoP 学报[Kel+96b]包含了完整的模式语言,包括适配器和优化模式。

 

简介

很多业务信息系统拥有一个简单的数据模型,即使他们可能会有30或更多实体表,但是很少发现继承或者复杂的关联。对于复杂的情况,通常是封装在应用程序的核心。用关系运算来为这些系统建模是一个很好的主意。

来看看下图1,从一个订单管理系统中摘录的片断,在左上角,有处理发货单的实体,这个片断遵循第三范式(3NF),在因数分解级别,数据分析家经常这么用。假设你使用这种逻辑数据模型来定义你的物理数据库表,系统当然可以正常工作,但是性能却是不敢恭维。

剖析这个系统,你将发现很多多余的数据库操作,同时,也会发现数据库操作过于缓慢的原因是大量的表连接或者是不必要地移动大量数据。为了提高性能,你可以在物理数据模型中不用太过范式化。对数据库内容的统计分析表明90%的订单拥有不超过5个订单项(OrderItems),因此可以将前5OrderItems放在Order表中,为包含剩余10%,可以创建一个OrderItemOverflow表,如图1右上角所示,此外,你还可以将物品的属性ArtPriceArtName集成到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.535个人年,使用生成器和依赖硬编码比构建维护工具代价和查询代理的代价要低,在你决定不同的选择前,充分考虑预期的变化、市场推广和软件的生命周期。

Ÿ           易用:这个访问层并没有将关系模型转换到一个面向对象视图,因此应用核心必须工作于关系视图上。你应该仔细考虑是否这种数据驱动的方式适合你的应用逻辑,看看是否你的工程更适合使用对象-关系的访问层。当应用核心确实需要一个隐式的映射是,避免构建一个更复杂的访问层并不是一个好的想法。

Ÿ           灵活性:如果你使用一个查询代理,你可以通过增加新的具体物理视图来维护并调优数据库,而不用修改应用核心代码。在底层物理数据库改变时,应用代码保持稳定。

Ÿ           复杂性:这个访问层包含最简单的类。查询代理是成本最高的部分,因为它包含一个复杂的树匹配算法。忽略它可以导致一个简单的适配器层,但是灵活性降低了。

Ÿ           性能:你要为映射付出一点运行时性能的牺牲,不过访问层可以使用缓冲来优化并易于调优,它将使用快速的处理器周期,而避免缓慢的IO处理。

Ÿ           遗留数据:你也许会使用访问层来降低现有应用的物理、逻辑数据模型间的耦合,这对重构遗留系统非常有用。首先,在代码中插入一个数据访问层,这是一个具有可控风险的单一步骤,接着,开始在不同的项目中重写数据库和应用核心。

 

变种

Ÿ           舍去视图缓冲:如果你不需要长事务,可以舍去视图缓冲。这对简单的对话框系统是可行的,这种系统在每个事务中一般之处理一条记录。但是如果应用核心有一些影响多条记录的事务时,就应该使用视图缓冲。

Ÿ           对于在事务监控器之上实现用户事务,一个缓冲机制是非常自然的选择,例如IMSCICSUTM。当用户事务包含多步对话框才完成,事务监控器为每一步开始一个新的事务,使用视图缓冲可以让你收集到一个用户事务中所有的写数据库操作,接着,它们可以作为一个技术上实际的事务执行,可以对这些多步的对话框操作保持事务一致性。

Ÿ           使用非关系型数据库:物理访问层可以封装非关系型数据库和文件格式,例如IMS-DBCODASYLVSAM。甚至可以将它改写到若干不同的数据库技术,隐藏对遗留数据的访问。

 

已知应用

VAA数据管理器规范使用这个模式,并带有元数据编辑器和对层次数据库系统复杂的映射[VAA95]VAA数据管理器是从Württembergische Versicherung[Würt96]的数据管理器派生而来的。

Denert[Den91,pp.230-239]中简单描述了这个模式语言的一些基本思想,sd&m的许多项目都使用这个模式的各种变种,包括ThyssenDeutshche BahnHYPO银行[Kel+96a]

CORBA持久对象服务(POS[Ses96]指定持久对象使用一个代理(持久对象管理器)来将数据写入任意数据存储中(持久数据服务)。

 

相关模式

这个模式一个分层(Layer的应用[Bus+96,pp.31]视图工厂一个抽象构造器[Lan96]的应用。

[Bro+96][Col+96]阐述了如何扩展这个模式,向应用核心提供面向对象的视图。BrownWhitenack[Bro+96]使用一个代理来降低层之间的耦合,而[Col+96]描述了一种直接连接的方法。

一些实现模式

模式:层次视图

示例

考虑图5中,订单处理系统的细节。其中的发货单可能具有右图所描述的情况,注意这个发货单有两级间接的层次结构。一个用例可能就是从一个订单号开始并浏览它各种订单项和它们的物品。

5:我们订单处理系统数据模型详细。左边用第三范式表示的 ER图,右边是发货单的结构,由这些实体构成。

 

环境

你已经决定使用关系数据库访问层来降低物理数据库和应用核心的逻辑数据模型之间的耦合。

 

问题

数据库访问层向应用核心提供什么接口?

 

先决条件

Ÿ           复杂度与易用性和开发成本:接口应该简单易用而且要有足够的功能来满足必要的数据库操作。因为应用核心反映域问题,你不会想用数据库特定的代码来打乱它,所以这个接口应该在某种程度上反映适当域级别的数据抽象。提供一个包含所用数据库功能的接口意味着重新实现大部分数据库管理系统,这个代价太高了,所以,SQL风格的接口不大可行。

Ÿ           灵活性与开发速度:在数据库访问时,使用逻辑数据模型作为指导对于短期基本功能实现可能是最简单的解决方案。但是,性能调优和维护迫使你要经常改动物理数据库的布局,同时我们也不想改变应用核心,所以我们需要一种和物理数据模型无关的接口。

Ÿ           性能:理论上,第三范式对于关系运算是最好的,但是在物理模型中用它将导致很差的性能。

Ÿ           大量问题:一个大型的数据模型包含上百甚至更多的实体,手工地为数百个实体编写包装函数或嵌入的SQL代码是一个无聊而又代价昂贵的任务,枯燥的工作还总是伴随错误发生。一般的解决方法是在数据库编程中使用宏、生成器或模板。

 

解决方案

根据域问题空间来表示接口,那就成为一个关系数据模型。从数据模型的一点(或实体)开始,并使用外键关系来行走到其他感兴趣的点。在行走过程中形成形成了一个有向无环图(DAG),每个节点标记上实体名,相关属性和选择谓词,每条边标记上所使用的外键以及它的粒度(一对一或一对多)。

 

结构

6显示了相当于左边发货单的图表示

 

要将这个图转换成一个层次视图,从View派生一个ConcreteView,这个ConcreteViewDAG的根,再为任何一个节点定义一个域级别的类,使用聚集来实现图中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:

// 私有方法用以读取和写入数据

//