[0] 对象与关系之间的矛盾“阻抗失配”效应

对象与关系之间的矛盾:
“阻抗失配”效应

概述

“阻抗失配”这一词组通常用来描述面向对象应用向传统的关系数据库(RDBMS)存放数据时所遇到的数据表述不一致问题。C++程序员已经被这个问题困扰了好多年,而现在的Java程序员和其它面向对象开发人员也对这个问题深感头痛。

“阻抗失配”产生的原因是因为对象模型与关系模型之间缺乏固有的亲合力。“阻抗失配”所带来的问题包括:类的层次关系必须绑定为关系模式(将对象类映射为关系表),ID生成,并发访问以及下面提到的一些问题。

应用和数据存储方式的不一致是导致这些问题的主要原因。这个问题在很多方面已经变得十分明显,如:产品上市的周期,应用设计、开发和维护的费用,代码维护和扩展性,以及能够满足响应时间和性能需求的硬件配置和结构。在这些方面使用不同的数据存储方式所得到的结果将有很大的不同。

面向对象与关系数据库之间不一致所带来的必然结果——基于SQL的应用程序与面向对象数据库之间的不匹配效应越来越受到人们的关注,所以,对这个问题解决方法的探讨十分必要。

面向对象的开发语言

目前许多的业务逻辑和用户界面都是用很受欢迎的面向对象语言开发的,比如:C++,Visual Basic,Delphi,不断成熟的Java,以及很多开源程序语言。这些面向对象语言环境或多或少地实现了封装,多态,继承等概念。正确使用这些概念可以为整个开发带来相当大的好处。

 

在何处存放数据?

如果对数据的持久性有要求的话,面向对象语言和所有开发语言一样,都需要绑定数据存储器,一般情况下都采用数据库作为数据存储器。三种主要的数据模型是:关系型,对象型和后关系型亦即事务多维模型。

关系与对象数据库模型的根本区别

将关系数据库和对象数据库模型的不同点和两种不同模式的开发方法做个比较对应用开发人员十分有用。

关系模型中的表将信息保存在列中并以行的形式进行组织。复杂的数据类型往往需要很多的表来表示。表之间的关系(一对一,一对多,多对多)基于表的外键实现。

业务逻辑操作必须由表外的资源执行,例如:通过使用嵌入式SQL语句或者使用存储过程和触发器。为了建立关系模式的高效应用,开发人员必须对表,表间关系有相当深入的了解,而这些都是独立于业务逻辑组件之外的。

相反,对象模式中的类则是自包容(self-contained)实体。与关系表一样,它们包含信息(通过属性的方式)。但类与关系表一个很大的不同之处是相关数据(即嵌入式类和类集)可以存储在“容器”类中,而不是将这些相关数据分隔成独立的表,再使用外键重构它们之间的联系。

另外一个重要的不同点是,在对象模式中业务逻辑不是由外部的资源执行的。一个类中的方法就包含了对其类属性操作的代码。方法提供了调用这个类的接口,所以应用开发人员就不用顾虑使用关系模式所带来的复杂度了。

开发实例

为了说明前面所提及的两种开发模式的不同点,下面分别采用不同的数据库开发编码方式举例。

比如要开发一个汽车登记程序。一部汽车(automobile)有以下的一些数据:make,model,trim line,year,VIN等,它可以有一个或几个车主(owners),一个或多个驾驶员(driver),还要有维修记录(repair history),汽车还能有登记,被划伤等行为。

通过上面的分析,使用对象方法描述汽车(automobile)数据可以创建Car,Person,Owner,Driver和RepairHistory类。

Owner和Driver类可以从Person类中继承方法和属性。你可以对这两项进行扩展,添加新的属性和方法。

Owner,Driver和RepairHistory类可以按集合的方式放到Car类中,前两个可以采用参考集合的方式,后一个可以采用嵌入式类。

在现实当中,一个Owner可以拥有很多个Car,一个Car可以有多个Owner。为了实现这个多对多的关系Owner类可以包含Car的参考集合(在上面的分析当中,已经为Car类包括了Owner的参考集合)。你可以按照Owner-Car的方式实现Car-Driver之间的多对多联系。

最后,我们来定义一个Car类的方法:RegisterNew(),参数是VIN。登记汽车的所有工作都可以通过调用这个接口来实现。

 

这些定义见 图1

关系模式中的实现方法

以关系模式分析前面的例子,你可以用图2 的方式创建表格。

注意到为了维护Owner,Driver和Car类之间的关系,必须添加两个多余的表:CarDrivers和CarOwner。

编写代码

让我们来处理两个实际的行为。例子1a1b展示了用对象方式和SQL分别实现给定Car的VIN查找它所有者的行为。

例子2a2b是登记汽车的对象方式和SQL伪代码。

管理数据库时的“阻抗失配”效应

