面向 Java 开发人员的 db4o 指南: 第 3 部分:db4o 中的数据库重构

重构 Java™ 代码远远比重构关系数据库简单,但幸运的是,对于对象数据库却并非如此。在本期的 面向 Java 开发人员的 db4o 指南 中,Ted Neward 介绍他喜欢的对象数据库的另一个优点:db4o 简化了重构,使之变得非常容易。

 

本系列的上一篇文章 中,我谈到了查询 RDBMS 与查询像 db4o 这样的对象数据库的不同之处。正如我所说的那样,与通常的关系数据库相比, db4o 可以提供更多的方法来进行查询,为您处理不同应用程序场景提供了更多选择。

这一次,我将继续这一主题 —— db4o 的众多选项 —— 看看 db4o 如何处理重构。自 6.1 版开始,db4o 能自动识别和处理三种不同类型的重构:添加字段、删除字段和添加一个类的新接口。我不会讨论所有这三种重构(我将着重介绍添加字段和更改类名),但我将介绍 db4o 中的重构最令人兴奋的内容 —— 将向后兼容和向前兼容引入到数据库变更管理中。

您将看到,db4o 能够静默地处理更新,并确保代码与磁盘的一致性,这大大减轻了重构系统中持久性存储的压力。这样的灵活性也使得 db4o 非常适合于测试驱动开发过程。

关于本系列
信息存储和检索作为同义语伴随 RDBMS 已经有 10 余年了,但现在情况有所改变。Java 开发人员为所谓的对象关系型阻抗失配而沮丧,也不再有耐心去尝试解决这个问题。加上可行替代方案的出现,就导致了人们对对象持久性和检索的兴趣的复苏。面向 Java 开发人员的 db4o 指南 对开放源码数据库 db4o 进行了介绍,db4o 可以充分利用当前的面向对象的语言、系统和理念。请访问 db4o 主页 ,立即下载 db4o。您需要用它来完成本文中的例子。

现实中的重构

上个月 ,我谈到了使用原生和 QBE 样式的查询来查询 db4o。在上次讨论中,我建议运行示例代码的读者删除包含之前运行结果的已有数据库文件。这是为了避免由于一致性概念在 OODBMS 中与在关系理论中的不同而导致的 “怪异” 结果。

这 种变通办法对于我的例子是适用的,但是也提出了现实中存在的一个有趣的问题。当定义其中所存储的对象的代码发生改变时,OODBMS 会怎样?在一个 RDBMS 中,“存储” 与 “对象” 之间的联系很清晰:RDBMS 遵从在使用数据库之前执行的 DDL 语句所定义的一种关系模式。然后,Java 代码要么使用手写的 JDBC 处理代码将查询结果映射到 Java 对象,要么通过 Hibernate 之类的库或新的 Java Persistence API (JPA) “自动” 完成映射。不管通过何种方式,这种映射是显式的,每当发生重构时,都必须作出修改。

从理论上讲,理论与实践之间没有不同。但也只是从理论上才能这么讲。重构关系数据库和对象/关系映射文件应该 是简单的。但在实际中,只有当重构仅仅与 Java 代码有关时,RDBMS 重构才比较简单。在这种情况下,只需更改映射就可以完成重构。但是,如果更改发生在数据的关系存储本身上,那么就突然进入一个全新的、复杂的领域,这个专 题复杂到足够写一本书。(我的一个同事曾经描述到 “500 页数据库表、触发器和视图” 这样的一本书。) 可以说,由于现实中的 RDBMS 常常包含需要保留的数据,仅仅删除模式然后通过 DDL 语句重新构建模式不是 正确的选择。

现在我们知道,当定义其中的对象的 Java 代码发生改变时 RDBMS 会发生什么样的变化。(或者,至少我们知道 RDBMS 管理器会怎样,这是一个很头痛的问题。)现在我们来看看当代码发生改变时 db4o 数据库的反应。

 



 

设置数据库

