- 译者序
- 前言
- 本书怎么使用
- 本书排版字体约定
- 本书网站
- 致谢
- 第一部分 Hibernate 快速入门
- 第 1 章 安装和设置
- 第 2 章 映射简介
- 第 3 章 驾驭 Hibernate
- 第 4 章 集合与关联
- 第 5 章 更复杂的关联
- 第 6 章 自定义值类型
- 第 7 章 映射标注
- 第 8 章 条件查询
- 第 9 章 浅谈 HQL
- 第二部分 与其他工具的集成
- 第 10 章 将 Hibernate 连接到 MySQL
- 第 11 章 Hibernate 与 Eclipse:Hibernate Tools 使用实战
- 第 12 章 Maven 进阶
- 第 13 章 Spring 入门:Hibernate 与 Spring
- 第 14 章 画龙点睛:用 Stripes 集成 Spring 和 Hibernate
- 附录 A Hibernate 类型
- 附录 B Criteria API
- 附录 C Hibernate SQL 方言
- 附录 D Spring 事务支持
- 附录 E 参考资源
- 作者简介
- 封面介绍
扩充集合中的关联
好了,如果我们想把专辑中的曲目按一定的顺序排列,现在已经有了所需要的解决方法。那么,如果我们想保存其他信息,应该怎么办?例如曲目是在哪一张唱片中找到的?当我们映射一个关联的集合时,我们已经知道 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 内容经过扩充后的关联集合
其他
删除集合元素、重新排列以及其他对这些相互关联的信息的处理?这些处理都是自动支持的,下一节将会演示这些内容。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论