返回介绍

集合的映射

发布于 2025-04-21 21:42:12 字数 12765 浏览 0 评论 0 收藏 0

在任何真实的应用程序中,总要管理对象的列表或分组。Java 提供了一套健壮而功能丰富的类来帮助实现这些应用:集合(Collection)工具。为了将数据库关系映射到集合中,Hibernate 也提供了一些很自然的实现方法。而且它们的使用通常也非常方便。不过,你得注意 Java 集合和 Hibernate 集合在语义上的一些差别,当然,这些差别很小。事实上最大的差别是 Java 集合没有提供"bag"(包)的定义,这可能多少会让一些经验丰富的数据库设计师感到失望。这一缺陷并不是 Hibernate 的错,Hibernate 甚至也做一些努力,以作为解决这一问题的变通方案(work around)。

注意:Bag 很像 Set,只不过相同的值可以多次出现。

这些抽象概念就点到为止!Hibernate 参考手册对整个 bag 问题已经做了详尽的讨论,所以我们在这里不做介绍,直接看一个集合映射的示例(就此示例而言,关系数据库和 Java 模型配合得相当好)。在第 2 章 Track 示例的基础上继续构建新的功能,将曲目分组成专辑(album)看起来似乎很自然,但对于学习来说,这并不是最简单的起点。因为组成专辑需要牵涉到记录其他额外的信息,例如曲目是从哪张唱片来的(对于那些包含数张唱片的专辑而言),以及其他类似的细节信息。所以,我们先把艺人(artist)的信息加进数据库吧。

注意:和以前一样,这些示例假设你已经按照前几章的步骤做好了基础工作。如果还没有,可以下载示例的源代码作为起点。

要记录的艺人信息相当简单,至少刚开始是这样的。先从艺人的姓名开始。可以为每个曲目分配一组艺人,这样,我们就知道应该找谁表示感谢或不满,还可以找出你喜欢的某位艺人的所有曲目。(允许为一个曲目分配多位艺人是相当重要的,但很少有音乐管理程序做到了这一点。新增加一个链接以记录曲目的作者的工作就留给读者,以作为读者理解这个示例之后的练习吧。)

应该怎么做

就目前而言,我们的 Artist 类只需要一个 name 属性就足够了(当然,还有它的主键属性(id))。为它建立映射文档也相当容易。在 Track 映射文档所在的目录中创建一个名为 Artist.hbm.xml 的文件,其内容如例 4-1 所示。

例 4-1:Artist 类的映射文档

<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN"

"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

<class name="com.oreilly.hh.data.Artist"table="ARTIST">

<meta attribute="class-description">

Represents an artist who is associated with a track or album.

@author Jim Elliott(with help from Hibernate)

</meta>

<id name="id"type="int"column="ARTIST_ID">

<meta attribute="scope-set">protected</meta>

<generator class="native"/>

</id>

<property name="name"type="string">❶

<meta attribute="use-in-tostring">true</meta>

<column name="NAME"not-null="true"unique="true"index="ARTIST_NAME"/>

</property>

<set name="tracks"table="TRACK_ARTISTS"inverse="true">❷

<meta attribute="field-description">Tracks by this artist</meta>❸

<key column="ARTIST_ID"/>

<many-to-many class="com.oreilly.hh.data.Track"column="TRACK_ID"/>

</set>

</class>

</hibernate-mapping>

❶我们对 name 属性的映射引入了一对限制标签,分别用于配置代码生成和模式生成过程。use-in-tostring 这个 meta 标签会让生成的 Java 类包含一个定制的 toString()方法,用于在输出时显示艺人的名字,以及神秘的散列码(hash code),以辅助调试(生成结果如例 4-4 底部所示)。扩展 column 的各属性,使其成为一个更加完整的标签,让我们对列属性进行更细粒度的控制。在这个例子中,我们用这个标签增加了一个索引,可以提高按艺人名字查询和排序的效率。

❷注意,在这个文件中我们可以很自然地表达一个艺人与一个或多个曲目关联的事实。这段映射告诉 Hibernate,为 Artist 类增加一个名为 tracks 的属性,它的类型是 java.util.Set 的一个实现。这会使用一个新的名为 TRACK_ARTISTS 的表来为该 Artist 链接由它负责的 Track 对象。属性 inverse=true 稍后在例 4-3 的讨论中再详细解释这种关联的双向性的本质。我们刚才提到的 TRACK_ARTISTS 表将包含两列:TRACK_ID 和 ARTIST_ID。在这个表中出现的每一行都意味着指定的 Artist 对象和指定的 Track 对象有一定的关系。将这种关联信息单独保存在它自己的表中,这样就不会限制多少个曲目可以链接到一个特定的艺人,也不会限制多少个艺人可以关联到一个曲目。这就是所谓的“多对多”关联。

