Chapter 7. The Annotations Alternative

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.

Hibernate Annotations

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.

Why do I care?

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.

Note

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.

Tip

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.

How do I do that?

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).

Example 7-1. Obtaining the Hibernate Annotations
  <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.

Example 7-2. Generating the schema using annotations
  <!-- Generate the schemas for annotated classes --> 1
  <target name="schema" depends="compile"
          description="Generate DB schema from the annotated model classes">

    <hibernatetool destdir="${source.root}">
      <classpath refid="project.class.path"/> 2
      <annotationconfiguration
          configurationfile="${source.root}/hibernate.cfg.xml"/> 3
      <hbm2ddl drop="yes"/>
    </hibernatetool>
  </target>
1

The comment and target description needed to be updated to reflect the way things now work.

2

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.

3

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.

Example 7-3. Simpler compilation dependencies
  <!-- 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).

Example 7-4. Configuring Hibernate to work with annotations
...
        
    <!-- 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 the annotated 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>

Why do I have to?

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.

Annotating Model Objects

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!

How do I do that?

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.

Example 7-5. Annotating the Artist class
package com.oreilly.hh.data;

import java.util.*;
import javax.persistence.*; 1
import org.hibernate.annotations.Index;

@Entity 2
@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 3
  @Column(name="ARTIST_ID")
  @GeneratedValue(strategy=GenerationType.AUTO)
  private Integer id;
    
  @Column(name="NAME",nullable=false,unique=true) 4
  @Index(name="ARTIST_NAME",columnNames={"NAME"}) 5
  private String name;
      
  @ManyToMany 6
  @JoinTable(name="TRACK_ARTISTS", 
             joinColumns={@JoinColumn(name="TRACK_ID")},
             inverseJoinColumns={@JoinColumn(name="ARTIST_ID")})
  private Set<Track> tracks;
        
  @ManyToOne 7
  @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();                                              
  }                                                                           

}
1

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.

Note

This query language relationship shows Hibernate really did influence the direction of EJB 3 in a deep way.

2

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.

3

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).

4

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.

5

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.

6

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.

7

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.

Annotating Track

The annotated Artist class references the Track class. Example 7-6 shows the annotations on Track, which introduce some new issues.

Example 7-6. Annotating the Track class
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) 1
  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) 2
  private Date added;
        
  @CollectionOfElements 3
  @JoinTable(name="TRACK_COMMENTS",
             joinColumns = @JoinColumn(name="TRACK_ID"))
  @Column(name="COMMENT")
  private Set<String> comments;

  @Enumerated(EnumType.STRING) 4
  private SourceMedia sourceMedia;
        
  @Embedded 5
  @AttributeOverrides({ 6
    @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() { 7
    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();
  }
}
1

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.

2

The added property, though it shares the same Date type with playTime, corresponds to a DATE column.

3

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.

4

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!

5

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.)

6

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.

7

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.

Annotating Album

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.

Example 7-7. Annotating the Album class
package com.oreilly.hh.data;

import java.util.*;
import javax.persistence.*;
import org.hibernate.annotations.CollectionOfElements; 1
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 2
  @IndexColumn(name="LIST_POS") 3
  @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();
  }
}
1

Yup, we keep being unable to do without Hibernate-specific extensions. No fewer than three in this class.

2

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.

3

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 Lists 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.

Example 7-8. Annotating the AlbumTrack class
package com.oreilly.hh.data;

import java.io.Serializable;
import javax.persistence.*;

@Embeddable 1
public class AlbumTrack {

    @ManyToOne(cascade=CascadeType.ALL) 2
    @JoinColumn(name="TRACK_ID", nullable=false)
    private Track track;
    
    private Integer disc; 3
    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();
    }
}
1

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.

2

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.

3

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.

Does it work?

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.

Example 7-9. Highlights from creating the schema using annotated classes

% ant schema
Buildfile: build.xml
Downloading: org/hibernate/hibernate-annotations/3.3.0.ga/hibernate-annotations-
3.3.0.ga.pom 1
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 2
[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)); 3
[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>. 4
[hibernatetool] Error #1: java.sql.SQLException: Table not found: ALBUM_ARTISTS 
in statement [alter table ALBUM_ARTISTS]
...

BUILD SUCCESSFUL
Total time: 5 seconds

1

Since this is the first time we’ve asked the Maven Ant Tools for the Hibernate Annotations, they are downloaded along with their dependencies.

2

Here’s where you can see that annotations are being used to drive the creation of the schema.

3

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.

4

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.

Example 7-10. Tweaks to the test classes needed to work with annotations
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 on the annotations in our
    // model classes.
    Configuration config = new AnnotationConfiguration();
    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.

Example 7-11. Running AlbumTest using annotations
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.

Note

It worked! It all worked!

An Alternate Approach

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.

Example 7-12. Annotating the AlbumTrack class as an entity
package com.oreilly.hh.data;

import java.io.Serializable;
import javax.persistence.*;
@Entity 1
@Table(name="ALBUM_TRACKS")
public class AlbumTrack {

    @Id 2
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Integer id;

    @ManyToOne
    @JoinColumn(name="ALBUM_ID", insertable=false, updatable=false, 3
                nullable=false)
    private Album album;
    
    @ManyToOne(cascade=CascadeType.ALL) 4
    @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;
    }
      5
    public Track getTrack() {
...
}
1

Obviously, we change the class annotation from @Embeddable to @Entity, and we choose the table name here rather than in the Album source.

2

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.

3

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).

4

Now that AlbumTrack is an entity, Hibernate can honor the cascade setting for its track property.

5

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.

Example 7-13. The AlbumTracks entity mapping in Album.java
...
    @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.

What now?

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset