返回介绍

扩充集合中的关联

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

好了,如果我们想把专辑中的曲目按一定的顺序排列,现在已经有了所需要的解决方法。那么,如果我们想保存其他信息,应该怎么办?例如曲目是在哪一张唱片中找到的?当我们映射一个关联的集合时,我们已经知道 Hibernate 会创建一个连接表来存储对象之间的关系。此外,我们也知道如何在 ALBUM_TRACKS 表中增加一个索引字段,以保存该集合元素的顺序。理想情况下,我们也想要能够扩充数据表的内容,以填入我们自己选择的信息,以便于保存专辑曲目的其他细节信息。

事实上,我们也可以做好这件事情,而且过程也很简单。

应该怎么做

到目前为止,我们已经看到过两种把数据表放进数据库模式的方法。第一种方法是明确地将 Java 对象的属性映射到数据表的字段;第二种方法是定义一个集合(值或关联的),并指定用于管理这个集合的数据表和字段。结果就是用这两种方法创建的数据表的使用是一样的。可以直接用该数据表的某些字段来映射对象的属性,而同时再用其他字段来管理集合的映射。这样可以让我们在实现以特定的顺序来保存专辑曲目的同时,还能够通过增加其他细节来扩展数据表,以支持包含多张唱片的曲目专辑。

注意:这种灵活性要花点时间来习惯,不过有其道理所在。尤其是如果你想将对象映射到现有的数据库模式时。

我们需要一个新的数据对象(AlbumTrack),用于保存有关曲目在专辑中的使用方式的信息。由于我们已经实践了几个示例,了解了如何映射完整而独立存在的实体,因此 AlbumTrack 对象实在没有必要单独存在于 Album 实体的范围以外。这刚好是个机会,可以看一看组件映射是怎么回事。回想一下,在 Hibernate 的术语中,实体就是在持久化机制中独立存在的对象:它可以被创建、查询以及删除,这些操作都独立于其他任何对象,因此有其自身的持久化身份(表现为它必须具有 id 属性)。相反地,组件虽然是可以存储到数据库,也能从数据库检索出来的对象,但它只是其他实体的从属部分。在这个示例中,我们要把一组 AlbumTrack 对象列表定义成 Album 实体的组件。例 5-4 就是定义这一关系的 Album 类的映射文件。

例 5-4:Album.hbm.xml(Album 类的映射定义)

<?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.Album"table="ALBUM">

<meta attribute="class-description">

Represents an album in the music database, an organized list of tracks.

@author Jim Elliott(with help from Hibernate)

</meta>

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

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

<generator class="native"/>

</id>

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

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

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

</property>

<property name="numDiscs"type="integer"/>

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

<key column="ALBUM_ID"/>

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

</set>

<set name="comments"table="ALBUM_COMMENTS">

<key column="ALBUM_ID"/>

<element column="COMMENT"type="string"/>

</set>

<list name="tracks"table="ALBUM_TRACKS">❶

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

<key column="ALBUM_ID"/>

<index column="LIST_POS"/>

<composite-element class="com.oreilly.hh.data.AlbumTrack">❷

<many-to-one class="com.oreilly.hh.data.Track"name="track">❸

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

<column name="TRACK_ID"/>

</many-to-one>

<property name="disc"type="integer"/>❹

<property name="positionOnDisc"type="integer"/>❺

</composite-element>

</list>

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

<meta attribute="field-description">

When the album was created</meta>

</property>

</class>

</hibernate-mapping>

在创建好 Album.hbm.xml 以后,还需要将它添加到 hibernate.cfg.xml 的映射资源列表中。打开 src 目录下的 hibernate.cfg.xml 文件,将例 5-5 中用粗体突出显示的那一行添加到这个文件中。

例 5-5:将 Album.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"/>

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

</session-factory>

</hibernate-configuration>

多数内容都类似于以前看到过的映射配置,但是 tracks 列表的配置值得我们仔细探讨。关于讨论的内容,先回想一下刚才我们究竟想做什么。

我们想要让专辑里的曲目列表保持一定的顺序,同时又能为每个曲目增加一些额外的信息,以指出曲目所属的唱片(专辑包括多张唱片的情况),以及该曲目在唱片中的位置。这种关系的概念如图 5-1 的中部内容所示。专辑和曲目之间的关联是由"AlbumTrack"对象作为媒介而体现的,这个对象新增加了唱片和位置信息,并保证曲目以正确的顺序排列。曲目对象本身的模型我们已经很熟悉了(此图中,为了保持简单,我们删去了艺人和评论信息),这个模型正是我们在专辑映射文档中需要使用的(例 5-4)。让我们讨论其中的细节吧。稍后,我们会介绍 Hibernate 如何将这样的映射配置规定转换成 Java 代码(图 5-1 的底部部分)和数据库模式(图 5-1 的顶部部分)。

好吧,有了这一概念性框架的提示和描述,我们接下来就看看例 5-4 的细节内容:

❶如果拿前一章的集合映射定义和这里的列表定义进行比较,你会发现它们之间存在很多相似之处。它看起来甚至更像例 5-2,只是关联映射定义已经移到一个新的 composite-element 映射元素内部了。

❷这个元素引入新的 AlbumTrack 对象,我们用它来分组唱片、位置以及组织专辑曲目所需要的 Track 链接。

❸此外,AlbumTrack 和 Track 之间的关联是多对一的(不是多对多的映射,因为专辑通常包含数个曲目,而特定曲目文件可能在几个专辑之间共享):如果我们为了节省磁盘空间,几个 AlbumTrack 对象(来自不同专辑)可能会引用相同的 Track 文件,但每一个 AlbumTrack 对象只与一个 Track 相关。包含 AlbumTrack 的 list 标签隐含是一对多的关系(如果这些数据建模概念让你很困惑,现在不用太花费精力去弄懂这些,源代码和数据库模式马上就会出来,希望有助于你明白这到底在干什么)。

好了,继续从整体上考虑一下这个新的 composite-element 元素定义。这个元素指出我们想要用一个新的 AlbumTrack 类作为 Album 数据 bean 的曲目列表中的值。composite-element 标签的主体定义了 AlbumTrack 的各个属性,把专辑中一个曲目的所有信息都汇集在这儿了。这些嵌套属性的映射语法和外层 Album 自身属性的映射语法并没有什么不同,甚至还能包含自己的嵌套复合元素、集合或 meta 元素(如此处所示)。这让我们在建立细粒度的映射上具有相当大的灵活性,同时也保留了一定良好程度的面向对象的封装。

在我们的复合 AlbumTrack 映射中,我们要记录和实际 Track 之间的关联(刚才介绍的 many-to-one 元素),以及该曲目在专辑中的播放位置。

❹复合映射也需要保存曲目所属的唱片的编号。

❺最后也要保存该曲目在唱片中的位置(例如第 2 号唱片的第 3 首曲目)。

这一映射取得了我们先前的既定目标,也就是可以将任意的信息附加到关联集合中。

组件类本身的源代码如例 5-6 所示,它有助于阐明相关讨论。可以对比一下该源代码和它的图形表示图 5-1 底部。

图 5-1 表示专辑曲目所涉及的数据表、概念、以及对象的模型

可以看到,我们选择 TRACK_ID 的字段名称作为链接到 TRACK 表的多对一(many-to-one)关联的连接字段。这样的处理在前面已经遇到过很多次了,但之前都不需要独立成一行。这里值得讨论一下这样选择的原因。没有这条指令,Hibernate 将只会用属性名(track)作为字段名称。你的字段可以使用任何名称,但《Java Database Best Practices》一书鼓励我们把外键(foreign key)字段命名成和其引用的原来数据表里的主键(primary key)的名称相同。这有助于数据建模工具识别并显示外键所代表的“自然连接”(natural join),也可以让人更容易理解和使用数据。出于这种考虑,我们就将数据表名称作为主键字段名称的一部分。

发生了什么事

我原本以为,由于选择使用 composite 元素来封装扩充过的曲目列表,就得自己编写 AlbumTrack 类的 Java 源代码,而这会超出代码生成工具能力所及的范围。但是出乎意料的是,当我试着执行 ant codegen 命令,想看看有什么错误信息会出现时,没想到这个命令居然报告成功,源代码目录下出现了 Album.java 和 AlbumTrack.java 这两个文件!

注意:偶尔证明是错的也不为过。

这时,我再回过头来为组件内曲目的多对一映射新增了一个 use-in-tostring 的 meta 元素。我不确信这是否行得通,因为我在参考文档中惟一找到的使用示例都是附加在 property 标签以内。但是居然行得通,这和我原来希望的一样。

Hibernate 最佳实践鼓励我们使用细粒度的(fine-grained)持久化类,再将它们映射为组件。代码生成工具可以轻易地根据映射文档来创建持久化类的源代码,绝对没有借口而对这一建议视而不见。例 5-6 演示了为嵌套的组件映射生成的源代码。

例 5-6:为 AlbumTrack.java 生成的代码

package com.oreilly.hh.data;

//Generated Jun 21,2007 11:11:48 AM by Hibernate Tools 3.2.0.b9

/**

*Represents an album in the music database, an organized list of tracks.

*@author Jim Elliott(with help from Hibernate)

*/

public class AlbumTrack implements java.io.Serializable{

private Track track;

private Integer disc;

private Integer positionOnDisc;

public AlbumTrack(){

}

public AlbumTrack(Track track, Integer disc, Integer positionOnDisc){

this.track=track;

this.disc=disc;

this.positionOnDisc=positionOnDisc;

}

public Track getTrack(){

return this.track;

}

public void setTrack(Track track){

this.track=track;

}

public Integer getDisc(){

return this.disc;

}

public void setDisc(Integer disc){

this.disc=disc;

}

public Integer getPositionOnDisc(){

return this.positionOnDisc;

}

public void setPositionOnDisc(Integer positionOnDisc){

this.positionOnDisc=positionOnDisc;

}

/**

*toString

*@return String

*/

public String toString(){

StringBuffer buffer=new StringBuffer();

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

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

buffer.append("track").append("='").append(getTrack()).append("'");

buffer.append("]");

return buffer.toString();

}

}

这段代码看起来和前几章为实体生成的代码差不多,但是此处少了一个 id 属性,这是有道理的。组件类不需要标识符字段,也不需要实现任何特殊接口。这个类和 Album 类共享同样的 JavaDoc,而在 Album 类中就使用了该组件类。Album 类的源代码是典型的代码生成工具生成的实体,所以这里不再赘述。

现在,我们可以通过 ant schema 命令为这些新的映射文档建立数据库模式了。例 5-7 演示了模式创建结果的重点内容,这就是图 5-1 顶端模式模型的具体 HSQLDB 表示。

例 5-7:由新的 Album 映射所增加的数据库模式

……

[hibernatetool]create table ALBUM(ALBUM_ID integer generated by default

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

numDiscs integer, added date, primary key(ALBUM_ID));

……

[hibernatetool]create table ALBUM_ARTISTS(ALBUM_ID integer not null,

ARTIST_ID integer not null,

primary key(ALBUM_ID, ARTIST_ID));

……

[hibernatetool]create table ALBUM_COMMENTS(ALBUM_ID integer not null,

COMMENT varchar(255));

……

[hibernatetool]create table ALBUM_TRACKS(ALBUM_ID integer not null,

TRACK_ID integer, disc integer, positionOnDisc integer, LIST_POS integer not null,

primary key(ALBUM_ID, LIST_POS));

……

[hibernatetool]create index ALBUM_TITLE on ALBUM(TITLE);……

[hibernatetool]alter table ALBUM_ARTISTS add constraint FK7BA403FC620962DF

foreign key(ARTIST_ID)references ARTIST;

[hibernatetool]alter table ALBUM_ARTISTS add constraint FK7BA403FC3C553835

foreign key(ALBUM_ID)references ALBUM;

[hibernatetool]alter table ALBUM_COMMENTS add constraint FK1E2C21E43C553835

foreign key(ALBUM_ID)references ALBUM;

[hibernatetool]alter table ALBUM_TRACKS add constraint FKD1CBBC782DCBFAB5

foreign key(TRACK_ID)references TRACK;

[hibernatetool]alter table ALBUM_TRACKS add constraint FKD1CBBC783C553835

foreign key(ALBUM_ID)references ALBUM;

……

你可能发现,对数据库模式做一些关键的修改会导致 Hibernate 或 HSQLDB 驱动程序发生问题。当我换用这种新方法来建立专辑曲目的映射时,遇到了麻烦,因为第一组映射会建立一些数据库约束,而 Hibernate 在试图重新建立模式时并不知道应该先删除这些约束。这样就不能继续删除和重新创建某些数据表了。如果你也遇到了这种问题,可以先删除数据库文件(data 目录下的 music.script 文件),再从头开始,应该就没有问题了。针对这些情况,Hibernate 最新版本的健壮性似乎有所增强。

图 5-2 展示了 HSQLDB 图形管理界面中扩展后的数据库模式。

你可能会问,究竟为什么要用这么一个单独的 Track 类,而不是直接把所有信息都放在扩充后的 AlbumTrack 集合中。答案很简单,并不是所有曲目都属于某个专辑,有些也许是单曲、下载而来的或其他单独的曲目。既然我们已经用一个单独的数据表来记录这些数据,所以再在 AlbumTracks 表中重复其内容而非建立与 Track 表的关联,将是一种很糟糕的设计。采用这种方法(我自己的音乐数据库就是用这种方法),还有另一个微妙的优点:这样的结构能让我们在多个专辑中共享同一个曲目。如果有专辑、精选集以及某个或多个年代的选集都出现了相同曲目,把这些曲目集都链接到相同的曲目就可以节省磁盘空间。

图 5-2 专辑相关数据表的模式

ALBUM_TRACK 模式中另一个值得注意的地方是它没有明显的 ID 字段。如果查看例 5-7 中 Hibernate 为 ALBUM_TRACK 生成的模式定义,可以看到主键是通过 primary key(ALBUM_ID, LIST_POS)这样的语句定义的。Hibernate 已经知道,根据我们在 Album.hbm.xml 中要求的映射关系,ALBUM_TRACK 表中的某一行可以由 Album(专辑)的 ID 和曲目在列表中的索引而惟一确定,所以 Hibernate 为这个表建立了一个复合主键(composite key)。对于这一优化,我们不需要过多讨论。还要注意,这些列中有一个类型是 AlbumTrack 类的属性,而其他列则不是。在第 7 章中,我们将以另一种稍微不同的方式来建立这一关系模型。

我们来看看示例程序代码,以了解如何使用这些新的数据对象。例 5-8 演示的类会创建一个专辑记录和一列曲目,然后通过配置好的 toString()方法来打印输出测试调试数据。

例 5-8:AlbumTest.java 的源代码

package com.oreilly.hh;

import org.hibernate.*;

import org.hibernate.cfg.Configuration;

import com.oreilly.hh.data.*;

import java.sql.Time;

import java.util.*;

/**

*Create sample album data, letting Hibernate persist it for us.*/

public class AlbumTest{

/**

*Quick and dirty helper method to handle repetitive portion of creating

*album tracks.A real implementation would have much more flexibility.

*/

private static void addAlbumTrack(Album album, String title, String file,

Time length, Artist artist, int disc,

int positionOnDisc, Session session){❶

Track track=new Track(title, file, length, new HashSet<Artist>(),

new Date(),(short)0,new HashSet<String>());

track.getArtists().add(artist);❷

session.save(track);

album.getTracks().add(new AlbumTrack(track, disc, positionOnDisc));❸}

public static void main(String args[])throws Exception{

//Create a configuration based on the properties file we've put

//in the standard place.

Configuration config=new Configuration();

config.configure();

//Get the session factory we can use for persistence

SessionFactory sessionFactory=config.buildSessionFactory();

//Ask for a session using the JDBC information we've configured

Session session=sessionFactory.openSession();

Transaction tx=null;

try{

//Create some data and persist it

tx=session.beginTransaction();

Artist artist=CreateTest.getArtist("Martin L.Gore",true,

session);

Album album=new Album("Counterfeit e.p.",1,

new HashSet<Artist>(),new HashSet<String>(),

new ArrayList<AlbumTrack>(5),new Date());

album.getArtists().add(artist);

session.save(album);

addAlbumTrack(album,"Compulsion","vol1/album83/track01.mp3",

Time.valueOf("00:05:29"),artist,1,1,session);

addAlbumTrack(album,"In a Manner of Speaking",

"vol1/album83/track02.mp3",Time.valueOf("00:04:21"),

artist,1,2,session);

addAlbumTrack(album,"Smile in the Crowd",

"vol1/album83/track03.mp3",Time.valueOf("00:05:06"),

artist,1,3,session);

addAlbumTrack(album,"Gone","vol1/album83/track04.mp3",

Time.valueOf("00:03:32"),artist,1,4,session);

addAlbumTrack(album,"Never Turn Your Back on Mother Earth",

"vol1/album83/track05.mp3",Time.valueOf("00:03:07"),

artist,1,5,session);

addAlbumTrack(album,"Motherless Child","vol1/album83/track06.mp3",

Time.valueOf("00:03:32"),artist,1,6,session);

System.out.println(album);

//We're done;make our changes permanent

tx.commit();

//This commented out section is for experimenting with deletions.

//tx=session.beginTransaction();

//album.getTracks().remove(1);

//session.update(album);

//tx.commit();

//tx=session.beginTransaction();

//session.delete(album);

//tx.commit();

}catch(Exception e){

if(tx!=null){

//Something went wrong;discard all partial changes

tx.rollback();

}

throw new Exception("Transaction failed",e);

}finally{

//No matter what, close the session

session.close();

}

//Clean up after ourselves

sessionFactory.close();

}

}

❶首先,addAlbumTrack()方法会根据指定的参数创建一个 Track 对象,并将之持久保存。