另一方面,由于这些关联信息保存在一个单独的表中,要获取关于艺人或曲目的任何有意义的信息,就必须执行一个连接查询。这也就是为什么称这样的表为“连接表”(join table)的原因。它们的所有目的就是为了连接其他的表。

最后要注意的是,不像我们用数据库模式定义建立的其他表,TRACK_ARTISTS 表没有任何相应的 Java 对象。它只是用于实现 Artist 和 Track 对象之间的链接,体现为 Artist 的 tracks 属性。

❸除了普通的值类型字段外,field-description meta 标签也可以为集合和关联提供 JavaDoc 描述。当字段名称不能完全自动文档化(self-documenting)时,这种方法就很方便了。

如果对连接表和多对多关联之类的概念还不熟悉,那么花些时间看看数据建模的很好的介绍是值得的。对这些概念的了解也有助于数据驱动的项目的设计、理解以及交流。George Reese 的《Java Database Best Practices》(O'Reilly)就有这样的介绍,而且还可以在线阅读这本书的部分章节,网址是 http://www.oreilly.com/catalog/javadtabp/chapter/ch02.pdf。

由映射文档提供的调整和配置选项(尤其是使用 meta 标签时),为如何构建源代码和数据库模式带来了很大的灵活性。你亲自编写这些配置文件所获得的控制能力,没什么可以与之相比,但是,对于大多数需要和应用场合来说,使用映射驱动(mapping-driven)的生成工具就已经足够了。这些生成工具的一个最大优点就是,它们可以免去你输入繁琐的文件内容!创建好了 Artist.hbm.xml 文件以后,我们需要将它添加到 hibernate.cfg.xml 的映射资源部分。在 src 目录中打开 hibernate.cfg.xml 文件,添加例 4-2 中用粗体字显示的那一行。

例 4-2:将 Artist.hbm.xml 添加到 Hibernate 配置文件中

<?xml version='1.0'encoding='utf-8'?>

<!DOCTYPE hibernate-configuration PUBLIC

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"

"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

<session-factory>

……

<mapping resource="com/oreilly/hh/data/Track.hbm.xml"/>

<mapping resource="com/oreilly/hh/data/Artist.hbm.xml"/>

</session-factory>

</hibernate-configuration>

准备好后,我们也为 Track 类增加一个 Artists 集合。编辑 Track.hbm.xml 文件,添加一个新的 artists 属性,如例 4-3 所示(新添加的内容以粗体显示)。

例 4-3:在 Track 映射文件中增加一个 artist 集合

……

<property name="playTime"type="time">

<meta attribute="field-description">Playing time</meta>

</property>

<set name="artists"table="TRACK_ARTISTS">

<key column="TRACK_ID"/>

<many-to-many class="com.oreilly.hh.data.Artist"column="ARTIST_ID"/>

</set>

<property name="added"type="date">

<meta attribute="field-description">When the track was created</meta>

</property>

……

这样会新增加一个名为 artists 的 Set 类型的属性。它同样使用例 4-1 提到的 TRACK_ARTISTS 连接数据表来链接到我们所映射的 Artist 对象。这种双向关联(bidirectional association)非常有用。其中,需要将关联的一端标记成“反向”(inverse),让 Hibernate 明确知道究竟在做什么,这一点很重要。在这种多对多关联的情况下,虽然 inverse 属性可以影响 Hibernate 在什么时候自动更新连接表,不过,选择哪一端作为反转映射并不重要。如果只是从试图理解数据库的人的角度来考虑,既然将连接数据表命名为"TRACK_ARTISTS",也就表明从艺人(ARTISTS 表)链接回曲目(TRACK 表)是反向端的最佳选择。

Hibernate 本身并不在意我们选择哪一端,只要我们将其中一端标识为 inverse 就可以。我们在例 4-1 中就是这样配置的。在这样的配置下,如果我们对 Track 对象内的 artists 集合做出了修改,Hibernate 将知道它需要更新 TRACK_ARTISTS 表。而如果我们对 Artist 对象中的 tracks 集合做出了修改,Hibernate 不会自动更新。

更新 Track 映射文档时,我们也可以像补充 Artist 的 name 属性那样,来充实 title 属性的配置:

……

<property name="title"type="string">

<meta attribute="use-in-tostring">true</meta>

<column name="TITLE"not-null="true"index="TRACK_TITLE"/>

</property>

……

有了这个更新过的映射文件,我们可以再次重新执行 ant codegen 来更新 Track 的源代码,并创建新的 Artist 类的源代码。如果你查看 Track.java 文件,会看到已经新增加了一个类型为 Set 的 artists 属性,还新增加了一个 toString()方法。例 4-4 是新的 Arist.java 的内容。

例 4-4:为 Artist 类生成的代码

package com.oreilly.hh.data;

//Generated Sep 3,2007 10:12:45 PM by Hibernate Tools 3.2.0.b9

import java.util.HashSet;

import java.util.Set;

/**

*Represents an artist who is associated with a track or album.

*@author Jim Elliott(with help from Hibernate)

*/

public class Artist implements java.io.Serializable{

private int id;

private String name;

/**

*Tracks by this artist

*/

private Set tracks=new HashSet(0);

public Artist(){

}

public Artist(String name){

this.name=name;

}

public Artist(String name, Set tracks){

this.name=name;

this.tracks=tracks;

}

public int getId(){

return this.id;

}

protected void setId(int id){

this.id=id;

}

public String getName(){

return this.name;

}

public void setName(String name){

this.name=name;

}

/**

**Tracks by this artist

*/

public Set getTracks(){

return this.tracks;

}

public void setTracks(Set tracks){

this.tracks=tracks;

}

/**

*toString

*@return String

*/

public String toString(){

StringBuffer buffer=new StringBuffer();

buffer.append(getClass().getName()).append("@").append(

Integer.toHexString(hashCode())).append("[");

buffer.append("name").append("='").append(getName()).append("'");

buffer.append("]");

return buffer.toString();

}

}

注意:有人在给 Hibernate 挑毛病吗?修改一下代码生成工具,在 toString()方法中使用 StringBuilder,而不是 StringBuffer,怎么样?

为什么不能正常运行

如果你看到 Hibernate 报告以下几行信息,先不要失望:

[hibernatetool]An exception occurred while running exporter

#2:hbm2java(Generates a set of.java files)

[hibernatetool]To get the full stack trace run ant with-verbose

[hibernatetool]org.hibernate.MappingNotFoundException:resource:

com/oreilly/hh/data/Track.hbm.xml not found

[hibernatetool]A resource located at com/oreilly/hh/data/Track.hbm.xml was not

found.

[hibernatetool]Check the following:

[hibernatetool]

[hibernatetool]1)Is the spelling/casing correct?

