快捷搜索:  www.ymwears.cn

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

一段光阴以来,在 面向 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 rules

if (spouse != null)

throw new IllegalArgumentException("Already married!");

if (value.getSpouse() != null && value.getSpouse() != this)

throw new IllegalArgumentException("Already married!");

spouse = value;

// Highly sexist business rule

if (gender == Gender.FEMALE)

this.setLastName(value.getLastName());

// Make marriage reflexive, if it's not already set that way

if (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

maleGalbraiths =

db.query(new Predicate

() {

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 工具时,与之相关的妃耦信息不仅已经存储而且还被自动检索。

思虑一下,这包孕了可骇的暗示。只管可以想见 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

maleGalbraiths =

db.query(new Predicate

() {

public boolean match(Person candidate) {

return candidate.getLastName().equals("Galbraith") &&

candidate.getGender().equals(Gender.MALE);

}

});

// Should only have one in the returned set

assertEquals(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)对工具进行了跟踪,是以不会对同一工具进行两次存储。

Retrieving 布局化工具则令我毛骨悚然。假如要检索的工具(无论是经由过程 QBE 或原生查询)拥有大年夜量工具引用,而每个被引用的工具也有着大年夜量的工具引用,以此类推。这有一点像糟糕的 Ponzi 模式,不是吗?

避免无限递归

不管大年夜多半开拓者的最初反映(一样平常是 “弗成能是这样的吧,是吗?”)若何,无限递归在某种意义上恰是 db4o 处置惩罚布局化工具的真正要领。事实上,这种要领是绝大年夜多半法度榜样员盼望的,由于我们都盼望在探求所创建的工具时,它们恰恰 “就在那里”。同时,我们也显然不想经由过程一根线缆得到全部天下的信息,至少不要一次就获得。

db4o 对此采纳了协调的法子,限定所检索的工具数量,应用称为激活深度(activation depth)的措施,它指明在工具图中进行检索的最低层。换句话说,激活深度表示从根工具中标识的引用总数,db4o 将在查询中遍历根工具并返回结果。在前面的例子中,当检索 Ben 时,默认的激活深度 5 足够用于检索 Jessica,由于它只必要仅仅一个引用遍历。任何间隔 Ben 跨越 5 个引用的工具将无法 被检索到,它们的引用将置为空。我的事情便是显式地从数据库激活那些工具,在 ObjectContainer 应用 activate() 措施。

假如要改变默认激活深度,必要以一种周详的要领,在 Configuration 类(从 db.configure() 返回)中应用 db4o 的 activationDepth() 措施改动默认值。还有一种要领,可以对每个类设置设置设备摆设摆设激活深度。在清单 5 中,应用 ObjectClass 为 Person 类型设置设置设备摆设摆设默认激活深度:

清单 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

maleGalbraiths =

db.query(new Predicate

() {

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 commit

db.set(ben);

db.commit();

// Find Jess, make sure she's 30

Person 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

maleGalbraiths =

db.query(new Predicate

() {

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 commit

db.set(ben);

db.commit();

// Close the ObjectContainer, then re-open it

db.close();

db = Db4o.openFile("persons.data");

// Find Jess, make sure she's 30

Person 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 again

db.close();

Db4o.configure().objectClass(Person.class).cascadeOnUpdate(true);

db = Db4o.openFile("persons.data");

List

maleGalbraiths =

db.query(new Predicate

() {

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 commit

db.set(ben);

db.commit();

// Close the ObjectContainer, then re-open it

db.close();

db = Db4o.openFile("persons.data");

// Find Jess, make sure she's 30

Person 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 again

db.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

results =

db.get(new Person("Jessica", "Galbraith", null, 0, null));

assertFalse(results.hasNext());

}

履行该操作时要小心,由于它意味着其他引用了被打消层叠的工具的工具将拥有一个对 null 的引用 —— db4o 工具数据库在防止删除被引用工具上应用的引用同等性 在这里没有什么感化。(引用同等性是 db4o 普遍必要的特点,听说开拓团队正在斟酌在未来某个版本中加入这一特点。对付应用 db4o 的开拓职员来说,关键在于要以一种不违反起码意外原则 的要领实现,以致某些时刻,纵然是在关系数据库中,突破同等性规则实际上也是一种抱负的实践。)

停止语

本文是该系列文章的分水岭:在此之前,我应用的所有示例都基于异常简单的工具,从利用角度来讲,那些例子都不现实,其主要感化只是为了使您理解 OODBMS,而不是被存储的工具。理解像 db4o 这样的 OODBMS 是若何经由过程引用存储相关工具,是对照繁杂的工作。幸运的是,一旦您掌握了这些行径(经由过程解释和理解),您所要做的就只是开始调剂代码来实现这些行径。

在本文中,您看到了一些基础例子,经由过程调剂繁杂代码来实现 db4o 工具模型。进修了若何对布局化工具履行一些简单 CRUD 操作,同时,也看到了一些弗成避免的问题和办理措施。

着实,今朝的布局化工具例子仍旧对照简单,工具之间还只是直接引用关系。许多伉俪都知道,娶亲一段光阴后,孩子将会呈现。本系列的下一文章中,我将继承探索 db4o 中的布局化工具的创建与操作,看看在引入多少子工具后, ben 和 jess 工具将发生什么。

您可能还会对下面的文章感兴趣: