面向 Java 开发人员的 db4o 指南:第 4 部分:超越简单对象

2024-01-26 20:08

本文主要是介绍面向 Java 开发人员的 db4o 指南:第 4 部分:超越简单对象,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

到目前为止,我们在 db4o 中创建并操作对象看起来都比较简单 —— 事实上,甚至有点 简单了。本文中,热心于 db4o 的 Ted Neward 将超越这些简单对象,他将展示简单对象结构化(引用对象的对象)时发生的操作。此外,他还阐述了包括无限递归、层叠行为以及引用一致性在内的一些话题。

 

一段时间以来,在 面向 Java 开发人员的 db4o 指南 中,我查看了各种使用 db4o 存储 Java 对象的方法,这些方法都不依赖映射文件。 使用原生对象数据库的其中一个优点就是可以避免对象关系映射(也许这不是重点),但我曾用于阐述这种优点的对象模型过于简单,绝大多数企业系统要求创建并操作相当复杂的对象,也称为结构化对象 ,因此本文将讨论结构化对象的创建。

结构化对象 基本上可以看成是一个引用其他对象的对象。尽管 db4o 允许对结构化对象执行所有常用的 CRUD 操作,但是用户却必须承受一定的复杂性。本文将探究一些主要的复杂情况(比如无限递归、层叠行为和引用一致性),以后的文章还将深入探讨更加高级的结构化 对象处理问题。作为补充,我还将介绍探察测试(exploration test) :一种少为人知的可测试类库 db4o API 的测试技术。

从简单到结构化

清单 1 重述了我在介绍 db4o 时一直使用的一个简单类 Person


清单 1. Person

                
package com.tedneward.model;public class Person
{public Person(){ }public Person(String firstName, String lastName, int age, Mood mood){this.firstName = firstName;this.lastName = lastName;this.age = age;this.mood = mood;}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 Mood getMood() { return mood; }public void setMood(Mood value) { mood = value; }public String toString(){return "[Person: " +"firstName = " + firstName + " " +"lastName = " + lastName + " " +"age = " + age + " " + "mood = " + mood +"]";}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;private Mood mood;
}

 

OODBMS 系统中的 String
您可能还记得,在我此前的文章示例中,Person 类型使用 String 作为字段。在 Java 和 .NET 里,String 是一种对象类型,从 Object 继承而来,这似乎有些矛盾。事实上,包括 db4o 在内的绝大多数 OODBMS 系统在对待 String 上与其他对象都有不同, 尤其针对 String 的不可变(immutable)特性。

这个简单的 Person 类在用于介绍基本 db4o 存储、查询和检索数据操作时行之有效,但它无法满足真实世界中企业编程的复杂性。 举例而言,数据库中的 Person 有家庭地址是很正常的。有些情况下,还可能需要配偶以及子女。

若要在数据库里加一个 “Spouse” 字段,这意味着要扩展 Person ,使它能够引用 Spouse 对象。假设按照某些业务规则,还需要添加一个 Gender 枚举类型及其对应的修改方法,并在构造函数里添加一个 equals() 方法。在清单 2 中,Person 类型有了配偶字段和对应的 get/set 方法对,此时还附带了某些业务规则:


清单 2. 这个人到了结婚年龄吗?

                
package com.tedneward.model;
public class Person {// . . .public Person getSpouse() { return spouse; }public void setSpouse(Person value) { // A few business rulesif (spouse != null)throw new IllegalArgumentException("Already married!");if (value.getSpouse() != null && value.getSpouse() != this)throw new IllegalArgumentException("Already married!");spouse = value; // Highly sexist business ruleif (gender == Gender.FEMALE)this.setLastName(value.getLastName());// Make marriage reflexive, if it's not already set that wayif (value.getSpouse() != this)value.setSpouse(this);}private Person spouse;    
}

 

清单 3 中的代码创建了两个到达婚龄的 Person ,代码和您预想的很接近:


清单 3. 去礼堂,要结婚了……

                
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;public class App
{public static void main(String[] args)throws Exception{ObjectContainer db = null;try{db = Db4o.openFile("persons.data");Person ben = new Person("Ben", "Galbraith", Gender.MALE, 29, Mood.HAPPY);Person jess = new Person("Jessica", "Smith", Gender.FEMALE, 29, Mood.HAPPY);ben.setSpouse(jess);System.out.println(ben);System.out.println(jess);db.set(ben);db.commit();List<Person> maleGalbraiths = db.query(new Predicate<Person>() {public boolean match(Person candidate) {return candidate.getLastName().equals("Galbraith") &&candidate.getGender().equals(Gender.MALE);}});for (Person p : maleGalbraiths){System.out.println("Found " + p);}}finally{if (db != null)db.close(); }}
}

 

开始变得复杂了