[hibernatetool]2)Is com/oreilly/hh/data/Track.hbm.xml available via the c

lasspath?

[hibernatetool]3)Does it actually exist?

BUILD FAILED

这只是因为你从新下载的示例目录中运行程序,Ant 还没有准备好类路径(classpath)声明,我们以前在第 2 章的 2.3 节讨论过这个问题。如果你再试着运行一次(或者在第一次运行之前,先手工运行一次 ant prepare 命令),就没有问题了。

其他

考虑过现在(Java 5)提倡的类型安全(type-safety)的问题吗?目前生成的这些类使用的都是 Collections 类的非泛型化(nongeneric)版本,用当前新版本的 Java 编译器来编译这些代码时,编译器会报告类似以下的信息:

[javac]Note:/Users/jim/svn/oreilly/hib_dev_2e/current/scratch/ch04

/src/com/oreilly/hh/CreateTest.java uses unchecked or unsafe operations.

[javac]Note:Recompile with-Xlint:unchecked for details.

如果有办法可以生成使用 Java 泛型(Generics)的代码,那结果一定非常棒,这样就可以加强对放入 tracks 集合(Set)的东西的控制,同时也避免了这些编译警告;也不用像原来使用 Collections 时,需要进行繁琐的类型转换。还好,非常幸运,只要修改一下我们调用 hbm2java 的方式,问题就可以解决了。编辑 build.xml 文件,将 hmb2java 一行修改为以下样子:

<hbm2java jdk5="true"/>

这是在告诉生成工具,我们正在使用 Java 5(或更高版本)环境,这样就可以利用新 JDK 提供的有用的新功能了。经过这一修改后,再次运行 ant codegen,注意观察一下新生成的代码中的变化,如例 4-5 中突出显示的部分所示。

例 4-5:用 jdk5 方式生成 Artist 类的改进

……

private Set<Track>tracks=new HashSet<Track>(0);

……

public Artist(String name, Set<Track>tracks){

this.name=name;

this.tracks=tracks;

}

……

public Set<Track>getTracks(){

return this.tracks;

}

public void setTracks(Set<Track>tracks){

this.tracks=tracks;

}

……

这就是我希望看到的代码,使用 Java 5 的 Collections 新增的泛型功能,漂亮地实现了类型安全的集合。对 Track.java 中的 artists 属性也进行类似的处理。这里先以新的 Track 类“完整”(fuu)构造函数作为例子,稍后将在例 4-9 中看到如何调用该构造函数:

public Track(String title, String filePath, Date playTime,

Set<Artist>artists, Date added, short volume){

……

}

注意:噢,这样就好多了。这么个不起眼的配置参数,竟带来这么大的好处。

在这个新的构造函数中,为 playTime 和 Data added 属性之间的 artists 属性创建了一个参数化的 Set 类型的参数。

现在已经创建(或更新)好了类,我们就可以使用 ant schema 来建立支持它们的新数据库模式了。

当然,在生成源代码和建立数据库模式时应该注意有没有出现错误消息,以免映射文档中有任何语法或概念性错误。不过,并非所有出现的异常都是你必须解决的实际问题。我在试验如何改进这个数据库模式时,遇到了几个异常,报告 Hibernate 试图删除以前运行时并没有建立过的外键(foreign key)约束。模式生成工具并不予以理会,而继续进行下去。这看起来很可怕,但却可以正常运行。这一点在未来版本中可能会有所改进(Hibernate 或 HSQLDB,或者也许只是修改 SQL 方言的实现),不然,也要学着忍受这些小缺点,它们已经存在好几年了。

生成的数据库模式中包含我们期待的数据库表,其中有一些索引和外键约束的设置。随着我们的对象模型越来越复杂,Hibernate 所做的工作量(以及专业性难度)也会逐渐增加。模式生成工具的完整输出相当冗长,所以例 4-6 仅列出一些重点内容。

例 4-6:摘录自新生成的数据库模式的部分内容

[hibernatetool]drop table ARTIST if exists;

[hibernatetool]drop table TRACK if exists;

[hibernatetool]drop table TRACK_ARTISTS if exists;

[hibernatetool]create table ARTIST(ARTIST_ID integer generated by default

as identity(start with 1),NAME varchar(255)not null,

primary key(ARTIST_ID),unique(NAME));

[hibernatetool]create table TRACK(TRACK_ID integer generated by default as

identity(start with 1),TITLE varchar(255)not null,

filePath varchar(255)not null, playTime time, added date,

volume smallint not null, primary key(TRACK_ID));

[hibernatetool]create table TRACK_ARTISTS(ARTIST_ID integer not null,

TRACK_ID integer not null, primary key(TRACK_ID, ARTIST_ID));

[hibernatetool]create index ARTIST_NAME on ARTIST(NAME);

[hibernatetool]create index TRACK_TITLE on TRACK(TITLE);

[hibernatetool]alter table TRACK_ARTISTS add constraint

FK72EFDAD8620962DF foreign key(ARTIST_ID)references ARTIST;

[hibernatetool]alter table TRACK_ARTISTS add constraint

FK72EFDAD82DCBFAB5 foreign key(TRACK_ID)references TRACK;

注意:酷!我甚至还不知道怎么在 HSQLDB 中做这些事!

图 4-1 是新增加了这些内容后,数据库模式在 HSQLDB 中的树形视图。我不确信为什么为艺人的姓名字段(NAME 字段)要用两个单独的索引来建立惟一性约束限制,但这似乎是 HSQLDB 本身特殊的实现方法,不过这种方式运行得很好。

图 4-1 更新数据库模式后的 HSQLDB 树状图形界面

发生了什么事

我们已经建立了一个对象模型,让 Track 和 Artist 对象可以记录彼此之间的任何一组关系。任何曲目都可以关联到任意数目的艺人,而任何艺人也可以关联任意数目的曲目。要将这种模型正确地建立起来可能确实不容易,尤其是对于面向对象编程或关系数据库的新手(或者二者都是新手!),所以,有 Hibernate 的帮助真的很好。但是等一下,等你看到用这样建立起来的模型来处理数据会有多么方便时,你更会确认这一点。

值得强调的是,艺人和曲目之间的连接关系并没有保存在 ARTIST 表或 TRACK 表自身内。因为二者之间是多对多关联,这意味着一位艺人可以关联到多个曲目,而一个曲目也可以关联到多位艺人,这些连接关系都保存在另一个名为 TRACK_ARTISTS 的单独的连接表中。这张数据库表中的每一条记录都有一对 ARTIST_ID 和 TRACK_ID,用于代表特定艺人关联到特定曲目。通过创建和删除这张表中的记录行,我们就可以建立任何需要的关联模式。(这就是为什么多对多关系总是要用关系数据库来表达的原因所在。前面提到的《Java Database Best Practices》一书中,George Reese 对这种数据模型有很好的介绍。)

记住这一点以后,你也会注意到生成的类不包含任何用于管理 TRACK_ARTISTS 表的代码,而后面用于创建和链接持久化 Track 和 Artist 对象的示例也没有。不需要这样的操作,是因为 Hibernate 中特殊的 Collection 类会利用例 4-1 和例 4-3 中增加的映射信息来为我们处理好这些细节。好了,来创建一些曲目和艺人对象吧。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。