❷其次,将新的曲目和一个 Artist 对象建立关联。

❸最后,将曲目对象加到 Album 内,记录所属的唱片以及在该唱片中的位置。

在这个示例中,我们创建了一个只有一张唱片的专辑。虽然这种快速而简单的方法并不能处理其他各种应用需求,但这样可以让示例更简洁一些。

为了调用这个类,还需要在 build.xml 的末尾增加一个新的 ant 构建目标,将例 5-9 的内容添加到该文件的结尾处(当然,应该在 project 元素的内部)。

例 5-9:运行 AlbumTest 类需要的 ant 构建目标

<target name="atest"description="Creates and persists some album data"

depends="compile">

<java classname="com.oreilly.hh.AlbumTest"fork="true">

<classpath refid="project.class.path"/>

</java>

</target>

准备好以后,假定已经生成了数据库模式,接着就依次执行 ant ctest 和 ant atest(先运行 ctest 并不是必需的,但是,测试开始时有些额外的数据,会让专辑数据变得更有趣些。回想一下,你也可以只用一行命令来运行这些构建目标:ant ctest atest。如果想在开始时先把数据库的内容清除掉,则可以执行 ant schema ctest atest)。这个命令产生的调试输出信息如例 5-10 所示。虽然有些难懂,但是应该可以看出来这段代码创建好了专辑和曲目数据,而且还保留了曲目的顺序。

例 5-10:运行专辑测试的输出

atest:

[java]com.oreilly.hh.data.Album@5bcf3a[title='Counterfeit e.p.'tracks='[

com.oreilly.hh.data.AlbumTrack@6a346a[track='com.oreilly.hh.data.Track@973271[

title='Compulsion'volume='Volume[left=100,right=100]'sourceMedia='CD']'],c

om.oreilly.hh.data.AlbumTrack@8e0e1[track='com.oreilly.hh.data.Track@e3f8b9[ti

tle='In a Manner of Speaking'volume='Volume[left=100,right=100]'sourceMedia='

CD']'],com.oreilly.hh.data.AlbumTrack@de59f0[track='com.oreilly.hh.data.Trac

k@e2d159[title='Smile in the Crowd'volume='Volume[left=100,right=100]'source

Media='CD']'],com.oreilly.hh.data.AlbumTrack@1e5a36[track='com.oreilly.hh.da

ta.Track@b4bb65[title='Gone'volume='Volume[left=100,right=100]'sourceMedia='

CD']'],com.oreilly.hh.data.AlbumTrack@7b1683[track='com.oreilly.hh.data.Trac

k@3171e[title='Never Turn Your Back on Mother Earth'volume='Volume[left=100,r

ight=100]'sourceMedia='CD']'],com.oreilly.hh.data.AlbumTrack@e2e4d7[track='

com.oreilly.hh.data.Track@1dfc6e[title='Motherless Child'volume='Volume[left=1

00,right=100]'sourceMedia='CD']']]']

如果执行原来的查询测试,会同时看到新旧数据,如例 5-11 所示。

例 5-11:无论是否来自专辑,所有曲目的播放时间都小于 7 分钟

%ant qtest

Buildfile:build.xml

……

qtest:

[java]Track:"Russian Trance"(PPK)00:03:30

[java]Track:"Video Killed the Radio Star"(The Buggles)00:03:49[java]Track:"Gravity's Angel"(Laurie Anderson)00:06:06

[java]Track:"Adagio for Strings(Ferry Corsten Remix)"(Ferry Corsten,

Samuel Barber, William Orbit)00:06:35

[java]Track:"Test Tone 1"00:00:10

[java]Comment:Pink noise to test equalization

[java]Track:"Compulsion"(Martin L.Gore)00:05:29

[java]Track:"In a Manner of Speaking"(Martin L.Gore)00:04:21[java]Track:"Smile in the Crowd"(Martin L.Gore)00:05:06

[java]Track:"Gone"(Martin L.Gore)00:03:32

[java]Track:"Never Turn Your Back on Mother Earth"(Martin L.Gore)

00:03:07

[java]Track:"Motherless Child"(Martin L.Gore)00:03:32

BUILD SUCCESSFUL

Total time:2 seconds

最后,图 5-3 显示了在 HSQLDB 界面中检索 ALBUM_TRACKS 表内容的查询。

图 5-3 内容经过扩充后的关联集合

其他

删除集合元素、重新排列以及其他对这些相互关联的信息的处理?这些处理都是自动支持的,下一节将会演示这些内容。

发布评论

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