许多的Java程序都需要处理持久性数据,在大多数的情况下,需要和关系数据库打交道,可能是遗留数据库或是一个工业标准的数据库管理系统(DBMS)。JDBC 的API和驱动为大多数的数据库系统提供了一个使用SQL语言进行查询的标准方式,然而,在对象模型应用程序和关系模型的数据库的领域之间,“错误匹配问题”使得接口程序十分复杂。对象模型是基于软件工程的原理和业务领域的对象模型,而关系模型是基于数学原理和数据库有效存取规则。两种模型没有谁比谁更好,问题是两者是不同的,并不总能在程序中有效协调工作。
对于这个问题已经有一些解决方案,例如Hibernate和Java Data Objects,他们为开发者提供了透明的持久性――应用程序只需使用面向对象的API处理持久性的对象,而不需要在Java代码中嵌入SQL语句。对于EJB的容器来说,容器管理的持久性(CMP)做了类似的工作,但是对Java平台来说这不是一个一般的持久性工具。在任何这些解决方案中,对象通过底层框架被映射关系数据库中的表,这些底层的框架生成SQL所需要存取的对象的属性。也就是说,对象模型越复杂,这种映射就越困难。我们所用的描述符,通常使用XML文件,来定义这种映射关系。继承和多对多的关系,尤其是一些关系模型无法直接表示的关系增加了映射复杂度。继承的结构可以有不同的方式被映射到一组表中,选择何种方式就需要在储存的效率和查询的复杂度上权衡,就如一个单独的表要实现一个多对多的关系一样。
在一个数据库中存储一个对象,数据库本身如果就是一个对象模型,这样,就提供了另外一个解决方案。在二十世纪九十年代,各种各样面向对象的数据库管理系统被开发出来,但是这些数据库配置十分复杂,而且需要使用对象定义语言。对象是作为对象格式存储的,但是对应用语言来说它们不是本地化的。目前这些数据库产品并没对市场有大的影响,只是和一些将关系和对象混杂的数据库一样,在关系数据库面向对象的API上下功夫。
嵌入式数据库
在一些应用中,数据库管理系统的维护费用是必不可少的,在一些小功耗,可嵌入的数据库引擎条件下,可以更好的提供数据库的存储需求,例如,SQLite,提供了一个自我包含的数据库引擎。但是,因为这个JAVA的接口是通过一个JDBC驱动,这种以SQL为基础的解决方案也存在“错误匹配问题”。
在许多案例中,持久性问题可以变得相当的简单,如果使用嵌入式的对象数据库引擎话。这是一个很好的机会,我们来了解一下Carl Rosenberg创建的db4o。db4o曾经是一个商业的对象数据库,现在它是开源的,而且,最近获得了GPL(GNU通用公共许可证)的许可证。
Db4o的功能特点:
●
没有错误匹配问题――对象以其本身方式来存储
●
自动管理数据模式
●
存储时没有改变类特征,以使得易于存储
●
与Java(.NET)无缝绑定
●
自动数据绑定
●
一个250Kb的库文件的简易安装(Java jar or .NET DLL)
●
一个数据库文件
●
自动模式版本
●
查询实例
●
S.O.D.A. (简单对象数据库访问), 一个开源查询的API
Db4o的优点
Db4o已经被一些嵌入式系统的应用程序所选用,这些程序特点都是要求零管理、高可靠性和低功耗的。例如,在德国,BMW Car IT在汽车嵌入式系统电子原型中使用了它。同样是德国的Die Mobilanten,他们在中型公共事业的PDA解决方案中使用了db4o。在美国,Massie Systems为婴儿眼睛诊断的视网膜图像处理系统依靠db4o加强其客户交流图像数据库。
在db4o中存储对象的方式相当的简单,同时,对于教学目的来说也相当具有吸引力。艾塞克斯(英)大学和德州农工大学都把db4o作为教学和研究对象。在我自己的学院,对于那些需要如何要将面向对象的概念应用到他们的项目中去的学生,和关系数据库打交道对于他们设计自己领域内的模型来说是一个负面的影响。使用db4o可以使得他们只需和持久层的数据打交道而不需要分心处理一个相冲突的不理解的数据关系模型,也不需要花费更多的时间学习一个工具,如Hibernate或一个复杂的OODBMS。同时,学习面向对象查询API的概念可能在将来被证明更有效。
同样的API,不同的存储
有时候,你正好不得不使用一个关系数据库。从一个Java开发者的观点来看,透明的持久性是理想境界。如果持久性可以通过面向对象的API实现,那么开发者就不必要为不同的数据存储来学习不同的技术。尽管db4o不是JDO兼容的(作为一个结果很容易使用),它的创造者的伙伴们有许多其他的开源项目,包括Mysql和Hibernate,它们使用单一的、一致的对象持久性的API和对象数据库交互,包括db4o本身,一些关系数据库或者其他存储模式,例如Prevayler。如果JDO对于你来说比较重要,你可以考虑ObjectDB,它是一个JDO兼容的纯对象数据库.
一个例子
这个例子演示了创建一个数据库和存储对象是如何简单。它同时演示了两种查询的方法:实例查询和更具有弹性的S.O.D.A查询API。整个的源代码在文章主题资源的部分可以下载[实际上对资源这部分不是合适的下载地方]。为了可以运行,你必须在你的路径里添加db4o jar文件并且执行Db4oTest.java。
在实例中有两个类,分别代表棒球队(Team)和成员(Player)。为了使得例子更加有趣,我们还有一个投手的类(Pitcher),Pitcher是Player的一个子类。并且添加了一个额外的数据域在Pitcher中。Team类有一个属性,就是该team的成员(player)列表,它当然包括Pitcher对象。Team、Player、Pitcher对象是一些简单传统的Java对象,这里并没有持久层的代码。也不需要有唯一的关键字属性,当一个对象数据库在自动存储对象时是需要唯一对象标识符(OIDS)。
Player class
public class Player {
protected String name;
protected int squadNumber;
protected float battingAverage;
protected Team team;
public Player(String name, int squadNumber,
float battingAverage){
this.name = name;
this.squadNumber = squadNumber;
this.battingAverage = battingAverage;
}
public void setName(String n){this.name = n;}
public String getName(){return this.name;}
public void setSquadNumber(int s){this.squadNumber = s;}
public int getSquadNumber(){return this.squadNumber;}
public void setBattingAverage(final float b) {
this.battingAverage = b; }
public float getBattingAverage(){
return this.battingAverage;}
public void setTeam(Team t) {this.team = t;}
public Team getTeam() {return this.team;}
public String toString() {
return name + ":" + battingAverage;
}
}
Pitcher class
public class Pitcher extends Player{
private int wins;
public Pitcher(String name, int squadNumber,
float battingAverage, int wins) {
super(name,squadNumber,battingAverage);
this.wins = wins;
}
public void setWins(final int w){this.wins = w;}
public int getWins() {return this.wins;}
public String toString() {
return name + ":" + battingAverage + ", " + wins;
}
}
Team class
import java.util.List;
import java.util.ArrayList;
public class Team {
private String name;
private String city;
private int won;
private int lost;
private List players;
public Team(String name, String city,
int won, int lost){
this.name = name;
this.city = city;
this.won = won;
this.lost = lost;
this.players = new ArrayList();
}
public void addPlayer(Player p) {
players.add(p);
}
public void setName(String n){this.name = n;}
public String getName(){return this.name;}
public void setStadium(String c){this.city = c;}
public String getCity(){return this.city;}
public void setPlayers(List p){players = p;}
public List getPlayers(){return players;}
public void setWon(int w) {this.won = w;}
public int getWon(){return this.won;}
public void setLost(int l) {this.lost = l;}
public int getLost() {return this.lost;}
public String toString() {
return name;
}
}
首先,我们来建立一些测试数据。
// Create Players
Player p1 = new Player("Barry Bonds", 25, 0.362f);
Player p2 = new Player("Marquis Grissom", 9, 0.279f);
Player p3 = new Player("Ray Durham", 5, 0.282f);
Player p4 = new Player("Adrian Beltre", 29, 0.334f);
Player p5 = new Player("Cesar Izturis", 3, 0.288f);
Player p6 = new Player("Shawn Green", 15, 0.266f);
// Create Pitchers
Player p7 = new Pitcher("Kirk Rueter",46, 0.131f, 9);
Player p8 = new Pitcher("Kazuhisa Ishii",17, 0.127f, 13);
// Create Teams
Team t1 = new Team("Giants", "San Francisco", 91, 71);
Team t2 = new Team("Dodgers", "Los Angeles", 93, 69);
// Add Players to Teams
t1.addPlayer(p1); p1.setTeam(t1);
t1.addPlayer(p2); p2.setTeam(t1);
t1.addPlayer(p3); p3.setTeam(t1);
t2.addPlayer(p4); p4.setTeam(t2);
t2.addPlayer(p5); p5.setTeam(t2);
t2.addPlayer(p6); p6.setTeam(t2);
// Add Pitchers to Teams
t1.addPlayer(p7); p7.setTeam(t1);
t2.addPlayer(p8); p8.setTeam(t2);
存储数据
一个Team的对象可以用一行代码执行存储操作:
db.set(t1);
这儿,db是一个指向ObjectContainer对象的一个引用,它在打开一个新数据文件时被创建,如下:
ObjectContainer db = Db4o.openFile(filename);
一个db4o数据库是一个以.yap为扩展名的文件,其中set方法用来存储一个对象。
注意这里是存储了一个Team的对象,并且它包含了Player的对象。我们能够通过取回Player对象来测试是否存储了这些Player的对象。最简单的方法就是用QBE来查询。
实例查询的简单查询
下面的代码列出了所有与实例对象匹配的Player对象,这里它们是唯一的。通过调用ObjectContainer的get方法,结果以ObjectSet的方式返还。
Player examplePlayer = new Player("Barry Bonds",0,0f);
ObjectSet result=db.get(examplePlayer);
System.out.println(result.size());
while(result.hasNext()) {
System.out.println(result.next());
}
同时,我们也可以得到所有我们预先虚构的,创建并存储的Player对象(所有数据域为0或者是空),如下:
Player examplePlayer = new Player(null,0,0f);
ObjectSet result=db.get(examplePlayer);
System.out.println(result.size());
while(result.hasNext()) {
System.out.println(result.next());
}
结果如下:
8
Kazuhisa Ishii:0.127, 13
Shawn Green:0.266
Cesar Izturis:0.288
Adrian Beltre:0.334
Kirk Rueter:0.131, 9
Ray Durham:0.282
Marquis Grissom:0.279
Barry Bonds:0.362
注意:我们可以取回所有的Player类的对象,包括其子类(这里是Pitcher类)的对象,而不需要任何额外的代价。上面结果中所示的Pitcher对象它们额外的信息(wins)也同样被取回来了。而在关系数据库中我们必须知道如何在表中映射继承树,可能不得不要加一些额外的表来取回对象所有的属性。
更新和删除
更新一个对象可以混合使用上面的方法技术。下面的代码假定了只有一个结果匹配,并且这个匹配的对象可以上溯到Player,这样可以保证它的属性能得到修改。
Player examplePlayer = new Player("Shawn Green",0,0f);
ObjectSet result = db.get(examplePlayer);
Player p = (Player) result.next();
p.setBattingAverage(0.299f);
db.set(p);
数据库的对象也可以以同样的方式被删除。
Player examplePlayer = new Player("Ray Durham",0,0f);
ObjectSet result = db.get(examplePlayer);
Player p = (Player) result.next();
db.delete(p);
功能更强的查询
在早期版本的db4o中一个主要的缺点就是实例查询提供相当有限的查询能力。例如,你不能像这样查询“所有平均击球率大于30%的队员”。现在,db4o包含了S.O.D.A的API可以使得查询的能力接近SQL。一个Query类的实例代表了含有约束条件的标准查询图中的一个节点,这个节点可以代表一个类,或者多个类,或者一个类属性。
下面的代码演示了如何执行这样的操作。我们定义了一个查询图的节点并且指定/限制它为Player类。这意味着查询的返回结果为Player对象。然后,我们向下具体化这张图,去查找一个节点,代表属性“平均击球率”,限制其大于0.3。最后,执行查询操作,返回数据库中所有满足条件的对象。
Query q = db.query();
q.constrain(Player.class);
q.descend("battingAverage").constrain(new Float(0.3f)).greater();
ObjectSet result = q.execute();
初一看,这个查询操作很类似SQL的查询,就如这样:
SELECT * FROM players WHERE battingAverage 0.3
然而,Player类的设计允许在Team对象和Player之间有相互引用关系,正如测试中的数据。一个Team的对象有一个应用指向一个序列的Player对象,同时每一个Player对象都有一个Team的引用。这就意味这查询的结果应该包含Player和Team的对象。演示代码如下:
System.out.println(result.size());
while(result.hasNext()) {
// Print Player
Player p = (Player) result.next();
System.out.println(p);
// Getting Player also gets Team - print Team
Team t = p.getTeam();
System.out.println(t);
}
输出:
2
Adrian Beltre:0.334
Dodgers
Barry Bonds:0.362
Giants
现在的查询十分类似SQL语言查询,如下:
SELECT teams.name, players.name, players.battingAverage FROM teams,
players
WHERE teams.teamID = players.playerID
AND battingAverage 0.3
这个能正常工作是因为相互引用关系已经被设计到对象模型中。对象数据库是具有可以导航性的:你只要根据预定义的关系就可以取回数据。从另一方面讲,关系数据库并不能直接在表中连接。所以,这样对于特定查询更具有弹性的。然而,在给定对象关系中,从对象数据库中取回对象的关系几乎不要任何的程序代价。数据库模型和应用程序对象模型是一致的,所以不需要程序员考虑数据的差异性。如果对象在内存里的时候,你可以从一个给定的Player中得到它所属的Team,你就可以在对象数据库中做同样事。
S.O.D.A.其他功能
SQL查询允许结果按要求排序,S.O.D.A.也可以达到同样功效。下面是一个例子演示了取回我们开始存储的Player对象,并以“平均击球率”排序。(很显然,这样可以知道哪些是投手)
Query q = db.query();
q.constrain(Player.class);
q.descend("battingAverage").orderAscending();
ObjectSet result = q.execute();
结果:
7
Kazuhisa Ishii:0.127, 13
Kirk Rueter:0.131, 9
Marquis Grissom:0.279
Cesar Izturis:0.288
Shawn Green:0.299
Adrian Beltre:0.334
Barry Bonds:0.362
S.O.D.A.可以定义更加复杂的查询,一旦你除去考虑关系数据库的方式思考它,那将十分的简单。为了设置限制条件,你只需根据查询图导航,去寻找你想设置条件的类或属性。查询图概念和对象模型领域关系十分紧密。这些对开发者理解十分有益。另一方面,为了达到类似SQL的查询结果,你必须考虑如何映射对象领域和关系数据的表格。
下面的例子演示了如何在Player类的两个属性上设置条件。我们将去查找这样的队员:他的击球率大于0.130,而且他是一个赢过五次以上的投手。另外,我们定义了一个查询图节点,限制它为Player类,然后,我们具体化这张图去寻找一个节点代表属性“平均击球率”,使其大于0.13,这个结果是一个Constraint的对象。为了设置下一个限制条件,我们具体化去寻找节点代表属性“赢”,这本身就意味着是去查找一个Pticher类对象。这个节点的限制条件是大于5,然后使用逻辑与――“AND”操作前一个Constraint对象。
Query q = db.query();
q.constrain(Player.class);
Constraint constr =
q.descend("battingAverage").constrain(
new Float(0.13f)).greater();
q.descend("wins").constrain(
new Integer(5)).greater().and(constr);
result = q.execute();
结果:
1
Kirk Rueter:0.131, 9
Giants
最后一个例子演示了如何组合不同类的属性作为查询条件。我们将查询满足如下条件的队员:他的击球率大于0.300并且他的球队已经赢了92场以上。最简单的方式是从Player类开始,然后导航到Team。我们可以和前面一样具体化去查找“平均击球率”节点,然后,生成一个Constraint对象。然后,具体化去查找Team的属性的时候,这个属性是Team的,而这个节点代表的是Team类,因此,我们可以再次具体化去查找一个节点代表“赢”的Team的赢得属性,然后生成Constraint对象,最后,我们与前一个Constraint对象与操作即可。
Query q = db.query();
q.constrain(Player.class);
Constraint constr =
q.descend("battingAverage").constrain(
new Float(0.3f)).greater();
q.descend("team").descend("won").constrain(
new Integer(92)).smaller().and(constr);
result = q.execute();
结果:
1
Barry Bonds:0.362
Giants
结论
一个低功耗、可嵌入的对象数据库提供了一个十分简单、间接的方法处理对象的持久性。Db4o现在是一个开源的对象数据库。它拥有一系列很具有吸引力的特点,同时支持Java和.Net。简单的安装,同时在对象模型和数据模型间不存在“错误匹配问题”。这些使得db4o无论在商业上还是教育上都有广泛的应用。