第三节 O/R阻抗失配

在使用面向对象的编程语言编写的应用程序中,对象是其基础构建块。由于前文表述的种种原因,我们需要将领域对象的状态持久化到某种存储媒体中,以便在需要时能够重建这些对象。通常而言,是将领域对象状态(值和关联)持久化到关系数据库中。

在以关系数据库为目标的持久化中,我们会尽量:

  • 将领域对象中的实体类与数据库中的表一一对应
  • 将实体实例与表中的记录(行)一一对应
  • 将实体实例属性与表中的字段(列)一一对应
  • 将实体类的标识属性对应到表中的主键字段

但是,对象模型(O)和关系模型(R)是基于不同的理论,针对不同的目标建立的,两者之间并不能那么完美匹配。实际上,两者在很多方面存在“阻抗失配(Impedance Mismatch)”。主要体现在以下的方面。

1. 粒度问题

我们知道,在领域模型中,有些实体持有值对象,这些值对象本身可能有多个属性。有时候,值对象内部甚至还包含下一级的值对象。类的粒度从粗到细:实体-值对象-简单属性,可以有3个甚至3个以上的粒度层次。

例如在电商网站中,顾客类Customer持有配送地址shippingAddress这样一个Address类型的值对象属性。

而关系数据库中只有两种粒度:由你创建的关系类型(表),以及内置的数据类型,如VARCHARBIGINTTIMESTAMP等。它不能直接对应到对象模型中的多级粒度。尽管各种数据库系统往往允许用户创建自定义类型,但是操作起来很复杂,更关键的是:不同数据库系统的自定义数据类型不可移植。

2. 标识问题

在应用程序和数据库中,常常需要判断两个对象或者两行数据是不是相同的(代表相同的对象或数据行),就是通过什么样的标识来识别两个事物是不是相同的事物。

Java中,有两种不同的相等性概念:

  • 实例标识:大致等同于对象的内存位置,使用a == b进行检查。
  • 实例等价性:通过equals()方法的实现进行判断。

另一方面,数据行的标识会体现为主键值的比较。同一个表中主键值相同两个数据行代表同一行数据。

Java中有几个不相等的对象实例同时对应数据库同一行的情况很常见,例如在并发的应用程序线程中就是如此。

3. 继承与多态问题

面向对象有三大特征:封装、继承和多态。Java作为面向对象的编程语言,可以使用基类和子类来实现类型继承。在领域模型中,类型继承是非常常见的。例如银行系统的领域模型:

银行账户Account这个超类有储蓄账户SavingsAccount和信用账户CreditAccount两种子类型。在超类Account中可以定义所有子类的共同属性(锁定状态locked、当前余额balance等),同时每个子类还可以分别定义自己特有的属性(例如信用账户CreditAccount子类拥有一个特有属性:信用额度creditLimit)。

Account抽象超类有一个owner(账户持有人)属性,指向另一个抽象基类Customer(客户)。Customer有两个具体子类:公司客户CompanyCustomer和个人客户PersonalCustomer。两个子类分别拥有自己的一批专有属性。

子类除了可以添加自己特有的属性之外,还可以添加方法、重写或实现超类的方法。例如超类Account上定义了抽象方法credit(),两个子类分别为这个方法提供自己的实现。

基于上面的分析,持久化必须:

  • 允许进行多态关联。

    例如上面的例子,某个Account实体实例account1owner属性可能关联的是一个CompanyCustomer,而另一个Account实体实例account2owner属性可能关联的是一个PeronalCustomer。持久化必须能够记录关联的具体子类型信息,以便在检索数据库重建Account实例的时候恢复正确的子类型关联。

  • 允许进行多态查询。

    例如上面的例子。当我们以具体子类为目标进行查询的时候,期望返回的是符合条件的某个子类的实例的集合。而当我们以基类为目标进行查询的时候,期望返回的是符合条件的所有子类型的实例的集合。

关系数据库中没有任何与数据继承和多态相关的概念。表之间没有继承关系,SQL也不提供任何多态查询的功能。

4. 关联的方向性问题

在领域模型中,关联表示了实体之间的关系。面向对象的语言如Java使用对象引用来表示关联,而在关系模型中,外键约束列表示了一个关联,它带有一些键值的副本。

5. 关联的多重性

6. 数据导航问题

7. 值对象的生命周期问题

results matching ""

    No results matching ""