如果您已经阅读了本系列中的前两篇文章,那么应该熟悉我的非常简单的数据库。目前,它由一种类型组成,即 Person 类型,该类型的定义包含在清单 1 中:


清单 1. Person

                package com.tedneward.model;

public class Person
{
    public Person()
    { }
    public Person(String firstName, String lastName, int age)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String value) { firstName = value; }
    
    public String getLastName() { return lastName; }
    public void setLastName(String value) { lastName = value; }
    
    public int getAge() { return age; }
    public void setAge(int value) { age = value; }

    public String toString()
    {
        return 
            "[Person: " +
            "firstName = " + firstName + " " +
            "lastName = " + lastName + " " +
            "age = " + age + 
            "]";
    }
    
    public boolean equals(Object rhs)
    {
        if (rhs == this)
            return true;
        
        if (!(rhs instanceof Person))
            return false;
        
        Person other = (Person)rhs;
        return (this.firstName.equals(other.firstName) &&
                this.lastName.equals(other.lastName) &&
                this.age == other.age);
    }
    
    private String firstName;
    private String lastName;
    private int age;
}

 

接下来,我填充数据库,如清单 2 所示:


清单 2. ‘t0’ 时的数据库

                import java.io.*;
import java.lang.reflect.*;
import com.db4o.*;
import com.tedneward.model.*;

// Version 1
public class BuildV1
{
    public static void main(String[] args)
        throws Exception
    {
        new File(".", "persons.data").delete();
        
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");

            Person brianG = new Person("Brian", "Goetz", 39);
            Person jason = new Person("Jason", "Hunter", 35);
            Person brianS = new Person("Brian", "Sletten", 38);
            Person david = new Person("David", "Geary", 55);
            Person glenn = new Person("Glenn", "Vanderberg", 40);
            Person neal = new Person("Neal", "Ford", 39);
            Person clinton = new Person("Clinton", "Begin", 19);
            
            db.set(brianG);
            db.set(jason);
            db.set(brianS);
            db.set(david);
            db.set(glenn);
            db.set(neal);
            db.set(clinton);

            db.commit();
            
            // Find all the Brians
            ObjectSet brians = db.get(new Person("Brian", null, 0));
            while (brians.hasNext())
                System.out.println(brians.next());            
        }
        finally
        {
            if (db != null)
                db.close(); 
        }
    }
}

 

注意,在清单 2 中开始的代码片段中,我显式地删除了文件 “persons.data”。这样做可以保证一开始就有整洁的记录。在 Build 应用程序将来的版本中,为了演示重构过程,我将保持 persons.data 文件不动。还需注意,Person 类型将来要发生改变(这将是我的重构的重点),所以请务必熟悉为每个例子存储和/或取出的版本。(请在 本文的源代码 中查看每个版本的 Person 中的注释,以及源代码树中的 Person.java.svn 文件。这些内容有助于理解例子。)

 



 

第一次重构

到目前为止,公司一直运营良好。公司数据库填满了 Person ,可以随时查询、存储和使用,基本上可以满足每个人的需求。但公司高层读了一本最近非常畅销的叫做 People have feelings too! 的管理书籍之后,决定修改该数据库,以包括 Person 的情绪(mood)。

在传统的对象/关系场景中,这意味着两个主要任务:一是重构代码(下面我会对此进行讨论),二是重构数据库模式,以包括反映 Person 情绪的新数据。现在,Scott Ambler 已经有了一些 RDBMS 重构方面的很好的资源(见 参考资料 ),但重构关系数据库远比重构 Java 代码复杂这一事实丝毫没有改变,而在必须保留已有生产数据的情况下,这一点特别明显。

