So far, we’ve been working with the XML mapping document as the starting point for our examples. In cases where you’re starting with just a concept and can leave the details of creating data tables and data objects to Hibernate, that remains a great option. The advent of Java 5’s flexible Annotation support opened up a very interesting alternate approach, however, especially for the common case where you’ve already got some objects written by the time that you’re thinking about how to save them to a database.
If you haven’t started using annotations yet, the code examples in this chapter will look a little strange, so it’s worth spending a minute or two to discuss the history and purpose of Java annotations. Basically, an annotation is a way to add information about a piece of code (in the Java world, typically a class, field, or method) to help tools understand how the code is being used, or to enable automation that saves you work. Rather than having a separate file, like a persistence mapping, to maintain in parallel with your source code, you would put that information right in the source code it affected. This way you are in no danger of a file separate from your source code becoming out of synch. Before Java 5 included robust support for this style of coding, people found a “back door” way of achieving it, by leveraging the extensible nature of the JavaDoc tools.
JavaDoc was a form of annotation that existed in Java from the beginning, with the purpose of enabling developers to produce quality documentation of their classes and APIs without having to maintain a set of files separate from the source code, and it worked very well, so much so that people wanted to be able to use it for other things. The XDoclet project was a popular and sophisticated framework that extended the JavaDoc framework in many interesting ways, and a rich set of Hibernate XDoclet tags were developed.
The power of this approach was so evident that Sun built full general-purpose annotation support right into Java 5, eliminating the need for tricky tools that leveraged JavaDoc comments. Now the Java compiler itself can process annotations (and uses some of its own to let you control specific warnings within individual classes, methods or even fields). Reflection can report on annotations (if they’re configured to stick around in compiled classes), and you can define your own annotation classes very easily. So naturally Hibernate adopted Java 5’s native annotations.
Convergent evolution went even farther than that, though—the power of using annotations to configure mapping for classes has such appeal that Hibernate’s own tags strongly influenced the EJB 3 specification. They also were impressed by how useful Hibernate-style persistence could be outside a full-blown Java Enterprise Edition (EE) environment, so they defined the Java Persistence API (often referred to as the JPA) as a stand-alone component which can be used for persistence in a normal Java Standard Edition (SE) environment. As the specifications firmed up, Hibernate adapted to support them directly, so you can now use many of Hibernate’s features through the EJB 3 and JPA interfaces and annotations, without making your application dependent on (or aware of) Hibernate at all.
We’re not going to go that far, since several of the features we’re already using in Hibernate require use of its own tags. In fact, once you get used to Hibernate, and its very flexible and powerful philosophy of “persist any old Java object, any way you need to,” you’ll find the effort of warping your code to fit the limits of the JPA specification too confining to consider except in cases where there is a strong mandate to stay agnostic about persistence implementations (and you will probably try to avoid such projects). Yet, even if you know you’re using Hibernate, sometimes it’s nice to use annotations to configure your mappings instead of XML files.
Once you’re used to annotation syntax (and IDEs like Eclipse already have surprisingly sophisticated abilities to understand, document, and autocomplete annotations—even custom ones you may come up with for your own use, a technique we’ll use in Chapter 14), ditching mapping documents means there’s one less file format you need to remember how to set up and read.
What, the introduction didn’t sell you on how cool they are?
As noted earlier, if you can keep your object and mapping specifications in one file, they’re less likely to get out of synch, and readers will have a more immediate understanding of what’s going on. It’s a form of self-documenting code. The reasonable defaults provided by the annotations often allow you to get away with specifying less configuration as well. Even when you specify everything, you’ll find the annotation syntax much more compact than the XML file, so you have less to type, or to read through once your IDE has helped type it.
This approach is most likely to work well when you have direct control over the database and it is strongly coupled to your object model (or if you are designing objects to work with a specific, fixed database structure, which yields the same kind of strong coupling). This means that cases where you need to adapt an object model to multiple databases aren’t good candidates for use of annotations; in that kind of situation, the separate nature of an external mapping file is actually an advantage, since you can have multiple separate mapping file sets without affecting your Java source.
Many other Java tools are adopting annotations for configuration and integration, as you’ll see starting in Chapter 13. When working with such tools it is often most convenient to use annotations with Hibernate as well—indeed, the other tools may depend on them. Had we not already wanted to show you how to use annotations in this new version of the book, the Spring chapter would have forced the issue.
One criticism of annotations is that database details get lost
in the shuffle of Java source code. Luckily, there are some useful
Hibernate tools available that will generate Hibernate Mapping
documentation from both Hibernate XML mapping files
and Hibernate Annotations. For more information about hbm2doc
, see Generating Hibernate mapping
documentation” in Chapter 12.
The first thing we need to do is update our Maven dependencies to retrieve the Hibernate annotations. Edit build.xml so the dependencies look like Example 7-1 (the additions are shown in bold).
<artifact:dependencies pathId="dependency.class.path"> <dependency groupId="hsqldb" artifactId="hsqldb" version="1.8.0.7"/> <dependency groupId="org.hibernate" artifactId="hibernate" version="3.2.5.ga"> <exclusion groupId="javax.transaction" artifactId="jta"/> </dependency> <dependency groupId="org.hibernate" artifactId="hibernate-tools" version="3.2.0.beta9a"/><dependency groupId="org.hibernate" artifactId="hibernate-annotations"
version="3.3.0.ga"/>
<dependency groupId="org.hibernate"
artifactId="hibernate-commons-annotations"
version="3.3.0.ga"/>
<dependency groupId="org.apache.geronimo.specs" artifactId="geronimo-jta_1.1_spec" version="1.1"/> <dependency groupId="log4j" artifactId="log4j" version="1.2.14"/> </artifact:dependencies>
While in the file, delete the usertypes
and codegen
targets. Working with annotations,
we are not going to generate Java code. Instead, we’ll be
starting with Java and using that to define the Hibernate mapping (and
the database schema). So those targets are obsolete (and dangerous) in
this chapter.
As you might expect, the schema
target itself needs a little
tweaking to work in this new way, as highlighted in Example 7-2.
<!-- Generate the schemas forannotated
classes --> <target name="schema" depends="compile" description="Generate DB schema from theannotated model classes
"> <hibernatetool destdir="${source.root}"><classpath refid="project.class.path"/>
<annotationconfiguration
configurationfile="${source.root}/hibernate.cfg.xml"/>
<hbm2ddl drop="yes"/> </hibernatetool> </target>
The comment and target description needed to be updated to reflect the way things now work.
We need a reference to the compiled classes, so the
annotations within them can be found by the schema generation tool.
Note that this means we need the classes to be compiled before a
schema can be generated, which was not the case in most earlier
versions of this target, but we added that dependency in Chapter 6, so our
schema
target already depends
on the compile
target.
Finally, we tell the tool to configure itself using annotations. We still supply an overall Hibernate configuration file, so the tool knows what database we’re using and such. And that file also must list the annotated classes with which we want to work, which we’ll tackle after one last build adjustment.
Our compile
target used to
depend on the code-generation target we’ve deleted, so it won’t work
until we remove that dependency. In our new approach, all that’s
needed to support compilation is for our basic prepare
target to have run. Edit the compile
target to reflect the changes
highlighted in Example 7-3.
<!-- Compile the java source of the project -->
<target name="compile" depends="prepare
"
description="Compiles all Java classes">
<javac srcdir="${source.root}"
destdir="${class.root}"
debug="on" optimize="off" deprecation="on">
<classpath refid="project.class.path"/>
</javac>
</target>
As noted earlier, we need to update the Hibernate configuration to list the annotated classes with which we want to work instead of the mapping documents we used to use. Delete those documents, and change the end of hibernate.cfg.xml in the src directory to look like Example 7-4 (as usual, the changes are highlighted in bold).
... <!-- Don't echo all executed SQL to stdout --> <property name="show_sql">false</property> <!-- disable batching so HSQLDB will propagate errors correctly. --> <property name="jdbc.batch_size">0</property> <!-- List all theannotated classes
we're using --><mapping class="com.oreilly.hh.data.Album"/>
<mapping class="com.oreilly.hh.data.AlbumTrack"/>
<mapping class="com.oreilly.hh.data.Artist"/>
<mapping class="com.oreilly.hh.data.Track"/>
</session-factory> </hibernate-configuration>
You might wonder why it’s necessary to list the annotated classes
in the configuration file. Can’t Hibernate find them by their
annotations? Well, it can, and if you change your coding style to
fully rely on the JPA interfaces (using
Hibernate’s implementation of the JPA
EntityManager
instead of a Hibernate
Session
), it will quite happily look for your
annotated classes without being told where to find them. But, as noted
earlier, we’re not going that far in this book. When you stick with
Hibernate’s native interfaces, it wants explicit declaration of the
classes for which you want it to perform persistence, even when you’re
using annotations to control that persistence.
We’re almost ready to show this working, except we need the annotated classes that form the core of this approach! So it’s time to get to the really interesting part of this chapter, and show you what annotated Java source code for data objects looks like, and how it controls Hibernate mappings.
Annotations can be applied to a variety of Java elements. When working with Hibernate, you’ll most often be concerned with annotating classes and their fields, to specify how a model object is to be mapped to a database schema. This is analogous to the way our XML mapping documents were structured around mapped classes and their properties. That’s enough background and explanation—let’s dive right in and make this concrete. Time to map the classes from the examples (as fully developed in Chapter 6) using annotations!
Example 7-5
shows one way you could annotate the Artist
class. This chapter will explain the basics of Hibernate Annotations,
but if you would like a more thorough description of some of these
annotations, please refer to the Hibernate Annotations project web site
at http://annotations.hibernate.org[5]. To save pages, the listings of annotated classes are
compressed slightly from what you’ll find in the source
download—whitespace is condensed and the JavaDoc is omitted. Since these
are hand-written classes, not code-generated ones as they were in prior
chapters, there is room for much more comprehensive JavaDoc, so it’s
worthwhile to take a look at the downloadable versions, too.
package com.oreilly.hh.data; import java.util.*; import javax.persistence.*; import org.hibernate.annotations.Index; @Entity @Table(name="ARTIST") @NamedQueries({ @NamedQuery(name="com.oreilly.hh.artistByName", query="from Artist as artist where upper(artist.name) = upper(:name)") }) public class Artist { @Id @Column(name="ARTIST_ID") @GeneratedValue(strategy=GenerationType.AUTO) private Integer id; @Column(name="NAME",nullable=false,unique=true) @Index(name="ARTIST_NAME",columnNames={"NAME"}) private String name; @ManyToMany @JoinTable(name="TRACK_ARTISTS", joinColumns={@JoinColumn(name="TRACK_ID")}, inverseJoinColumns={@JoinColumn(name="ARTIST_ID")}) private Set<Track> tracks; @ManyToOne @JoinColumn(name="actualArtist") private Artist actualArtist; public Artist() {} public Artist(String name, Set<Track> tracks, Artist actualArtist) { this.name = name; this.tracks = tracks; this.actualArtist = actualArtist; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Artist getActualArtist() { return actualArtist; } public void setActualArtist(Artist actualArtist) { this.actualArtist = actualArtist; } public Set<Track> getTracks() { return tracks; } public void setTracks(Set<Track> tracks) { this.tracks = tracks; } /** * Produce a human-readable representation of the artist. * * @return a textual description of the artist. */ public String toString() { StringBuilder builder = new StringBuilder(); builder.append(getClass().getName()).append("@"); builder.append(Integer.toHexString(hashCode())).append(" ["); builder.append("name").append("='").append(getName()).append("' "); builder.append("actualArtist").append("='").append(getActualArtist()); builder.append("' ").append("]"); return builder.toString(); } }
In order to use annotations that aren’t core to the Java language itself, such as the persistence-related ones we’re discussing here, you need to import them. Annotations are just Java classes (albeit ones which implement a particular interface, and which are declared in an interesting way that’s beyond the scope of this chapter, but which you can see in Chapter 14), so you import them using ordinary import statements like these.
As discussed in the history earlier, most of the annotations we need are the standard EJB 3 variety, which are defined in the javax.persistence package. We do need one Hibernate-specific annotation in order to be able to request a specific index. The Java Persistence API was downloaded and made available to us automatically by Maven because it’s a dependency of the Hibernate Annotations we requested in the updated build.xml. We can use the Java Persistence API on its own like this because it was designed to be usable by itself within a Java SE environment like ours, as well as part of the EJB support built into Java EE. You may want to learn more about it, and its home page is a good starting point.
This query language relationship shows Hibernate really did influence the direction of EJB 3 in a deep way.
This cluster of annotations applies to the
Artist
class as a whole. The
Entity
annotation marks the class as capable
of persistence. The Table
annotation is
optional; the annotation processor will make very reasonable default
assumptions for mappings, but we wanted to show how to explicitly
state a table name, in case you’re connecting to an existing
database with strange names.
And what is our query doing back in the Java source? Alas,
this is another drawback to using Hibernate-native interfaces
with annotations: there’s nowhere else to put the named
queries. If we were using the JPA EntityManager
, we could put
named queries in a persistence.xml file, and retain the
advantages of keeping them outside of Java source code. Since we’re
sticking with the Session
interface in this
book, we lose one of the advantages of named queries when we use
annotations instead of XML mapping files. But, we
can still write them in HQL, and use all its
features. Switching to the JPA interfaces would
require us to use JPAQL instead, which is a
subset of HQL.
Here’s how you annotate a mapped property. This is a
special case, because the property is also the unique
identifier for the object, as indicated by the @Id
annotation. You can specify different
kinds of ID generation strategies with the
annotations, just as you can with Hibernate’s XML
mapping documents. (The annotations were intended to be a full
replacement for the capabilities of existing O/R
layers, and between the standard JPA choices and
Hibernate’s own, you really can do almost anything you need.)
Choosing AUTO
for the generation
style is the annotations-based equivalent of specifying
<generator class="native"/>
in
XML. It tells Hibernate to use whatever approach
is most natural for the database being used.
As with the entity-level annotations, if you omit some
choices like the column name, the defaults chosen are quite
reasonable, but we wanted to illustrate how to be specific when you
need to. In fact, you don’t need to annotate the properties at
all—the JPA will assume that all properties of an
entity are to be mapped, unless instructed otherwise (through
annotations, naturally: @Transient
serves this purpose).
Also note that we’ve attached the annotations to the actual field, rather than to an accessor method. This tells Hibernate to access the field directly, which you might want to do in a class where the accessors are good for providing abstractions to other classes at runtime, but not compatible with persistence. In many cases, you’ll want to have Hibernate use the accessors, which you’d achieve by putting the annotations on the getter or setter method. You need to pick one approach or the other—mixing and matching from property to property is not allowed (by JPA, although Hibernate has extensions… but it may confuse others even if you can do it).
When mapping a column, there are a number of optional attributes you can supply to control things such as nullability, uniqueness constraints, and so on, just like when you’re doing it in an XML mapping file.
The ability to specify that a column should have an index (and
how that index should be set up) is one reason we added the
Hibernate annotations to the mix. This @Index
tag is not part of the standard JPA
annotations; it’s a useful Hibernate extension. Relying on it makes
our code dependent on Hibernate, but beyond the fact that this is a
book about Hibernate, as we noted earlier (and
you’ll see later), there are a
lot of reasons why you’ll often make the same choice.
As with mapping documents, there are more options when it
comes to associations. In this case we’re describing a many-to-many
relationship with Track
objects, and
explicitly spelling out how that relationship is represented in the
database.
Sometimes there really isn’t much you need to say, even when
you’re being explicit. This is an example of how the annotation
approach can be significantly more concise than
XML mapping files. We do use an @JoinColumn
annotation to keep the column name the same as it was in our
XML-based approach. It works fine without this,
but the default column name is the slightly more verbose ACTUALARTIST_ARTIST_ID
.
Other than that, there isn’t much to say about the class—it’s a simple data bean, very similar to the one that was generated from our mapping document in earlier chapters. The JavaDoc in the downloadable version explains the fields and methods better than was possible in generated code.
This annotated class produces the same ARTIST
table we achieved with the Artist.hbm.xml mapping
document we developed in the preceding chapters.
The annotated Artist
class references the
Track
class. Example 7-6 shows the annotations on
Track
, which introduce some new issues.
package com.oreilly.hh.data; import java.sql.Time; import java.util.*; import javax.persistence.*; import org.hibernate.annotations.CollectionOfElements; import org.hibernate.annotations.Index; @Entity @Table(name="TRACK") @NamedQueries({ @NamedQuery(name="com.oreilly.hh.tracksNoLongerThan", query="from Track as track where track.playTime <= :length") }) public class Track { @Id @Column(name="TRACK_ID") @GeneratedValue(strategy=GenerationType.AUTO) private Integer id; @Column(name="TITLE",nullable=false) @Index(name="TRACK_TITLE",columnNames={"TITLE"}) private String title; @Column(nullable=false) private String filePath; @Temporal(TemporalType.TIME) private Date playTime; @ManyToMany @JoinTable(name="TRACK_ARTISTS", joinColumns={@JoinColumn(name="ARTIST_ID")}, inverseJoinColumns={@JoinColumn(name="TRACK_ID")}) private Set<Artist> artists; @Temporal(TemporalType.DATE) private Date added; @CollectionOfElements @JoinTable(name="TRACK_COMMENTS", joinColumns = @JoinColumn(name="TRACK_ID")) @Column(name="COMMENT") private Set<String> comments; @Enumerated(EnumType.STRING) private SourceMedia sourceMedia; @Embedded @AttributeOverrides({ @AttributeOverride(name = "left", column = @Column(name = "VOL_LEFT")), @AttributeOverride(name = "right", column = @Column(name = "VOL_RIGHT")) }) StereoVolume volume; public Track() {} public Track(String title, String filePath) { this.title = title; this.filePath = filePath; } public Track(String title, String filePath, Time playTime, Set<Artist> artists, Date added, StereoVolume volume, SourceMedia sourceMedia, Set<String> comments) { this.title = title; this.filePath = filePath; this.playTime = playTime; this.artists = artists; this.added = added; this.volume = volume; this.sourceMedia = sourceMedia; this.comments = comments; } public Date getAdded() { return added; } public void setAdded(Date added) { this.added = added; } public String getFilePath() { return filePath; } public void setFilePath(String filePath) { this.filePath = filePath; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Date getPlayTime() { return playTime; } public void setPlayTime(Date playTime) { this.playTime = playTime; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Set<Artist> getArtists() { return artists; } public void setArtists(Set<Artist> artists) { this.artists = artists; } public Set<String> getComments() { return comments; } public void setComments(Set<String> comments) { this.comments = comments; } public SourceMedia getSourceMedia() { return sourceMedia; } public void setSourceMedia(SourceMedia sourceMedia) { this.sourceMedia = sourceMedia; } public StereoVolume getVolume() { return volume; } public void setVolume(StereoVolume volume) { this.volume = volume; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(getClass().getName()).append("@"); builder.append(Integer.toHexString(hashCode())).append(" ["); builder.append("title").append("='").append(getTitle()).append("' "); builder.append("volume").append("='").append(getVolume()).append("' "); builder.append("sourceMedia").append("='").append(getSourceMedia()); builder.append("' ").append("]"); return builder.toString(); } }
Since Java lacks distinct classes for representing times of
day as compared to dates (or timestamps which include both), we need
a way of declaring how a Date
class is being
used. The @Temporal
annotation provides that clarity. In this case we are annotating
the fact that playTime
corresponds to a TIME
column in
SQL. If this annotation is omitted, the default
column type used is TIMESTAMP
.
The added
property, though
it shares the same Date
type with playTime
, corresponds
to a DATE
column.
The @CollectionOfElements
annotation is another Hibernate extension, which
gives us a simpler way to control the table and column in which an
association to a collection of a simple value type is mapped. This
is one of the biggest reasons to break out of the standard
annotations. With pure JPA, you can’t directly
map collections of simple value types like
String
or Integer
at
all. You need to declare a full-blown entity class to hold such
values, and then map that. For people who are used to Hibernate’s
flexibility in mapping Plain Old Java Objects, this would be a big
step backwards.
To learn about all Hibernate’s extensions to the mapping annotations, you can take advantage of the fact that they are all listed nicely in a section of the online manual.
On the other hand, the JPA has robust
support for mapping enumerations built right in, one advantage it
gained by coming out after the Java 5
enum
(and the widespread adoption of the
type-safe enumeration pattern that led to this language feature). This annotation is
a lot easier than the work we had to go through in Chapter 6!
This is the standard JPA annotation
for mapping a composite user type, the equivalent of
Example 6-10. The JPA spec
requires that when we do this we also mark the
StereoVolume
class as embeddable:
package com.oreilly.hh.data; import java.io.Serializable;import javax.persistence.Embeddable;
/** * A simple structure encapsulating a stereo volume level. */@Embeddable
public class StereoVolume implements Serializable { ... }
(In fact, Hibernate is happy to map the embedded class without us doing this, but a future release might be more stringent about sticking to the spec, and there is no harm in being well-behaved.)
Once again we’ve added more annotations than are really
needed, in order to end up with exactly the same database schema we
had in the XML-mapped version of the
examples. Without the @AttributeOverrides
, the columns used to
store the two pieces of the volume would be just LEFT
and RIGHT
(the names of the properties in the
StereoVolume
class), rather than VOL_LEFT
and VOL_RIGHT
.
In order for our test programs to print out the same
information as they did using the old approach, we’ve copied over
the toString()
implementations
Hibernate’s code generator created for us. Note that we took the
opportunity to upgrade them to use
StringBuilder
rather than the unnecessarily
thread-safe[6] (and therefore slower)
StringBuffer
.
This set of annotations causes the creation of the TRACK
, TRACK_ARTISTS
, and TRACK_COMMENTS
tables, with the same
schema that we achieved using Track.hbm.xml as it evolved in Examples 2-1 through 6-10.
The Album
class is the core model class of
the rest of the examples we’ve built so far. Example 7-7 shows how to
annotate it to recreate the database schema and mappings with which
we’ve been working, and introduces a few more concepts worth
noting.
package com.oreilly.hh.data; import java.util.*; import javax.persistence.*; import org.hibernate.annotations.CollectionOfElements; import org.hibernate.annotations.Index; import org.hibernate.annotations.IndexColumn; @Entity @Table(name="ALBUM") public class Album { @Id @Column(name="ALBUM_ID") @GeneratedValue(strategy=GenerationType.AUTO) private Integer id; @Column(name="TITLE",nullable=false) @Index(name="ALBUM_TITLE",columnNames={"TITLE"}) private String title; @Column(nullable=false) private Integer numDiscs; @ManyToMany(cascade=CascadeType.ALL) @JoinTable(name="ALBUM_ARTISTS", joinColumns=@JoinColumn(name="ARTIST_ID"), inverseJoinColumns=@JoinColumn(name="ALBUM_ID")) private Set<Artist> artists; @CollectionOfElements @JoinTable(name="ALBUM_COMMENTS", joinColumns = @JoinColumn(name="ALBUM_ID")) @Column(name="COMMENT") private Set<String> comments; @Temporal(TemporalType.DATE) private Date added; @CollectionOfElements @IndexColumn(name="LIST_POS") @JoinTable(name="ALBUM_TRACKS", joinColumns = @JoinColumn(name="ALBUM_ID")) private List<AlbumTrack> tracks; public Album() {} public Album(String title, int numDiscs, Set<Artist> artists, Set<String> comments, List<AlbumTrack> tracks, Date added) { this.title = title; this.numDiscs = numDiscs; this.artists = artists; this.comments = comments; this.tracks = tracks; this.added = added; } public Date getAdded() { return added; } public void setAdded(Date added) { this.added = added; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Integer getNumDiscs() { return numDiscs; } public void setNumDiscs(Integer numDiscs) { this.numDiscs = numDiscs; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public List<AlbumTrack> getTracks() { return tracks; } public void setTracks(List<AlbumTrack> tracks) { this.tracks = tracks; } public Set<Artist> getArtists() { return artists; } public void setArtists(Set<Artist> artists) { this.artists = artists; } public Set<String> getComments() { return comments; } public void setComments(Set<String> comments) { this.comments = comments; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(getClass().getName()).append("@"); builder.append(Integer.toHexString(hashCode())).append(" ["); builder.append("title").append("='").append(getTitle()).append("' "); builder.append("tracks").append("='").append(getTracks()).append("' "); builder.append("]"); return builder.toString(); } }
Yup, we keep being unable to do without Hibernate-specific extensions. No fewer than three in this class.
AlbumTrack
is not an entity (it doesn’t
have an ID property, and instances can’t be looked
up on their own outside the context of an
Album
record), so we map the
association using @CollectionOfElements
(as we do for
basic types) rather than @OneToMany
, which we’d use for an
entity.
JPA and EJB only support
set-like semantics for mapped collections. As we saw in Chapter 5,
being able to keep rows in a particular order is important, which is
why we use data structures like List
s and
arrays, and Hibernate makes it easy to map them. You can’t in pure
JPA, however! Hibernate’s @IndexColumn
extension is the way around
that.
The combination of this annotation and the @JoinColumn
information that follows causes Hibernate to produce exactly the
schema we had based on Album.hbm.xml in Example 5-4, in which
the ALBUM_TRACKS
table has a
composite key formed from ALBUM_ID
and LIST_POS
.
Album
is closely related to the
AlbumTrack
class, which gets annotated as shown
in Example 7-8 to
round out the recreation of our example schema.
package com.oreilly.hh.data; import java.io.Serializable; import javax.persistence.*; @Embeddable public class AlbumTrack { @ManyToOne(cascade=CascadeType.ALL) @JoinColumn(name="TRACK_ID", nullable=false) 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 track; } public void setTrack(Track track) { this.track = track; } public Integer getDisc() { return disc; } public void setDisc(Integer disc) { this.disc = disc; } public Integer getPositionOnDisc() { return positionOnDisc; } public void setPositionOnDisc(Integer positionOnDisc) { this.positionOnDisc = positionOnDisc; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(getClass().getName()).append("@"); builder.append(Integer.toHexString(hashCode())).append(" ["); builder.append("track").append("='").append(getTrack()).append("' "); builder.append("]"); return builder.toString(); } }
As described earlier in the discussion of the
Album
mapping, this class is not an entity
capable of standing on its own, so we map it with @Embeddable
, as we
did StereoVolume
.
Even though it’s not an entity, we need to tell Hibernate
how to handle the track
property, which
does refer to an entity. Without this annotation, attempts to build
the schema will fail. This also gives us the ability to retain the
same column name we had in the XML-based
approach. Unfortunately, the request to cascade through to the
Track
class isn’t enough to automatically
save tracks when creating a new album, so we’ll need to go back to
an earlier version of the AlbumTest
class, as shown later. In
“An Alternate
Approach” later in this chapter, we’ll
explore a variation on the schema which regains this
automation.
These last two properties illustrate the fact that when using annotations, sometimes you don’t need to supply any annotations at all. These two properties do get mapped, naked though they look, and the defaults provided by the annotation processor are just what we want them to be.
The rest of the class is straightforward, and shorter than the others we’ve looked at, as there’s no ID property to manage, and only a handful of other properties.
As mentioned in the discussion earlier, this mapping requires us
to reclaim the responsibility for saving tracks when creating new
albums. With the final version of the mapping in Album.hbm.xml in Example 5-13, we
commented out the line that called session.save(track)
in
the addAlbumTrack()
method of
AlbumTest.java
. We need to uncomment that line so
the method looks the way it did in Example 5-8.
With all this in place, we can create the schema. Example 7-9 shows most of the result of running ant schema with the annotations-based approach we’ve now set up. A few less-interesting stretches have been omitted to save space, and the table-creation lines have been reformatted for better readability.
%
ant schema
Buildfile: build.xml
Downloading: org/hibernate/hibernate-annotations/3.3.0.ga/hibernate-annotations-
3.3.0.ga.pom
Transferring 1K
Downloading: org/hibernate/hibernate/3.2.1.ga/hibernate-3.2.1.ga.pom
Transferring 3K
Downloading: javax/persistence/persistence-api/1.0/persistence-api-1.0.pom
Transferring 1K
Downloading: org/hibernate/hibernate-commons-annotations/3.3.0.ga/hibernate-comm
ons-annotations-3.3.0.ga.pom
Transferring 1K
Downloading: org/hibernate/hibernate-annotations/3.3.0.ga/hibernate-annotations-
3.3.0.ga.jar
Transferring 258K
Downloading: javax/persistence/persistence-api/1.0/persistence-api-1.0.jar
Transferring 50K
Downloading: org/hibernate/hibernate-commons-annotations/3.3.0.ga/hibernate-comm
ons-annotations-3.3.0.ga.jar
Transferring 64K
prepare:
[copy] Copying 1 file to /Users/jim/svn/oreilly/hibernate/current/examples/
ch07/classes
compile:
[javac] Compiling 10 source files to /Users/jim/svn/oreilly/hibernate/curren
t/examples/ch07/classes
schema:
[hibernatetool] Executing Hibernate Tool with a Hibernate Annotation/EJB3 Config
uration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] alter table ALBUM_ARTISTS drop constraint FK7BA403FCB99A6003;
...
[hibernatetool] alter table TRACK_COMMENTS drop constraint FK105B2688E424525B;
[hibernatetool] drop table ALBUM if exists;
...
[hibernatetool] drop table TRACK_COMMENTS if exists;
[hibernatetool] create table ALBUM (ALBUM_ID integer generated by default
as identity (start with 1), added date, numDiscs integer,
TITLE varchar(255) not null,
primary key (ALBUM_ID));
[hibernatetool] create table ALBUM_ARTISTS (ARTIST_ID integer not null,
ALBUM_ID integer not null,
primary key (ARTIST_ID, ALBUM_ID));
[hibernatetool] create table ALBUM_COMMENTS (ALBUM_ID integer not null,
COMMENT varchar(255));
[hibernatetool] create table ALBUM_TRACKS (ALBUM_ID integer not null,
disc integer, positionOnDisc integer, TRACK_ID integer,
LIST_POS integer not null,
primary key (ALBUM_ID, LIST_POS));
[hibernatetool] create table ARTIST (ARTIST_ID integer generated by default
as identity (start with 1), NAME varchar(255) not null,
actualArtist integer,
primary key (ARTIST_ID), unique (NAME));
[hibernatetool] create table TRACK (TRACK_ID integer generated by default
as identity (start with 1), added date,
filePath varchar(255) not null, playTime time,
sourceMedia varchar(255), TITLE varchar(255) not null,
VOL_LEFT smallint, VOL_RIGHT smallint,
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 table TRACK_COMMENTS (TRACK_ID integer not null,
COMMENT varchar(255));
[hibernatetool] create index ALBUM_TITLE on ALBUM (TITLE);
[hibernatetool] alter table ALBUM_ARTISTS add constraint FK7BA403FCB99A6003 fore
ign key (ARTIST_ID) references ALBUM;
...
[hibernatetool] alter table TRACK_COMMENTS add constraint FK105B2688E424525B for
eign key (TRACK_ID) references TRACK;
[hibernatetool] 9 errors occurred while performing <hbm2ddl>.
[hibernatetool] Error #1: java.sql.SQLException: Table not found: ALBUM_ARTISTS
in statement [alter table ALBUM_ARTISTS]
...
BUILD SUCCESSFUL
Total time: 5 seconds
Since this is the first time we’ve asked the Maven Ant Tools for the Hibernate Annotations, they are downloaded along with their dependencies.
Here’s where you can see that annotations are being used to drive the creation of the schema.
If you compare this with the corresponding line in Example 5-7,
you’ll see that the column definitions are identical, although they
show up in a different order. The same is true for the definitions
of ALBUM_ARTISTS
, ALBUM_COMMENTS
, and, most challengingly,
ALBUM_TRACKS
. We’ve successfully
recreated our original schema.
These are the normal errors you see in creating a schema when the database didn’t exist at all at the time. They don’t abort the build, and it is considered successful despite them.
You may also want to run ant db
simultaneously in this chapter’s folder with another copy running in the
folder for Chapter 5, as an easier visual way of comparing
schema definitions. Also, looking at actual data might be nice. Example 7-10
shows the two small changes we need to make to CreateTest.java to get it working with
annotation-based mappings. We need to import the AnnotationConfiguration
class, and
then use it where we previously used the
Configuration
class.
package com.oreilly.hh; import org.hibernate.*;import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Configuration; ... public static void main(String args[]) throws Exception { // Create a configuration based onthe annotations in our
//model classes
. Configuration config = newAnnotationConfiguration
(); config.configure(); ...
Once that’s done, you can run ant ctest to create sample data and perform the same side-by-side comparison using multiple instances of ant db.
The same changes need to be made to QueryTest.java, QueryTest2.java, and AlbumTest.java. We won’t
show the output from running ant
qtest or ant qtest2 since
it’s the same as in the previous chapter. But we will show the output
from AlbumTest
because it
relies on the entire schema, and so will be a good sanity check. This
also acts at a verification that the track-saving line was uncommented
successfully as described in the discussion after Example 7-8. The output
from running ant atest with our
annotations-based schema is shown in Example 7-11.
atest: [java] com.oreilly.hh.data.Album@27d19d [title='Counterfeit e.p.' tracks='[ com.oreilly.hh.data.AlbumTrack@bf4c80 [track='com.oreilly.hh.data.Track@2e3919 [ title='Compulsion' volume='Volume[left=100, right=100]' sourceMedia='CD' ]' ], c om.oreilly.hh.data.AlbumTrack@3778cf [track='com.oreilly.hh.data.Track@f4d063 [t itle='In a Manner of Speaking' volume='Volume[left=100, right=100]' sourceMedia= 'CD' ]' ], com.oreilly.hh.data.AlbumTrack@dc696e [track='com.oreilly.hh.data.Tra ck@a5dac0 [title='Smile in the Crowd' volume='Volume[left=100, right=100]' sourc eMedia='CD' ]' ], com.oreilly.hh.data.AlbumTrack@8dbef1 [track='com.oreilly.hh.d ata.Track@c4b579 [title='Gone' volume='Volume[left=100, right=100]' sourceMedia= 'CD' ]' ], com.oreilly.hh.data.AlbumTrack@f2f761 [track='com.oreilly.hh.data.Tra ck@8cd64 [title='Never Turn Your Back on Mother Earth' volume='Volume[left=100, right=100]' sourceMedia='CD' ]' ], com.oreilly.hh.data.AlbumTrack@4f1541 [track= 'com.oreilly.hh.data.Track@c042ba [title='Motherless Child' volume='Volume[left= 100, right=100]' sourceMedia='CD' ]' ]]' ]
Apart from the differences in memory addresses where Java happens to have loaded classes, this is identical to the output we saw in Example 5-10, just as we hoped.
It worked! It all worked!
This exercise has shown that annotations are clearly a viable way
of mapping model classes. By jumping through a few hoops we
were able to maintain the exact schema we’d evolved in the preceding
chapters, though we lost the ability to cascade creation of
Tracks
during the creation of an
Album
. There’s another approach we could have taken
in the schema which would maintain that automatic cascade, and give us
some other abilities, as well, if we think about the
AlbumTrack
class slightly differently.
Mapping AlbumTrack
as a full-blown entity
gives us places to put cascade annotations that Hibernate will honor all
the way from the Album
definition to the embedded
Track
reference. It also gives us a few new
complications to think about, but some of those can be seen as
opportunities. First of all, AlbumTrack
as an
entity will need an ID. And since we will then be able to get our hands on
AlbumTrack
objects without starting from an
Album
, the AlbumTrack
model
ought to be enhanced to expose the link back to the ALBUM
table from the ALBUM_TRACKS
table (which used to be hidden in
Hibernate’s composite key). We’d achieve that by adding an album
property. Example 7-12 shows
the key parts of the AlbumTrack
mapping as they
would differ in this approach.
package com.oreilly.hh.data; import java.io.Serializable; import javax.persistence.*;@Entity
@Table(name="ALBUM_TRACKS")
public class AlbumTrack {@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
@ManyToOne
@JoinColumn(name="ALBUM_ID", insertable=false, updatable=false,
nullable=false)
private Album album;
@ManyToOne(cascade=CascadeType.ALL) @JoinColumn(name="TRACK_ID", nullable=false) private Track track; ...public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Album getAlbum() {
return album;
}
public Track getTrack() { ... }
Obviously, we change the class annotation from @Embeddable
to @Entity
, and we choose the table name here
rather than in the Album
source.
Here is where we diverge the most from the schema in the
XML-based examples. Previously our ALBUM_TRACK
table used a composite key,
taking advantage of the fact that there can only ever be one row with
a particular combination of ALBUM_ID
and TRACK_ID
. That saved us from needing a
separate ID column in the ALBUM_TRACK
table. And this composite key
was achieved effortlessly, due to the way we mapped the
association.
While it would be possible to retain that database schema even
after making AlbumTrack
an entity, it would
require significant effort. The JPA requires all
composite keys to be mapped to a separate class that exposes the
components of the key. So, in order to preserve our database schema,
we’d have to make even more radical changes to our model classes,
creating a new class for the sole purpose of holding
AlbumTrack
keys.
Changing the schema slightly instead, by adding an (admittedly,
pretty useless) ID
column to
ALBUM_TRACKS
seems like a less
disruptive option. In any case, this is an interesting illustration of
the tradeoffs one faces when changing mapping approaches.
The mapping back to Album is @ManyToOne
, which we’ve seen before, but we need some extra
parameters to make it work the way we want. This incantation is used
to recreate the situation we achieved with Album.hbm.xml, in which Hibernate is
completely in charge of maintaining the ALBUM_ID
column in the ALBUM_TRACKS
table. Without the insertable
and
updatable
attributes on the
@JoinColumn
annotation, we’d have
to change AlbumTest
to explicitly set the
album property for each
AlbumTrack
object, which means missing out on
some of the automation we want from Hibernate.
This is a pattern you will want to remember when you’re using
indexed mappings to entities (with either @IndexColumn
or @MapKey
).
Now that AlbumTrack
is an entity,
Hibernate can honor the cascade
setting for its track
property.
We can enforce the notion that Hibernate is managing the links
to albums by omitting a setAlbum()
method.
There would be a small difference in the way the relationship is mapped in Album.java, as well, of course. This is shown in Example 7-13.
...@OneToMany(cascade=CascadeType.ALL)
@IndexColumn(name="LIST_POS")@JoinColumn(name="ALBUM_ID", nullable=false)
private List<AlbumTrack> tracks; ...
The cascade
setting in the
@OneToMany
annotation sets up the
same unbroken cascade from Album
through
to its embedded Track
references we had in the
final version of Album.hbm.xml,
developed in Example 5-13, in which albums manage their tracks’ life
cycles. This lets us comment out the track-saving line in the
addAlbumTrack()
method of
AlbumTest
again. So we’ve recreated the old
functionality with a different schema. If we were willing to go to the
trouble of creating a class to manage a composite key, we could keep both
the functionality and database schema intact.
As with many other parts of the Hibernate API (and with object-oriented modeling in general), there are a great many ways to reach your goals.
Hopefully this chapter has given you a feel for how to use annotations to express data mappings, and will serve as a good starting point when you want to explore that option in your own projects. As with the rest of the book, we’ve not tried to list all the details and features available—that’s what the reference documentation is for, although it can seem a bit sparse at times, so hopefully some of the issues we sorted out in this discussion will also serve as examples of how to resolve ambiguity. If all else fails, just keep trying variations and pasting error messages into Google! Or, if you’re a good citizen, delve into the source code and post questions on the Hibernate forum, to lay down a trail of crumbs for future users, and help highlight where the documentation could stand shoring up.
We’ll be going back to the XML-based world for the next few chapters as we explore more ways to query for data. But keep the annotations concepts at the back of your mind—they’ll be put to good use again in the Spring and Stripes chapters at the end of the book.
[5] If you’ve skipped ahead to this chapter, you will probably get more out of it if you learn about the classes and relationships we’re discussing by going back and at least skimming from Chapter 3 onward before proceeding.
[6] The variable builder
is
local to the method, so there’s no way more than one thread can
be messing with it.