返回介绍

建立组合自定义类型

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

回想一下,我们的 Track 对象中有个属性,用于决定曲目在播放时音量的大小。假设我们希望自动唱机系统除了能控制曲目的音量以外,也能够调整播放曲目时音质的均衡度(balance)。为了实现这一点,我们需要为左右声道分别保存音量。一种快速的解决方案就是编辑 Track 映射文档,将这些功能要求映射到单独的属性。

如果我们严格地从面向对象架构的角度来考虑,可能希望将这两个音量值封装到一个 StereoVolume 类中。然后,再将这个类直接映射到一个 composite-element 元素,就像我们在例 5-4 中对 AlbumTrack 组件所做的那样。这依然相当直截了当。

不过,这种简单的解决方案有个缺点。系统中的其他地方可能也需要 StereoVolume 值。如果我们建立一种播放列表机制,它可以改写曲目默认的播放选项,同时又能对整个专辑的播放音量进行控制。突然间,我们就得在好几个地方重建组合映射配置,而且有可能无法保持前后一致(对于更复杂的复合类型来说,这更可能是个问题,但现在只需要有个想法就可以了)。Hibernate 参考文档指出,像这种情况,使用一个自定义的复合类(composite user type)会是种很好的务实做法,我也同意这一点。

应该怎么做

我们先从定义 StereoVolume 类开始做起。没有理由要将这个类作为实体(使其独立存在于其他持久化对象以外),所以只要将它作为普通的(而且相当简单的)Java 对象来实现即可。例 6-8 展示了它的代码。

注意:该例的 JavaDoc 经过精简以节省空间。相信你在实际项目中不会这么做。从网站下载的版本则更加完整。

例 6-8:一个用于表示音量等级的值类(StereoVolume.java)

package com.oreilly.hh;

import java.io.Serializable;

/**

*A simple structure encapsulating a stereo volume level.

*/

public class StereoVolume implements Serializable{

/**The minimum legal volume level.*/

public static final short MINIMUM=0;

/**The maximum legal volume level.*/

public static final short MAXIMUM=100;

/**Stores the volume of the left channel.*/

private short left;

/**Stores the volume of the right channel.*/

private short right;

/**Default constructor sets full volume in both channels.*/

public StereoVolume(){❶

this(MAXIMUM, MAXIMUM);

}

/**Constructor that establishes specific volume levels.*/

public StereoVolume(short left, short right){

setLeft(left);

setRight(right);

}

/**

*Helper method to make sure a volume value is legal.

*@param volume the level that is being set.

*@throws IllegalArgumentException if it is out of range.

*/

private void checkVolume(short volume){

if(volume<MINIMUM){

throw new IllegalArgumentException("volume cannot be less than"+

MINIMUM);

}

if(volume>MAXIMUM){

throw new IllegalArgumentException("volume cannot be more than"+

MAXIMUM);

}

}

/**Set the volume of the left channel.*/

public void setLeft(short volume){❷

checkVolume(volume);

left=volume;

}

/**Set the volume of the right channel.*/

public void setRight(short volume){

checkVolume(volume);

right=volume;

}

/**Get the volume of the left channel*/

public short getLeft(){

return left;}

/**Get the volume of the right channel.*/

public short getRight(){

return right;}

/**Format a readable version of the volume levels, for debugging.*/

public String toString(){

return"Volume[left="+left+",right="+right+']';

}

/**

*Compare whether another object is equal to this one.

*@param obj the object to be compared.

*@return true if obj is also a StereoVolume instance, and represents

*the same volume levels.

*/

public boolean equals(Object obj){❸

if(obj instanceof StereoVolume){

StereoVolume other=(StereoVolume)obj;

return other.getLeft()==getLeft()&&

other.getRight()==getRight();

}

return false;//It wasn't a StereoVolume

}

/**

*Returns a hash code value for the StereoVolume.This method must be

*consistent with the{@link#equals}method.

*/

public int hashCode(){

return(int)getLeft()*MAXIMUM*10+getRight();

}

}

❶因为我们想用 Hibernate 持久化这个对象,所以必须提供一个默认的构造函数。

❷以及属性访问器。

❸提供对 Java equals()和 hashCode()比较的支持也是重要的,因为这是一个可变值对象。

为了将这个类作为复合类型进行持久化保存,而非每次使用时都将其定义为嵌套的组合对象,我们需要建立一个复合自定义类型来管理它的持久化处理。在该自定义类型中所需要提供的大部分处理和我们在 SourceMediaType(例 6-2,本章前面演示的一个例子)中提供的差不多。所以这里只集中介绍新鲜而有趣的内容。例 6-9 演示了以复合自定义类型的方式来持久化 StereoVolume 类的一种方法。

例 6-9:用于持久化 StereoVolume 的复合自定义类型(StereoVolumeType.java)

package com.oreilly.hh;

import java.io.Serializable;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import org.hibernate.Hibernate;

import org.hibernate.engine.SessionImplementor;

import org.hibernate.type.Type;

import org.hibernate.usertype.CompositeUserType;

/**

*Manages persistence for the{@link StereoVolume}composite type.

*/

public class StereoVolumeType implements CompositeUserType{

/**

*Get the names of the properties that make up this composite type, and

*that may be used in a query involving it.

*/

public String[]getPropertyNames(){❶

//Allocate a new response each time, because arrays are mutable

return new String[]{"left","right"};

}

/**

*Get the types associated with the properties that make up this composite

*type.

*

*@return the types of the parameters reported by{@link#getPropertynames},

*

in the same order.

*/

public Type[]getPropertyTypes(){

return new Type[]{Hibernate.SHORT, Hibernate.SHORT};

}

/**

*Look up the value of one of the properties making up this composite type.

*

*@param component a{@link StereoVolume}instance being managed.

*@param property the index of the desired property.

*@return the corresponding value.

*@see#getPropertyNames

*/

public Object getPropertyValue(Object component, int property){❷

StereoVolume volume=(StereoVolume)component;

short result;

switch(property){

case 0:

result=volume.getLeft();

break;

case 1:

result=volume.getRight();

break;

default:

throw new IllegalArgumentException("unknown property:"+property);

}

return new Short(result);

}

/**

*Set the value of one of the properties making up this composite type.

*

*@param component a{@link StereoVolume}instance being managed.

*@param property the index of the desired property.

*@object value the new value to be established.

*@see#getPropertyNames

*/

public void setPropertyValue(Object component, int property, Object value){

StereoVolume volume=(StereoVolume)component;

short newLevel=((Short)value).shortValue();

switch(property){

case 0:

volume.setLeft(newLevel);

break;

case 1:

volume.setRight(newLevel);

break;

default:

throw new IllegalArgumentException("unknown property:"+property);

}

}

/**

*Determine the class that is returned by{@link#nullSafeGet}.

*

*@return{@link StereoVolume},the actual type returned by

*

{@link#nullSafeGet}.

*/

public Class returnedClass(){

return StereoVolume.class;

}

/**

*Compare two instances of the class mapped by this type for persistence

*"equality".

*

*@param x first object to be compared.

*@param y second object to be compared.

*@return<code>true</code>iff both represent the same volume levels.

*@throws ClassCastException if x or y isn't a{@link StereoVolume}.

*/

public boolean equals(Object x, Object y){❸

if(x==y){//This is a trivial success

return true;

}

if(x==null||y==null){//Don't blow up if either is null!

return false;

}

//Now it's safe to delegate to the class'own sense of equality

return((StereoVolume)x).equals(y);

}

/**

*Return a deep copy of the persistent state, stopping at entities and

*collections.

*

*@param value the object whose state is to be copied.

*@return a copy representing the same volume levels as the original.

*@throws ClassCastException for non{@link StereoVolume}values.

*/

public Object deepCopy(Object value){❹

if(value==null)

return null;

StereoVolume volume=(StereoVolume)value;

return new StereoVolume(volume.getLeft(),volume.getRight());

}

/**

*Indicates whether objects managed by this type are mutable.

*

*@return<code>true</code>,since{@link StereoVolume}is mutable.

*/

public boolean isMutable(){

return true;

}

/**

*Retrieve an instance of the mapped class from a JDBC{@link ResultSet}.

*

*@param rs the results from which the instance should be retrieved.

*@param names the columns from which the instance should be retrieved.

*@param session an extension of the normal Hibernate session interface

*that gives you much more access to the internals.

*@param owner the entity containing the value being retrieved.

*@return the retrieved{@link StereoVolume}value, or<code>null</code>.

*@throws SQLException if there is a problem accessing the database.

*/

public Object nullSafeGet(ResultSet rs, String[]names,❺

SessionImplementor session, Object owner)

throws SQLException{

Short left=(Short)Hibernate.SHORT.nullSafeGet(rs, names[0]);

Short right=(Short)Hibernate.SHORT.nullSafeGet(rs, names[1]);

if(left==null||right==null){

return null;//We don't have a specified volume for the channels

}

return new StereoVolume(left.shortValue(),right.shortValue());

}

/**

*Write an instance of the mapped class to a{@link PreparedStatement},

*handling null values.

*

*@param st a JDBC prepared statement.

*@param value the StereoVolume value to write.

*@param index the parameter index within the prepared statement at which

*this value is to be written.

*@param session an extension of the normal Hibernate session interface

*that gives you much more access to the internals.

*@throws SQLException if there is a problem accessing the database.

*/

public void nullSafeSet(PreparedStatement st, Object value, int index,

SessionImplementor session)

throws SQLException{

if(value==null){

Hibernate.SHORT.nullSafeSet(st, null, index);

Hibernate.SHORT.nullSafeSet(st, null, index+1);

}else{

StereoVolume vol=(StereoVolume)value;

Hibernate.SHORT.nullSafeSet(st, new Short(vol.getLeft()),index);

Hibernate.SHORT.nullSafeSet(st, new Short(vol.getRight()),

index+1);

}

}

/**

*Reconstitute a working instance of the managed class from the cache.

*

*@param cached the serializable version that was in the cache.

*@param session an extension of the normal Hibernate session interface

*that gives you much more access to the internals.

*@param owner the entity containing the value being retrieved.

*@return a copy of the value as a{@link StereoVolume}instance.

*/

public Object assemble(Serializable cached, SessionImplementor session,❻

Object owner){

//Our value type happens to be serializable, so we have an easy out.

return deepCopy(cached);

}

/**

*Translate an instance of the managed class into a serializable form to be

*stored in the cache.

*

*@param session an extension of the normal Hibernate session interface

*that gives you much more access to the internals.

*@param value the StereoVolume value to be cached.

*@return a serializable copy of the value.

*/

public Serializable disassemble(Object value, SessionImplementor session){

return(Serializable)deepCopy(value);

}

/**

*Get a hashcode for the instance, consistent with persistence"equality"

*/

public int hashCode(Object x){❼

return x.hashCode();//Can delegate to our well-behaved object

}

/**

*During merge, replace the existing(target)value in the entity we are

*merging to with a new(original)value from the detached entity we are

*merging.For immutable objects, or null values, it is safe to simply

*return the first parameter.For mutable objects, it is safe to return a

*copy of the first parameter.However, since composite user types often

*define component values, it might make sense to recursively replace

*component values in the target object.

*

*@param original value being merged from.

*@param target value being merged to.

*@param session the hibernate session into which the merge is happening.

*@param owner the containing entity.

*@return an independent value that can safely be used in the new context.

*/

public Object replace(Object original, Object target,❽

SessionImplementor session, Object owner){

return deepCopy(original);

}

}

❶由于有了 getPropertyNames()和 getPropertyTypes()方法,Hibernate 就可以知道组成复合类型的各“组成部分”。当编写 HQL 查询时就可以使用这些值类型。在这个例子中它们相当于我们正在持久化的实际 StereoVolume 类的属性值,但不是必需的。例如,我们可以借这个机会来为根本不是为持久化而设计的遗留(legacy)类提供一个友好的属性接口。

❷复合用户定义类型的虚拟属性和它们基于的真实数据之间转换是由 getPropertyValue()和 setPropertyValue()方法提供的。在本质上,Hibernate 只是给了我们一个想要管理的类型的实例,而且没有一点假设,它只是说“嗨,给我第二个属性”,或者是说“把第一个属性设置为这个值”。你可以看到如何用这种方法为旧的或第三方代码增加属性接口。在这个例子中,因为我们实际上不需要这种功能,我们要将属性处理传递给底层的 StereoVolume 类,这里要跨越的障碍只是模板类。

接下来的一大段代码由前面例 6-2 中见过的方法组成,不过例子中的版本具有的一些区别很有意思。大部分变化与前面提到的内容有关,不像 SourceMedia,我们的 StereoVolume 类是可变的,它包含了可变化的值。所以,我们得为一些最终适合的方法设计出完整的实现。

❸我们需要在 equals()中提供一种有意义的方法来比较实例。

❹还要在 deepCopy()中实现独立的实例复制。

❺实际的持久化方法(nullSafeGet()和 nullSafeSet())与例 6-2 非常相似,只有一点不同,我们对此不需要过多介绍。它们都有一个 SessionImplementor 参数,通过这个参数可以访问让 Hibernate 正常工作的内部组件。只有真正复杂的持久化处理才需要使用这个参数,这已经超出本书介绍的范围。如果你需要使用 SessionImplementor 方法,在实现上相当有技巧,必须深刻理解 Hibernate 的体系结构。其实你正写的是对系统的扩展,可能需要研究源代码才能获得必需的专业知识。

❻assemble()和 disassemble()方法可以让自定义类型支持对非 Serializable 值的缓存。它们让我们的持久化工具方法有机会可以将任何重要的值复制到另一个能够被序列化(serialized)的对象中(使用任何必要的手段)。因为让 StereoVolume 首先成为可序列化的并不重要,我们也不需要这种灵活性,所以我们的实现只是为了能保存在缓存中复制一些可序列化的 StereoVolume 实例(之所以要复制实例,是因为我们的数据类是可变的,不应该让缓存值也莫名其妙地发生变化)。

❼hashCode()方法是 Hibernate 3 中新增加的方法,虽然需要修改 CompositeUserType 实现,但它有助于提高效率。在这个例子中,我们有一个对象已经实现了这个方法,可以将处理委托给这个对象。但是,再一次强调,如果我们正在封装一些糟糕的遗留数据结构,就可以借这个机会为它们增加一个漂亮的 Java 包装器。

❽最后,replace()方法是 Hibernate 3 要求的另一个新方法。再一次,因为我们需要复制一个对象,可以用一种容易的办法来实现。另外,我们也可以手工将所有内嵌的属性值从最初的对象复制到目标对象。

注意:这么一个简单的值类居然需要做这么多的工作。但是,这正是对更复杂的值类进行建模的好起点。

好了,这头“野兽”已经创建完成,接下来应该怎么用?为了使用该新的复合类型,例 6-10 改进了 Track 映射文档中 volume 属性的配置,同时为了便于查看测试的输出,也趁此机会将它加到了 toString()方法中。

例 6-10:修改 Track.hbm.xml 以使用 StereoVolume

……

<property name="volume"type="com.oreilly.hh.StereoVolumeType">

<meta attribute="field-description">How loud to play the track</meta>

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

<column name="VOL_LEFT"/>

<column name="VOL_RIGHT"/>

</property>

……

再次注意,映射文档中提供的是负责管理持久化的自定义类型的名称,而不是它所管理的原始类型。这与例 6-3 是一样的。此外,这个复合类型使用两个字段存储数据,所以这里也得提供两个字段名称。

现在,当我们执行 ant codegen 命令,为 Track 重新生成 Java 源代码时,可以得到例 6-11 所示的结果。

例 6-11:新生成的 Track.java 源代码的变动之处

……

/**

*How loud to play the track

*/

private StereoVolume volume;

……

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

Set<Artist>artists, Date added,

StereoVolume volume, SourceMedia sourceMedia,

Set<String>comments){

……

}

……

/**

*How loud to play the track

*/

public StereoVolume getVolume(){

return this.volume;

}

public void setVolume(StereoVolume volume){

this.volume=volume;

}

……

public String toString(){

StringBuffer buffer=new StringBuffer();

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

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

buffer.append("title").append("='").append(getTitle()).append("'");

buffer.append("volume").append("='").append(getVolume()).append("'");

buffer.append("sourceMedia").append("='").append(getSourceMedia()).append(

"'");

buffer.append("]");

return buffer.toString();

}

……

此时,可以执行 ant schema 命令来重建数据库表,例 6-12 演示了相关的结果。

例 6-12:根据新的映射而创建的曲目数据库模式

……

[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,

VOL_LEFT smallint, VOL_RIGHT smallint, sourceMedia varchar(255),

primary key(TRACK_ID));

……

让我们进一步改进数据创建的测试程序,使其能够采用新的 Track 结构。例 6-13 演示了我们需要做出的修改。

例 6-13:对 CreateTest.java 做必要的修改以测试立体声音量

……

//Create some data and persist it

tx=session.beginTransaction();

StereoVolume fullVolume=new StereoVolume();

Track track=new Track("Russian Trance",

"vol2/album610/track02.mp3",

Time.valueOf("00:03:30"),

new HashSet<Artist>(),

new Date(),fullVolume, SourceMedia.CD,

new HashSet<String>());

addTrackArtist(track, getArtist("PPK",true, session));

session.save(track);

……

//The other tracks created use fullVolume too, until……

……

track=new Track("Test Tone 1",

"vol2/singles/test01.mp3",

Time.valueOf("00:00:10"),new HashSet<Artist>(),

new Date(),new StereoVolume((short)50,(short)75),

null, new HashSet<String>());

track.getComments().add("Pink noise to test equalization");

session.save(track);

……

现在,如果我们执行 ant ctest,并用 ant db 查看结果,就会看到如图 6-2 所示的结果。

图 6-2 TRACK 表中的立体声音量信息

为了让 AlbumTest 与新的 Track 格式保持兼容,我们只需要对 AlbumTest 做一处修改,如例 6-14 所示。

例 6-14:对 AlbumTest.java 所做的修改,以支持立体声曲目音量

……

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(),new StereoVolume(),SourceMedia.CD,

new HashSet<String>());

……

这样我们就可以运行 ant atest 命令,查看新版的 Track 的 toString()方法所显示的音量信息,如例 6-15 所示。

例 6-15:带有立体声音量信息的专辑

[java]com.oreilly.hh.data.Album@ccad9c[title='Counterfeit e.p.'tracks='[com.oreilly.hh.data.AlbumTrack@9c0287[track='com.oreilly.hh.data.Track@6a21b2[title='Compulsion'volume='Volume[left=100,right=100]'sourceMedia='CD']'],c om.oreilly.hh.data.AlbumTrack@aa8eb7[track='com.oreilly.hh.data.Track@7fc8a0[t itle='In a Manner of Speaking'volume='Volume[left=100,right=100]'sourceMedia='CD']'],com.oreilly.hh.data.AlbumTrack@4cadc4[track='com.oreilly.hh.data.Tra ck@243618[title='Smile in the Crowd'volume='Volume[left=100,right=100]'sourc eMedia='CD']'],com.oreilly.hh.data.AlbumTrack@5b644b[track='com.oreilly.hh.d ata.Track@157e43[title='Gone'volume='Volume[left=100,right=100]'sourceMedia='CD']'],com.oreilly.hh.data.AlbumTrack@1483a0[track='com.oreilly.hh.data.Tra ck@cdae24[title='Never Turn Your Back on Mother Earth'volume='Volume[left=100,right=100]'sourceMedia='CD']'],com.oreilly.hh.data.AlbumTrack@63dc28[track='com.oreilly.hh.data.Track@ae511[title='Motherless Child'volume='Volume[left=100,right=100]'sourceMedia='CD']']]']

嗯,这里介绍的内容可能深入了一点,超过了你目前对自定义类型所需要的程度。但是,总有一天你也许会回过头来深入研究这个例子,找到你正在寻找的主题。同时,接下来让我们换档去看一看完全不同、全新而且简单的事物。第 7 章将介绍一种完全取代 XML 映射文档的方法。第 8 章将介绍条件查询,这是 Hibernate 独特的功能,对于程序员来说这个功能非常友好。

其他

映射自定义类型还有哪些奇特的诀窍?好吧,如果本章介绍的信息还不够用,你可以研究一下 org.hibernate.usertype 包中的其他接口,包括 EnhancedUserType(这个接口将自定义类型作为实体的 id,以及其他有趣的窍门)和 ParameterizedUserType(可以配置它支持多种不同类型的映射)等。在 Hibernate 维基的 Java 5 EnumUserType( [1] )页面上也讨论了针对 Java 5 enum 枚举类型的可重用映射,这些都很好地演示了各种用法,只是对它们的讨论已经超出了本书的范围。

[1] http://www.hibernate.org/272.html.

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

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

发布评论

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