除了讨厌的业务规则之外,有几个重要的情况出现了。首先,当对象 ben 存储到数据库后,OODBMS 除了存储一个对象外,显然还做了其他一些事情。 再次检索 ben 对象时,与之相关的配偶信息不仅已经存储而且还被自动检索。

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

思考一下,这包含了可怕的暗示。尽管可以想见 OODBMS 是如何避免无限递归 的场景, 更恐怖的问题在于,设想一个对象有着对其他 几十个、成百上千个对象的引用,每个引用对象又都有着 其自身对其他对象的引用。不妨考虑一下模型表示子女、双亲等的情景。 仅仅是从数据库中取出一个 Person 就会导致 追溯到所有人类的源头。这意味着在网络上传输大量对象!

幸运的是,除了那些最原始的 OODBMS,几乎所有的 OODBMS 都已解决了这个问题,db4o 也不例外。

db4o 的探察测试

考察 db4o 的这个领域是一项棘手的任务,也给了我一个机会 展示一位好友教给我的策略:探察测试 。(感谢 Stu Halloway,据我所知,他是第一个拟定该说法的人。) 探察测试,简要而言,是一系列单元测试,不仅测试待查的库,还可探究 API 以确保库行为与预期一致。该方法具有一个有用的副作用,未来的库版本可以放到探察测试代码中,编译并且测试。如果代码不能编译或者无法通过所有的探察测试,则显然意味着库没有做到向后兼容,您就可以在用于生产系统之前发现这个问题。

对 db4o API 的探察测试使我能够使用一种 “before” 方法来创建数据库并使用 Person 填充数据库,并使用 “after” 方法来删除数据库并消除测试过程中发生的误判(false positive)。若非如此,我将不得不记得每次手工删除 persons.data 文件。 坦白说,我并不相信自己在探索 API 的时候还能每次都记得住。

我在进行 db4o 探察测试时,在控制台模式使用 JUnit 4 测试库。写任何测试代码前,StructuredObjectTest 类如清单 4 所示:


清单 4. 影响 db4o API 的测试

                
import java.io.*;
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;import org.junit.Before;
import org.junit.After; 
import org.junit.Ignore; 
import org.junit.Test; 
import static org.junit.Assert.*;public class StructuredObjectsTest
{ObjectContainer db;@Before public void prepareDatabase(){db = Db4o.openFile("persons.data");Person ben = new Person("Ben", "Galbraith", Gender.MALE, 29, Mood.HAPPY);Person jess = new Person("Jessica", "Smith", Gender.FEMALE, 29, Mood.HAPPY);ben.setSpouse(jess);db.set(ben);db.commit();}@After public void deleteDatabase(){db.close();new File("persons.data").delete();}@Test public void testSimpleRetrieval(){List<Person> maleGalbraiths = db.query(new Predicate<Person>() {public boolean match(Person candidate) {return candidate.getLastName().equals("Galbraith") &&candidate.getGender().equals(Gender.MALE);}});// Should only have one in the returned setassertEquals(maleGalbraiths.size(), 1);// (Shouldn't display to the console in a unit test, but this is an// exploration test, not a real unit test)for (Person p : maleGalbraiths){System.out.println("Found " + p);}}
}

 

自然,针对这套测试运行 JUnit 测试运行器会生成预计输出:要么是“.”,要么是绿条,这与所选择的测试运行器有关(控制台或 GUI)。注意,一般不赞成向控制台写数据 —— 应该用断言进行验证,而不是用眼球 —— 不过在探察测试里,做断言之前看看得到的数据是个好办法。如果有什么没通过,我总是可以注释掉 System.out.println 调用。(可以自由地添加,以测试您想测试的其他 db4o API 特性。)

从这里开始,假定清单 4 中的测试套件包含了代码示例和测试方法(由方法签名中的 @Test 注释指明。)。

 




 

存取结构化对象

存储结构化对象很大程度上和以前大部分做法一样:对对象调用 db.set() ,OODBMS 负责其余的工作。对哪个对象调用 set() 并不重要,因为 OODBMS 通过对象标识符(OID)对对象进行了跟踪(参阅 “ 面向 Java 开发人员的 db4o 指南: 查询、更新以及一致性 ”),因此不会对同一对象进行两次存储。

Retrieving 结构化对象则令我不寒而栗。如果要检索的对象(无论是通过 QBE 或原生查询) 拥有大量对象引用,而每个被引用的对象也有着大量的对象引用,以此类推。这有一点像糟糕的 Ponzi 模式,不是吗?

避免无限递归

不管大多数开发者的最初反应(一般是 “不可能 是这样的吧,是吗?”)如何, 无限递归在某种意义上正是 db4o 处理结构化对象的真正方式。事实上,这种方式是绝大多数程序员希望的,因为我们都希望在寻找所创建的对象时,它们正好 “就在那里”。同时,我们也显然不想通过一根线缆获得整个世界的信息,至少不要一次就得到。

db4o 对此采用了折衷的办法,限制所检索的对象数量,使用称为激活深度(activation depth) 的方法,它指明在对象图中进行检索的最低层。换句话说,激活深度表示从根对象中标识的引用总数,db4o 将在查询中遍历根对象并返回结果。在前面的例子中,当检索 Ben 时,默认的激活深度 5 足够用于检索 Jessica ,因为它只需要 仅仅一个引用遍历。任何距离 Ben 超过 5 个引用的对象将无法 被检索到, 它们的引用将置为空。我的工作就是 显式地从数据库激活那些对象,在 ObjectContainer 使用 activate() 方法。

如果要改变默认激活深度, 需要以一种精密的方式,在 Configuration 类(从 db.configure() 返回)中使用 db4o 的 activationDepth() 方法修改默认值。 还有一种方式,可以对每个类配置激活深度。 在清单 5 中,使用 ObjectClassPerson 类型配置默认激活深度:


清单 5. 使用 ObjectClass 配置激活深度

                
// See ObjectClass for more info
Configuration config = Db4o.configure();
ObjectClass oc = config.objectClass("com.tedneward.model.Person");
oc.minimumActivationDepth(10);

 




 

更新结构化对象

更新所关注的是另外一个问题:如果在对象图中更新一个对象,但并没有做显式设置, 那么会发生什么?正如最初调用 set() 时,将存储引用了其他存储对象的相关对象,与之相似, 当一个对象传递到 ObjectContainer ,db4o 遍历所有引用,将发现的对象存储到数据库中,如清单 6 所示:


清单 6. 更新被引用的对象

                
@Test public void testDependentUpdate()
{List<Person> maleGalbraiths = db.query(new Predicate<Person>() {public boolean match(Person candidate) {return candidate.getLastName().equals("Galbraith") &&candidate.getGender().equals(Gender.MALE);}});Person ben = maleGalbraiths.get(0);// Happy birthday, Jessica!ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);// We only have a reference to Ben, so store that and commitdb.set(ben);db.commit();// Find Jess, make sure she's 30Person jess = (Person)db.get(new Person("Jessica", "Galbraith", null, 0, null)).next();assertTrue(jess.getAge() == 30);
}

 

尽管已经对 jess 对象做了变动, ben 对象还拥有对 jess 的引用。因此内存中 jess Person 的更新会保存在数据库中。

其实不是这样。好的,我刚才是在撒谎。

测试误判

事实是,探察测试在某个地方出问题了,产生了一个误判。 尽管从文档来看并不明显, ObjectContainer 保持着已激活对象的缓存, 所以当清单 6 中的测试从容器中检索 Jessica 对象时,返回的是 包含变动的内存对象,而不是写到磁盘上真正数据。 这掩盖了一个事实,某类型的默认更新深度 是 1, 意味着只有原语值(包括 String )才会在调用 set() 时被存储。为了使该行为生效,我必须稍微修改一下测试,如清单 7 所示:


清单 7. 测试误判

                
@Test(expected=AssertionError.class)
public void testDependentUpdate()
{List<Person> maleGalbraiths = db.query(new Predicate<Person>() {public boolean match(Person candidate) {return candidate.getLastName().equals("Galbraith") &&candidate.getGender().equals(Gender.MALE);}});Person ben = maleGalbraiths.get(0);assertTrue(ben.getSpouse().getAge() == 29);// Happy Birthday, Jessica!ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);// We only have a reference to Ben, so store that and commitdb.set(ben);db.commit();// Close the ObjectContainer, then re-open itdb.close();db = Db4o.openFile("persons.data");// Find Jess, make sure she's 30Person jess = (Person)db.get(new Person("Jessica", "Galbraith", null, 0, null)).next();assertTrue(jess.getAge() == 30);
}

 

测试时,得到 AssertionFailure , 说明此前有关对象图中层叠展开的对象更新的论断是错误的。(通过将您希望抛出异常的类类型的 @Test 注释的值设置为 expected ,可以使 JUit 提前预测到这种错误。)

设置层叠行为

Db4o 仅仅返回缓存对象,而不对其更多地进行隐式处理,这是一个有争议的话题。 很多编程人员认为 要么这种行为是有害的并且违反直觉,要么 这种行为正是 OODBMS 应该做的。不要去管这两种观点优劣如何, 重要的是理解数据库的默认行为并且知道如何修正。在清单 8 中,使用 ObjectClass.setCascadeOnUpdate() 方法为一特定类型改变 db4o 的 默认更新动作。不过要注意,在打开 ObjectContainer 之前,必须 设定该方法为 true 。清单 8 展示了修改后的正确的层叠测试。


清单 8. 设置层叠行为为 true

                
@Test
public void testWorkingDependentUpdate()
{// the cascadeOnUpdate() call must be done while the ObjectContainer // isn't open, so close() it, setCascadeOnUpdate, then open() it againdb.close();Db4o.configure().objectClass(Person.class).cascadeOnUpdate(true);db = Db4o.openFile("persons.data");List<Person> maleGalbraiths = db.query(new Predicate<Person>() {public boolean match(Person candidate) {return candidate.getLastName().equals("Galbraith") &&candidate.getGender().equals(Gender.MALE);}});Person ben = maleGalbraiths.get(0);assertTrue(ben.getSpouse().getAge() == 29);// Happy Birthday, Jessica!ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);// We only have a reference to Ben, so store that and commitdb.set(ben);db.commit();// Close the ObjectContainer, then re-open itdb.close();db = Db4o.openFile("persons.data");// Find Jess, make sure she's 30Person jess = (Person)db.get(new Person("Jessica", "Galbraith", null, 0, null)).next();assertTrue(jess.getAge() == 30);
}

 

不仅可以为更新设置层叠行为,也可以对检索(创建值为 “unlimited” 的激活深度)和删除设置层叠行为 —— 这是我最新琢磨的 Person 对象的最后一个应用 。

 



 

删除结构化对象

从数据库中删除对象与检索和更新对象类似: 默认情况下,删除一个对象时,不删除它引用的对象。 一般而言,这也是理想的行为。如清单 9 所示:


清单 9. 删除结构化对象

                
@Test
public void simpleDeletion()
{Person ben = (Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();db.delete(ben);Person jess = (Person)db.get(new Person("Jessica", "Galbraith", null, 0, null)).next();assertNotNull(jess);
}

 

但是,有些时候在删除对象时,希望强制删除其引用的对象。 与激活和更新一样, 可以通过调用 Configuration 类触发此行为。如清单 10 所示:


清单 10. Configuration.setCascadeOnDelete()

                
@Test
public void cascadingDeletion()
{// the cascadeOnUpdate() call must be done while the ObjectContainer // isn't open, so close() it, setCascadeOnUpdate, then open() it againdb.close();Db4o.configure().objectClass(Person.class).cascadeOnDelete(true);db = Db4o.openFile("persons.data");Person ben = (Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();db.delete(ben);ObjectSet<Person> results = db.get(new Person("Jessica", "Galbraith", null, 0, null));assertFalse(results.hasNext());
}

 

执行该操作时要小心,因为它意味着其他引用了被消除层叠的对象的对象将拥有一个对 null 的引用 —— db4o 对象数据库在防止删除被引用对象上使用的引用一致性 在这里没有什么作用。 (引用一致性是 db4o 普遍需要的特性,据说开发团队正在考虑在未来某个版本中加入这一特性。对于使用 db4o 的开发人员来说,关键在于要以一种不违反最少意外原则 的方式实现,甚至某些时候, 即使是在关系数据库中, 打破一致性规则实际上也是一种理想的实践。)

 





 

结束语

本文是该系列文章的分水岭:在此之前, 我使用的所有示例都基于非常简单的对象,从应用角度来讲, 那些例子都不现实,其主要作用只是为了使您理解 OODBMS,而不是被存储的对象。 理解像 db4o 这样的 OODBMS 是如何通过引用存储相关对象,是比较复杂的事情。 幸运的是,一旦您掌握了这些行为(通过解释和理解),您所要做的就只是 开始调整代码来实现这些行为。

在本文中,您看到了一些基本例子,通过调整复杂代码来实现 db4o 对象模型。 学习了如何对结构化对象执行一些简单 CRUD 操作,同时,也看到了一些 不可避免的问题和解决方法。

其实,目前的结构化对象例子仍然比较简单, 对象之间还只是直接引用关系。 许多夫妻都知道,结婚一段时间后,孩子将会出现。 本系列的下一文章中,我将继续 探索 db4o 中的结构化对象的创建与操作,看看在引入若干子对象后, benjess 对象将发生什么。

这篇关于面向 Java 开发人员的 db4o 指南:第 4 部分:超越简单对象的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/647921

相关文章

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

利用Python编写一个简单的聊天机器人

《利用Python编写一个简单的聊天机器人》这篇文章主要为大家详细介绍了如何利用Python编写一个简单的聊天机器人,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 使用 python 编写一个简单的聊天机器人可以从最基础的逻辑开始,然后逐步加入更复杂的功能。这里我们将先实现一个简单的

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Python使用qrcode库实现生成二维码的操作指南

《Python使用qrcode库实现生成二维码的操作指南》二维码是一种广泛使用的二维条码,因其高效的数据存储能力和易于扫描的特点,广泛应用于支付、身份验证、营销推广等领域,Pythonqrcode库是... 目录一、安装 python qrcode 库二、基本使用方法1. 生成简单二维码2. 生成带 Log

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去