工作在纯面向对象开发环境的编程人员可能会采用例1a和2a的编码方式。使用关系型数据库的SQL编程人员很可能采用例1b和2b的编码方式。

对象开发人员在使用关系型数据库时可能会将例1a、1b和连到数据库的例子2b中的一些方法进行结合。换而言之,每个类的方法-Open(),Save(),Delete()等必须重新用SQL进行重写。例如在客车这个例子中:

  • Open()方法执行了一个SQL语句SELECT遍历Car表列并将列值绑定到Car类中的属性

  • Count()方法得到所有满足下面条件的Owner数:
    Select Count(*) From CarOwners Where CarOwners.Fkey_Car=Car.CarID AND Car.VIN=:vin)

  • Get(index)方法得到所有满足下面条件的Owner的Name:
    Select Person.Name
    From Car, CarOwners, Owner, Person
    Where CarOwners.Fkey_Car=Car.CarID
    And CarOwners.Fkey_Owner=Owner.OwnerID
    And Owner.Fkey_Person=Person.PersonID

 

其他“阻抗失配”效应的复杂度

在混合使用面向对象应用和关系型数据库时,还需要考虑到以下的几个问题。

 

  • 需要将类转换为一个或多个底层的表格

    这个任务是将类中的每一个属性对应成为表格中的列。如果类和表格能够一一对应的话,这种转换就十分简单,但是大多数的对象模型都需要把多个表格进行分解,并从中抽取一部分作为类的属性。还可能有很多类都会映射到相同的表中并根据自身类的需要对表的语义进行解释(这就需要一个新列用来存放类名及其作用的行)。

  • ID生成——生成对象ID并将其插入关系数据库

    对象ID的生成过程十分重要,在关系数据库中通过Save()方法生成不重复键时需要Object ID。在一一对应的类与表之间,最好的方法就是使用关系数据库引擎生成一个ID号,并将它作为对象的ID。但是一个类可能会跨越多个表,它的Save()方法可能会需要把多个关系ID作为一组来调用。生成的ID必须是持久性的,这样才能在打开对象时获取表中的行来生成实例。因此,为了维护对象ID和关系ID的联系必须另外创建一个表。

  • 值的检验

    如果原先基于SQL,使用关系数据库的应用采用外部数据检验的方式,那么应用的数据检验代码就不需要进行改动而直接用到面向对象应用中来。

  • 手动编写持久性代码与操作的顺序

    你必须对数据在应用级的完整性有足够的认识。如果你要实现Save()方法,其内部包括的操作(插入,更新,删除)必须遵守特定的顺序才能保证应用级的数据完整性。

  • 注意并发性

    会对应用级数据完整性产生影响的另外一个因素是应用访问的并发性。你必须确定关系数据库提供的数据锁定级别与类的方法中对数据的锁定能够协调一致。

  • 注意模式的演变

    因为使用到了对象-关系映射,所以必须注意对象模式和关系模式在演变的同时能否保持映射的同步。对象模式和关系模式之间在发展时可能存在的不协调性会使得这种同步变得十分地复杂。

  • 保证两种应用都能使用

    前面提到的问题都取决于原来的应用和新开发的面向对象应用能否同时协调地运行关系数据库上。

市场提供的解决方案

在市场上通常有四种方法来处理“阻抗失配”问题。

  1. 使用关系数据库的面向对象功能

  2. 使用对象-关系映射工具

  3. 采用对象数据库

  4. 采用后关系型数据库

1. 使用关系数据库的面向对象功能

虽然关系数据库提供商为在他们的数据引擎中添加面向对象的功能做出了很大的努力,但是他们到现在为止仍然没有从根本上解决“阻抗失配”这个问题。他们提供的方案没有一个用真正的面向对象概念实现。在很多的情况下,他们自己就会陷入自相矛盾的窘境当中。继承,多态和封装这些概念并不是很容易地集成到关系数据库提供商的核心技术当中。最后,开发人员还是被限制在关系数据库之上对类对象层进行管理。

2.使用对象关系映射工具

很多数据库提供商和第三方开发商都提供“对象-关系映射”工具来处理应用中的类与关系数据库中表的联系。这些工具可能会包含一些缓存运行组件用来增强数据库操作(插入,更新,删除,选取和并发需求)的性能和完整性,生成唯一标识ID的方法,为特定语言(JavaBeans,COM对象)生成绑定。这种工具在把关系模式向基于对象的应用转换时十分有效。

但是在问题变得复杂的情况下,这些映射工具这会把责任重新推给开发人员。当需要把多表映射为一个类或多类映射为一个表时和需要生成唯一标识ID时,开发人员还是要负责对象的映射工作。

3.采用对象数据库

处理“阻抗失配”问题的另外一个方法就是采用对象数据库。这种数据库的优势就是它们完全支持OO的概念。