然而,在 OODBMS 中,事情就变得简单多了,因为重构完全发生在代码(在这里就是 Java 代码)中。需要记住的是,在 OODBMS 中,代码就是 模式。因此可以说, OODBMS 提供一个 “单一数据源(single source of truth)”,而不是 O/R 世界,即数据被编码在两个不同的位置:数据库模式和对象模型。(两者发生冲突时哪一方 “胜出”,这是 Java 开发人员中争论得很多的一个话题。

重构数据库模式

我的第一步是创建一个新类型,用于定义要跟踪的所有情绪。使用一个 Java 5 枚举类型就很容易做到这一点,如清单 3 所示:


清单 3. Howyadoin'?(你好吗?)

                package com.tedneward.model;

public enum Mood
{
    HAPPY, CONTENT, BLAH, CRANKY, DEPRESSED, PSYCHOTIC, WRITING_AN_ARTICLE
}

 

第二步,我需要更改 Person 代码,添加一个字段和一些用于跟踪情绪的属性方法,如清单 4 所示:


清单 4. No, howYOUdoin'?(不,你好吗?)

                package com.tedneward.model;

// Person v2
public class Person
{
    // ... as before, with appropriate modifications to public constructor and
    // toString() method
    
    public Mood getMood() { return mood; }
    public void setMood(Mood value) { mood = value; }

    private Mood mood;
}

 

检查 db4o

在做其它事情之前,我们先来看看 db4o 对查找数据库中所有 Brian 的查询如何作出响应。换句话说,当数据库中没有存储 Mood 实例时,如果在数据库上运行一个基于已有的 Person 的查询,db4o 将如何作出响应(见清单 5)?


清单 5. 每个人都还好吗?

                import com.db4o.*;
import com.tedneward.model.*;

// Version 2
public class ReadV2
{
    public static void main(String[] args)
        throws Exception
    {
        // Note the absence of the File.delete() call



        
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");

            // Find all the Brians
            ObjectSet brians = db.get(new Person("Brian", null, 0, null));
            while (brians.hasNext())
                System.out.println(brians.next());            
        }
        finally
        {
            if (db != null)
                db.close(); 
        }
    }
}

 

结果有些令人吃惊,如清单 6 所示:


清单 6. db4o 应付自如

                
[Person: firstName = Brian lastName = Sletten age = 38 mood = null]
[Person: firstName = Brian lastName = Goetz age = 39 mood = null]

 

Person 的两个定义(一个在磁盘上,另一个在代码中)并不一致的事实面前,db4o 不但没有 卡壳,而且更进了一步:它查看磁盘上的数据,确定那里的 Person 实例没有 mood 字段,并静默地用默认值 null 替代。(顺便说一句,在这种情况下,Java Object Serialization API 也是这样做的。)

这 里最重要的一点是,db4o 静默地处理它看到的磁盘上的数据与类型定义之间的不匹配。这成为贯穿 db4o 重构行为的永恒主题:db4o 尽可能静默地处理版本失配。它或者扩展磁盘上的元素以包括添加的字段,或者,如果给定 JVM 中使用的类定义中不存在这些字段,则忽略它们。

 



 

代码到磁盘兼容性

db4o 对磁盘上缺失的或不必要的字段进行某种调整,这一思想需要解释一下,所以让我们看看当更新磁盘上的数据以包括情绪时会出现什么情况,如清单 7 所示:


清单 7. 我们很好

                import com.db4o.*;
import com.tedneward.model.*;

// Version 2
public class BuildV2
{
    public static void main(String[] args)
        throws Exception
    {
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");

            // Find all the Persons, and give them moods
            ObjectSet people = db.get(Person.class);
            while (people.hasNext())
            {
                Person person = (Person)people.next();
                
                System.out.print("Setting " + person.getFirstName() + "'s mood ");
                int moodVal = (int)(Math.random() * Mood.values().length);
                person.setMood(Mood.values()[moodVal]);
                System.out.println("to " + person.getMood());
                db.set(person);
            }
            
            db.commit();            
        }
        finally
        {
            if (db != null)
                db.close(); 
        }
    }
}

 

在清单 7 中,我发现数据库中的所有 Person ,并随机地为他们赋予 Mood 。在更现实的应用程序中,我会使用一组基准数据,而不是随即选择数据,但对于这个例子而言这样做也行。运行上述代码后产生清单 8 所示的输出:


清单 8. 今天每个人的情绪如何?

                Setting Brian's mood to BLAH
Setting David's mood to WRITING_AN_ARTICLE
Setting Brian's mood to CONTENT
Setting Jason's mood to PSYCHOTIC
Setting Glenn's mood to BLAH
Setting Neal's mood to HAPPY
Setting Clinton's mood to DEPRESSED

 

可以通过再次运行 ReadV2 验证该输出。最好是再运行一下初始的查询版本 ReadV1 (该版本看上去很像 ReadV2,只是它是在 V1 版本的 Person 的基础上编译的)。 运行之后,产生如下输出:


清单 9. 旧版的 ‘今天每个人的情绪如何?’

                
[Person: firstName = Brian lastName = Sletten age = 38]
[Person: firstName = Brian lastName = Goetz age = 39]

 

对于清单 9 中的输出,值得注意的是,与我将 Mood 扩展添加到 Person 类(在 清单 6 中)之前 db4o 的输出相比,这里的输出并无不同 —— 这意味着 db4o 是同时向后兼容和向前兼容的。

 



 

再度重构!

假设要更改已有类中一个字段的类型,例如将 Person 的 age 从整型改为短整型。(毕竟,通常没有人能活过 32,000 岁 —— 而且我相信,即使真的 有那么长寿的人,仍然可以重构代码,将该字段改回整型。)假定两种类型在本质上是类似的,就像 int 与 short,或 float 与 double,db4o 只是静默地处理更改 —— 同样是或多或少仿效 Java Object Serialization API。这种操作的缺点是,db4o 可能会意外地将一个值截尾。只有当一个值 “转换为范围更小的类型” 时,即这个值超出了新类型允许的范围,例如试图从 long 类型转换为 int 类型,才会发生这样的情况。货物出门概不退货,买主需自行小心 —— 在开发期间或原型开发期间,务必进行彻底的单元测试。

db4o 保存旧的数据吗?
如果删除一个字段,然后再添加那个字段,db4o 竟然可以找回那个字段当初存在时原有的值。不,db4o 不会永远跟踪所有被删除的字段的值 —— 当数据库被请求执行所谓的碎片整理操作时,它就会删除那些值。本系列将来的文章中会更详细地谈到碎片整理,请继续关注。

实际 上,db4o 向后兼容的妙法值得解释一下。基本上,当 db4o 看到新类型的字段时,就会在磁盘上创建一个新字段,该字段有相同的名称,但是具有新的类型,就好像它是添加到类中的另一个新字段一样。这还意味着,旧的值 仍然保留在旧类型的字段中。因此,通过将字段重构回初始值,总可以 “回调” 旧值,取决于观察问题的角度,这可以说是一个特性,也可以说是一个 bug。

注意,对类中方法的更改与 db4o 无关,因为它不将方法或方法实现作为存储的对象数据的一部分,对于构造函数的重构也是如此。只有字段和类名本身(接下来会进行讨论)对于 db4o 才是重要的。

 



 

第三次重构比较困难

在某些情况下,需要发生的重构可能更剧烈一些,例如整个更改一个类的名称(可以是类名,也可以是类所在的包的名称)。像这样的更改对于 db4o 是比较大的更改,因为它需要根据 classname 来存储对象。例如,当 db4o 查找 Person 实例时,它在标有名称 com.tedneward.model.Person 的块的特定区域中进行查找。因此,改变名称会使 db4o 不知所措:它不能魔术般地推断 com.tedneward.model.Person 现在就是 com.tedneward.persons.model.Individual 。幸运的是,有两种方法可以教会 db4o 如何管理这样的转换。

更改磁盘上的名称

使 db4o 适应这样剧烈的更改的一种方法是编写自己的重构工具,使用 db4o Refactoring API 打开已有的数据文件,并更改在磁盘上的名称。可以通过一组简单的调用做到这一点,如清单 10 所示:


清单 10. 从 Person 重构为 Individual

                import com.db4o.*;
