2007 年 6 月 08 日
重构 Java 代码远远比重构关系数据库简单,但幸运的是,对于对象数据库却并非如此。在本期的 面向 Java 开发人员的 db4o 指南 中,Ted Neward 介绍他喜欢的对象数据库的另一个优点:db4o 简化了重构,使之变得非常容易。
在 本系列的上一篇文章 中,我谈到了查询 RDBMS 与查询像 db4o 这样的对象数据库的不同之处。正如我所说的那样,与通常的关系数据库相比, db4o 可以提供更多的方法来进行查询,为您处理不同应用程序场景提供了更多选择。
这一次,我将继续这一主题 —— db4o 的众多选项 —— 看看 db4o 如何处理重构。自 6.1 版开始,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 中:
package com.tedneward.model; |
接下来,我填充数据库,如清单 2 所示:
import java.io.*; |
注意,在清单 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 所示:
package com.tedneward.model; |
第二步,我需要更改 Person 代码,添加一个字段和一些用于跟踪情绪的属性方法,如清单 4 所示:
package com.tedneward.model; |
在做其它事情之前,我们先来看看 db4o 对查找数据库中所有 Brian 的查询如何作出响应。换句话说,当数据库中没有存储 Mood 实例时,如果在数据库上运行一个基于已有的 Person 的查询,db4o 将如何作出响应(见清单 5)?
import com.db4o.*; |
结果有些令人吃惊,如清单 6 所示:
|
在 Person 的两个定义(一个在磁盘上,另一个在代码中)并不一致的事实面前,db4o 不但没有 卡壳,而且更进了一步:它查看磁盘上的数据,确定那里的 Person 实例没有 mood 字段,并静默地用默认值 null 替代。(顺便说一句,在这种情况下,Java Object Serialization API 也是这样做的。)
这 里最重要的一点是,db4o 静默地处理它看到的磁盘上的数据与类型定义之间的不匹配。这成为贯穿 db4o 重构行为的永恒主题:db4o 尽可能静默地处理版本失配。它或者扩展磁盘上的元素以包括添加的字段,或者,如果给定 JVM 中使用的类定义中不存在这些字段,则忽略它们。
![]() ![]() |
![]()
|
db4o 对磁盘上缺失的或不必要的字段进行某种调整,这一思想需要解释一下,所以让我们看看当更新磁盘上的数据以包括情绪时会出现什么情况,如清单 7 所示:
import com.db4o.*; |
在清单 7 中,我发现数据库中的所有 Person,并随机地为他们赋予 Mood。在更现实的应用程序中,我会使用一组基准数据,而不是随即选择数据,但对于这个例子而言这样做也行。运行上述代码后产生清单 8 所示的输出:
Setting Brian's mood to BLAH |
可以通过再次运行 ReadV2 验证该输出。最好是再运行一下初始的查询版本 ReadV1(该版本看上去很像 ReadV2,只是它是在 V1 版本的 Person 的基础上编译的)。 运行之后,产生如下输出:
|
对于清单 9 中的输出,值得注意的是,与我将 Mood 扩展添加到 Person 类(在 清单 6 中)之前 db4o 的输出相比,这里的输出并无不同 —— 这意味着 db4o 是同时向后兼容和向前兼容的。
![]() ![]() |
![]()
|
假设要更改已有类中一个字段的类型,例如将 Person 的 age 从整型改为短整型。(毕竟,通常没有人能活过 32,000 岁 —— 而且我相信,即使真的 有那么长寿的人,仍然可以重构代码,将该字段改回整型。)假定两种类型在本质上是类似的,就像 int 与 short,或 float 与 double,db4o 只是静默地处理更改 —— 同样是或多或少仿效 Java Object Serialization API。这种操作的缺点是,db4o 可能会意外地将一个值截尾。只有当一个值 “转换为范围更小的类型” 时,即这个值超出了新类型允许的范围,例如试图从 long 类型转换为 int 类型,才会发生这样的情况。货物出门概不退货,买主需自行小心 —— 在开发期间或原型开发期间,务必进行彻底的单元测试。
![]() |
|
实际 上,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 所示:
import com.db4o.*; |
注意,清单 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” 类型(存储的名称)。启用这种别名是一种双线操作。
import com.db4o.config.*; |
当运行时,db4o 现在将查询数据库中的 Individual 对象的任何调用识别为一个请求,而不会查找存储的 Person 实例;这意味着,Individual 类中的名称和类型应该和 Person 中存储的名称和类型类似,db4o 将适当地处理它们之间的映射。然后,Individual 实例将被存储在 Person 名称之下。
![]() |
|
由 于对象数据库中的模式就是类定义本身,而不是采用不同语言的单独的 DDL 定义,因此本文中的每个重构例子都显得简单很多。db4o 中的重构是使用代码完成的,常常可以通过一个配置调用来确定,最坏情况也只不过是编写和运行一个转换实用程序,以将已有实例从旧的类型更新为新的类型。而 且这种类型的转换对于几乎所有生产中的 RDBMS 重构都是必需的。
db4o 强大的重构能力使之在开发期间非常有用,因为在开发期间,正在设计的很多对象仍然是变化无常的,即使不需要每个小时都重构,至少也需要每天都重构。如果将 db4o 用于单元测试和测试驱动开发,则可以节省大量更改数据库的时间,如果重构只是简单的字段添加/删除或类型/名称更改,这一点就更加明显了。
这就是本文讨论的内容,但是请记住:如果要用对象编写应用程序,并且持久性存储实际上 “只是和实现有关”,那么为什么非得把很好的对象限制成规规矩矩、四四方方的样子呢?
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-130063/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/374079/viewspace-130063/
基本思想:本质上就是要找出n/2+1个数,并且对于这个选中集合里面的数,在非选中的数中都能分别找到一个与之对应的小于它的数(所以这样的对应为两个数为一组,一个数来自选中集合,一个数来自非选中集合),这样就一定能保证选出数大于剩下没有选中的数,因为a>b ------> a*2 > a+b = sum(a为选中集合元素之和,b为非选中集合元素之和), 满足题目要求。例:1,4,9,6,5,2,选
oracle 数据库、实例、服务名、SID 在实际的开发应用中,关于Oracle数据库,经常听见有人说建立一个数据库,建立一个Instance,启动一个Instance之类的话。其实问他们什么是数据库,什么是Instance,很可能他们给的答案就是数据库就是Instance,Instance就是数据库啊,没有什么区别。在这里,只能说虽然他们Oracle用了可能有了一定的经验,不过基础的概念还是不太清楚。(我目前就是这个状态) 一、什么是数据库,其实很简单,数据库就是存储数据的一种...
oracle学习之sid_name
今天给大家介绍一下opencv图像处理中比较有意思的东西:图像加权和函数这个函数参数有点多,语法格式如下:dst=cv2.addWeighted(src1,alpha,src2,beta,gamma)src1//可以理解为Filenamealpha//图像系数1src2//另一个Filenamebeta//图像系数2gamma//光度调节量,就是字面意思,嗯…我今天本来是想利用这个函数将两张图片做一下加权和,结果过程还蛮曲折的。这是我一开始的代码,我发现运行的时候老报错,我一度以为是我的
过去十年,无线通信的革新与人工智能的复兴使人类社会发生了翻天覆地的变化。这两股新兴力量的交织碰撞,推动着移动通信系统的一步步演进,从一开始的“人联”到“物联”,然后迈向“万物智联”。边缘计算与边缘智能接踵而至,为“万物智联”提供解决方案,被学术界和工业界一致视为是实现下一代移动通信系统的两项关键技术。这期,就和文档君一起了解一下,什么是边缘计算吧。边缘计算旨在将云计算平台迁移到无线接入网的边缘侧,...
§2 LINGO中的集 对实际问题建模的时候,总会遇到一群或多群相联系的对象,比如工厂、消费者群体、交通工具和雇工等等。LINGO允许把这些相联系的对象聚合成集(sets)。一旦把对象聚合成集,就可以利用集来最大限度的发挥LINGO建模语言的优势。现在我们将深入介绍如何创建集,并用数据初始化集的属性。学完本节后,你对基于建模技术的集如何引入模型会有一个基本的理解。
Allen is hosting a formal dinner party. 2n2n people come to the event in nn pairs (couples). After a night of fun, Allen wants to line everyone up for a final picture. The 2n2n people line up, but All...
一、安装JDK1、下载并安装 sudo apt-get install openjdk-6-jdk(安装JDK7为:sudo apt-get install openjdk-7-jdk) 要求输入当前用户密码时输入密码,回车; 要求输入YES/NO时,输入YES,回车,一路向下安装完成;2、在命令行输入JAVA -VERSION查看是否安装成功...
源程序:#include #include #include #include int main(int argc,char **argv){double y;sigset_t intmask;int i,repeat_factor;if(argc!=2){fprintf(stderr,"Usage:%s repeat_factor\n\a",arg
微信公众号1. 传统的循环神经网络传统的神经网络可以看作只有两个time step。如果输入是“Hello”(第一个time step),它会预测“World”(第二个time step),但是它无法预测更多的time step。2. LSTM、GRU等【知乎】如何理解LSTM中的time step? - 知乎 https://www.zhihu.com/question/271...
(I will try my best to make this note clearer.)We mainly focus on solve_c_svc in this note.Our goal: mindB\displaystyle \min_{\mathbf{d}_B} 12dTB∇2f(αk)dB+∇f(αk)TBdB\frac{1}{2}\mathbf{d}_{B}^{T}\nabla