但市场是还有一些顾虑:

  • 对象数据库的市场份额还比较小

  • 向关系数据库提交的事务也应该在面向对象数据库得以实现

  • 基于SQL的应用与关系数据库产品之间存在的“阻抗失配”效应。

还有一个考虑就是终端用户在桌面上运行的决策支持系统一般都是通过ODBC使用SQL查询来与数据库进行互联,因此需要把基于对象的数据按照关系表的方法表示出来。一些面向对象数据库提供商采用在中间层设置关系-对象映射策略来克服这个“阻抗失配”效应。

4.采用后关系数据库

后关系型数据库(即事务型多维数据库)实现了联合数组技术,使用这种技术可以将数据同时映射为对象模型和关系模型。使用后关系型数据库就不再需要其它的对象-关系映射工具和中间层了。InterSystems公司的Caché就是后关系型产品。

在Caché数据库中,类的方法(如前例中的Save()方法)直接映射给对象开发人员,这些方法由数据库中管理联合数组的指令进行实现。同时,Caché数据库引擎可以将相同的数据(联合数组)通过ODBC和JDBC以关系表的方式表示出来。像Insert等SQL数据定义语言,数据管理语言和数据控制语言都能映射为实现对象方法的数据库指令。因为Caché的多维数据引擎能够以两种方式(SQL和对象)实现对数据的直接存取,所以Caché大大消除了前面提到的ID生成,并发访问等问题。

开发人员可以通过两种方式对数据进行直接同步的存取(包括对并发访问的管理),这就意味着不论是基于SQL的应用还是基于对象的应用都可以在Caché上运行,同时,Caché的关系映射功能还可以满足终端用户使用他们喜欢的基于SQL的前端决策支持工具。

结论

我们已经对能够解决面向对象语言与关系数据库之间“阻抗失配”效应的几种方法进行了评述:使用关系数据库提供的部分面向对象支持,使用对象-关系映射工具,采用对象数据库,采用后关系型数据库(事务型多维数据库)。

在对这些技术进行评测时需要考虑到的几点是:

  • 该技术是不是易于掌握,能否与原应用集成以满足立即应用

  • 该技术能否满足未来应用的数据库的需要

  • 该技术是否有足够的灵活性,方便与新兴的技术进行结合,如XML,SOAP等

  • 该技术是否能够允许开发人员在正确的逻辑屋实现其代码,换句话说就是本该在数据库执行的代码是不是必须转移到应用服务器或中间层

  • 该技术能否提供足够的可性能和扩展性

  • 该技术在解决开发问题时是否有用

有一件事我们可以肯定:当今的市场十分清楚的意识到面向对象技术是开发新应用和对原有应用进行升级所必须的技术。已经对关系数据库投入大量资金的企业应该考虑他们是否采取短期,中期长期的迁移计划以消除对象和关系模式之间固有的“阻抗失配”效应。

 

图1- 打开新窗口查看

图2- 打开新窗口查看


1a - 列出Car VINs和他们的所有者:面向对象方法

objCar=Open Car(vin);
owner_count=objCar.Owners.Count()
For (i=0; i<owner_count; i++) {
      owner_name=objCar.Owners.Get(i).Name;
}

1b - 列出Car VINs和他们的所有者:SQL方法

Select Car.VIN,Person.Name
From Car, CarOwners, Owner, Person
Where CarOwners.Fkey_Car=Car.CarID
      And CarOwners.Fkey_Owner=Owner.OwnerID
      And Owner.Fkey_Person=Person.PersonID
      And Car.VIN=:vin

2a - 登记新Car: 面向对象方法

// somehow we acquired the car's VIN, make, model and list of owners
// use a class method rather than an instance method to validate
rc = Car.IsValidVin(vin)
// if rc indicates an error, logic to reject goes here
// now assume an owners[i] array and this is where the logic for validating them goes
// instantiate a new car
objCar = New Car;
// assign the properties
objCar.VIN = vin;
objCar.Model=model;
objCar.Make=make;
// now register the car
objCar.RegisterNew(vin)
// assign the Owners
      For (i = 1; i < count; i++) {
obj.Car.Owners.SetAt[i] = owner[i]
}
// make it persistent
objCar.Save()

2b - 登记新Car: SQL方法

// somehow we acquired the car's VIN, make, model and list of owners
//validation of information is external to the table
If (vin == "") ! (vin=0){
      Exception("VIN required")
}
//Other fields would be validated here
//
&sql(Insert Into Car(Make, Model, VIN) Values(:make, :model, :vin)
// recover the new assigned Car ID
&sql(Select CarId Into :car_id from Car Where Car.VIN=:vin)
// and assign the owners to the Car
For (i = 1; i < count; i++) {
      &sql(Insert Into CarOwners(CarKey,OwnerKey) Values(:car_id,:owner[i])

posted on 2013-11-30 10:01  范兵  阅读(2487)  评论(0编辑  收藏  举报

导航