import com.db4o.config.*;

// ...

Db4o.configure().objectClass("com.tedneward.model.Person")
    .rename("com.tedneward.persons.model.Individual");

 

注意,清单 10 中的代码使用 db4o Configuration API 获得一个配置对象,该配置对象被用作对 db4o 的大多数选项的 “元控制(meta-control)” —— 在运行时,您将使用这个 API 而不是命令行标志或配置文件来设置特定的设置(虽然您完全可以创建自己的命令行标志或配置文件来驱动 Configuration API 调用)。然后,使用 Configuration 对象获得 Person 类的 ObjectClass 实例……或者更确切地说,是表示磁盘上存储的 Person 实例的 ObjectClass 实例。ObjectClass 还包含很多其它选项,在本系列的后面我会展示其中的一些选项。

使用别名

在某些情况下,磁盘上的数据必须存在,以支持由于技术或策略上的某种原因而不能重新编译的早期应用程序。在这些情况下,V2 应用程序必须能够提取 V1 实例,并在内存中将它们转换成 V2 实例。 幸运的是,在向磁盘存储并从中检索对象时,可以依靠 db4o 的别名 特性创建一个 shuffle 步骤。这样便可以区别内存中使用的类型和存储的类型。

db4o 支持三种类型的别名,其中一种类型只有当 .NET 和 Java 风格的 db4o 之间共享数据文件时才有用。 清单 11 中出现的别名是 TypeAlias ,它有效地告诉 db4o 用内存中的 “A” 类型(运行时名称 )替换磁盘上的 “B” 类型(存储的名称 )。启用这种别名是一种双线操作。


清单 11. TypeAlias shuffle

                import com.db4o.config.*;

// ...

TypeAlias fromPersonToIndividual = 
    new TypeAlias("com.tedneward.model.Person", "com.tedneward.persons.model.Individual");
Db4o.configure().addAlias(fromPersonToIndividual);

 

当运行时,db4o 现在将查询数据库中的 Individual 对象的任何调用识别为一个请求,而不会查找存储的 Person 实例;这意味着,Individual 类中的名称和类型应该和 Person 中存储的名称和类型类似,db4o 将适当地处理它们之间的映射。然后,Individual 实例将被存储在 Person 名称之下。

更多重构方法
我还没有谈到 db4o 支持重构的所有方法,也就是说还有很多要学的东西。即使您发现 db4o 的重构选项不能很好地处理自己的情况,也仍然有旧的后备选项可用,您可以在要求的位置用一个临时名称创建新类,编写一些代码从旧类创建新类的对象,然后删 除旧的对象,并将临时类重新命名为适当的名称。如果急于知道这种选项,请参阅 db4o 的 doc\reference directory 的 Advanced Type Handling 小节中的 “Refactoring and meta-information”。

结束语

由 于对象数据库中的模式就是类定义本身,而不是采用不同语言的单独的 DDL 定义,因此本文中的每个重构例子都显得简单很多。db4o 中的重构是使用代码完成的,常常可以通过一个配置调用来确定,最坏情况也只不过是编写和运行一个转换实用程序,以将已有实例从旧的类型更新为新的类型。而 且这种类型的转换对于几乎所有生产中的 RDBMS 重构都是必需的。

db4o 强大的重构能力使之在开发期间非常有用,因为在开发期间,正在设计的很多对象仍然是变化无常的,即使不需要每个小时都重构,至少也需要每天都重构。如果将 db4o 用于单元测试和测试驱动开发,则可以节省大量更改数据库的时间,如果重构只是简单的字段添加/删除或类型/名称更改,这一点就更加明显了。

这就是本文讨论的内容,但是请记住:如果要用对象编写应用程序,并且持久性存储实际上 “只是和实现有关”,那么为什么非得把很好的对象限制成规规矩矩、四四方方的样子呢?

 

以下是db4o 中的数据库重构

你可能感兴趣的:(java,设计模式,配置管理,jpa,单元测试)