Hibernate: A Developer's Notebook
Chapter 2. Introduction to Mapping
Now that we're in a position to work with Hibernate,
it's worth pausing to reflect on why we wanted to in
the first place, lest we remain lost in the details of installation
and configuration. Object-oriented languages like Java
provide a powerful and convenient abstraction for working with
information at runtime in the form of objects that instantiate
classes. These objects can link up with each other in a myriad of
ways, and they can embody rules and behavior as well as the raw data
they represent. But when the program ends, all the objects swiftly
and silently vanish.
Writing a Mapping Document
Hibernate uses an
XML document to track the mapping
between Java classes and relational database tables. This
mapping document is designed to be readable
and hand-editable. You can also start by using graphical CASE tools
(like Together, Rose, or Poseidon) to build UML diagrams representing
your data model, and feed these into AndroMDA
(http://www.andromda.org/),
turning them into Hibernate mappings.
We'll write one by hand, showing
it's quite practical.
We're going to start by writing a mapping document
for tracks, pieces of music that can be
listened to individually or as part of an album or play list. To
begin with, we'll keep track of the
track's title, the path to the file containing the
actual music, its playing time, the date on which it was added to the
database, and the volume at which it should be played (in case the
default volume isn't appropriate because it was
recorded at a very different level than other music in the database).
Why do I care?
You might not have any need for a new system to keep track of your
music, but the concepts and process involved in setting up this
mapping will translate to the projects you actually want to tackle.
How do I do that?
Fire up your favorite
text editor, and create the file
Track.hbm.xml in the
src/com/oreilly/hh directory you set up in the
previous chapter. (If you skipped that chapter,
you'll need to go back and follow it, because this
example relies on the project structure and tools we set up there.)
Type in the mapping document as shown in Example 2-1. Or, if you'd rather avoid
all that typing, download the code examples from this
book's web site, and find the mapping file in the
directory for Chapter 2.
Example 2-1. The mapping document for tracks, Track.hbm.xml
1 <?xml version="1.0"?>
2 <!DOCTYPE hibernate-mapping
3 PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN"
4 "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
5 <hibernate-mapping>
6
7 <class name="com.oreilly.hh.Track" table="TRACK">
8 <meta attribute="class-description">
9 Represents a single playable track in the music database.
10 @author Jim Elliott (with help from Hibernate)
11 </meta>
12
13 <id name="id" type="int" column="TRACK_ID">
14 <meta attribute="scope-set">protected</meta>
15 <generator class="native"/>
16 </id>
17
18 <property name="title" type="string" not-null="true"/>
19
20 <property name="filePath" type="string" not-null="true"/>
21
22 <property name="playTime" type="time">
23 <meta attribute="field-description">Playing time</meta>
24 </property>
25
26 <property name="added" type="date">
27 <meta attribute="field-description">When the track was created</meta>
28 </property>
29
30 <property name="volume" type="short">
31 <meta attribute="field-description">How loud to play the track</meta>
32 </property>
33
34 </class>
35 </hibernate-mapping>
The first four lines are a required preamble to
make this a valid XML document and announce that it conforms to the
document type definition used by Hibernate for mappings. The actual
mappings are inside the
hibernate-mapping tag. Starting at line 7 we're defining a mapping for
a single class, com.oreilly.hh.Track, and the name
and package of this class are related to the name and location of the
file we've created. This relationship
isn't necessary; you can define mappings for any
number of classes in a single mapping document, and name it and
locate it anywhere you want, as long as you tell Hibernate how to
find it. The advantage of following the convention
of naming the mapping file after the
class it maps, and placing it in the same place on the class path as
that class, is that this allows Hibernate to automatically locate the
mapping when you want to work with the class. This simplifies the
configuration and use of Hibernate.
In the opening of the
class tag on line 7,
we have also specified that this class is stored in a database table
named TRACK. The next tag, a
meta tag (lines 8-11), doesn't directly affect the
mapping. Instead, it provides additional information that can be used
by different tools. In this case, by specifying an
attribute value of
"class-description," we are telling
the Java code generation tool the JavaDoc text we want associated
with the Track class. This is entirely optional,
and you'll see the result of including it in Section 2.2.
TIP:
Although databases vary in terms of whether they keep track of the
capitalization of table and column names, this book will use the
convention of referring to these database entities in all-caps, to
help clarify when something being discussed is a database column or
table, as opposed to a persistent Java class or property.
The remainder of the mapping sets up the pieces of information we
want to keep track of, as properties in the class and their
associated columns in the database table. Even though we
didn't mention it in the introduction to this
example, each track is going to need an id.
Following database best practices, we'll use a
meaningless surrogate key (a value with no
semantic meaning, serving only to identify a specific database row).
In Hibernate, the key/id mapping is set up using an
id tag (starting at line 13). We're choosing to use an
int to store our id in the
database column TRACK_ID, which will correspond to
the property id in our Track
object. This mapping contains another meta tag to
communicate with the Java code generator, telling it that the
set method for the id property
should be protected—there's no need for
application code to go changing track IDs.
The generator tag on line 15 configures how Hibernate creates
id values for new instances. (Note that it relates
to normal O/R mapping operation, not to the Java
code generator, which is often not even used;
generator is more fundamental than the optional
meta tags.) There are a number of different ID
generation strategies to choose from, and you can even write your
own. In this case, we're telling Hibernate to use
whatever is most natural for the underlying database
(we'll see later on how it learns what database
we're using). In the case of HSQLDB, an identity
column is used.
NOTE
You may be thinking there's a lot of dense information in this file. That's true, and as you'll see, it can be used to create a bunch of useful project resources.
After the id, we just enumerate the various track properties we care
about. The title (line 18) is a string, and it cannot be null. The
filePath (line 20) has
the same characteristics, while the remainder are allowed to be null:
playTime (line 22) is a
time, added (line 26) is a date, and
volume (line 30) is a
short. These last three properties use a new kind
of meta attribute,
"field-description," which
specifies JavaDoc text for the individual properties, with some
limitations in the current code generator.
What just happened?
We took the abstract description of the information about music
tracks that we wanted to represent in our Java code and database, and
turned it into a rigorous specification in the format that Hibernate
can read. Hopefully you'll agree that
it's a pretty compact and readable representation of
the information. Next we'll look at what Hibernate
can actually do with it.
What about...
. . . Other data types, including nested classes and enumerations?
Relationships between tables? Indices? Class hierarchies and
polymorphism? Tables that contain rows we need to ignore? Hibernate
can handle all these things, and we'll cover most of
them in later examples. Appendix A lists the
basic types that Hibernate supports "out of the
box."
Generating Some Class
Our mapping contains information about both the database and the Java
class between which it maps. We can use it to help us create both.
Let's look at the class first.
How do I do that?
The Hibernate Extensions you installed in Chapter 1 included a tool that can write Java source
matching the specifications in a mapping document, and an Ant task
that makes it easy to invoke from within an Ant build file. Edit
build.xml to add the portions shown in bold in Example 2-2.
Example 2-2. The Ant build file updated for code generation
1 <project name="Harnessing Hibernate: The Developer's Notebook"
2
3 default="db" basedir=".">
4 <!-- Set up properties containing important project directories -->
5 <property name="source.root" value="src"/>
6 <property name="class.root" value="classes"/>
7 <property name="lib.dir" value="lib"/>
8 <property name="data.dir" value="data"/>
9
10 <!-- Set up the class path for compilation and execution -->
11 <path id="project.class.path">
12 <!-- Include our own classes, of course -->
13 <pathelement location="${class.root}" />
14 <!-- Include jars in the project library directory -->
15 <fileset dir="${lib.dir}">
16 <include name="*.jar"/>
17 </fileset>
18 </path>
19
20 <target name="db" description="Runs HSQLDB database management UI
21 against the database file--use when application is not running">
22 <java classname="org.hsqldb.util.DatabaseManager"
23 fork="yes">
24 <classpath refid="project.class.path"/>
25 <arg value="-driver"/>
26 <arg value="org.hsqldb.jdbcDriver"/>
27 <arg value="-url"/>
28 <arg value="jdbc:hsqldb:${data.dir}/music"/>
29 <arg value="-user"/>
30 <arg value="sa"/>
31 </java>
32 </target>
33
34 <!— Teach Ant how to use Hibernate's code generation tool —>
35 <taskdef name="hbm2java"
36 classname="net.sf.hibernate.tool.hbm2java.Hbm2JavaTask"
37 classpathref="project.class.path"/>
38
39 <!— Generate the java code for all mapping files in our source tree —>
40 <target name="codegen"
41 description="Generate Java source from the O/R mapping files">
42 <hbm2java output="${source.root}">
43 <fileset dir="${source.root}">
44 <include name="**/*.hbm.xml"/>
45 </fileset>
46 </hbm2java>
47 </target>
48
49 </project>
We added a taskdef (task definition) and a new
target to the build file. The task definition at line 33 teaches Ant a new trick: it tells Ant how to
use the hbm2java tool that is part of the
Hibernate Extensions, with the help of a class provided for this
purpose. Note that it also specifies the class path to be used when
invoking this tool, using the project.class.path
definition found earlier in the file.
The codegen target
at line 38 uses the new
hbm2java task to run Hibernate's
code generator on any mapping documents found in the
src tree, writing the corresponding Java source.
The pattern
"**/*.hbm.xml"
means "any file ending in
.hbm.xml, within the specified directory, or any
subdirectory, however deeply nested."
Let's try it! From within your top-level project
directory (the folder containing build.xml),
type the following command:
1 ant codegen
You should see output like this:
1 Buildfile: build.xml
2
3 codegen:
4 [hbm2java] Processing 1 files.
5 [hbm2java] Building hibernate objects
6 [hbm2java] log4j:WARN No appenders could be found for logger (net.sf.
7 hibernate.util.DTDEntityResolver).
8 [hbm2java] log4j:WARN Please initialize the log4j system properly.
The warnings are griping about the fact that we
haven't taken the trouble to set up the logging
environment that Hibernate expects. We'll see how to
do that in the next example. For now, if you look in the directory
src/com/oreilly/hh, you'll see
that a new file named Track.java has appeared,
with the content shown in Example 2-3.
Example 2-3. Code generated from the Track mapping document
1 package com.oreilly.hh;
2
3
4 import java.io.Serializable;
5 import java.util.Date;
6 import org.apache.commons.lang.builder.EqualsBuilder;
7 import org.apache.commons.lang.builder.HashCodeBuilder;
8 import org.apache.commons.lang.builder.ToStringBuilder;
9
10 /**
11 * Represents a single playable track in the music database.
12 * @author Jim Elliott (with help from Hibernate)
13 *
14 */
15 public class Track implements Serializable {
16
17 /** identifier field */
18 private Integer id;
19
20 /** persistent field */
21 private String title;
22
23 /** persistent field */
24 private String filePath;
25
26 /** nullable persistent field */
27 private Date playTime;
28
29 /** nullable persistent field */
30 private Date added;
31
32 /** nullable persistent field */
33 private short volume;
34
35 /** full constructor */
36 public Track(String title, String filePath, Date playTime,
37 Date added, short volume) {
38 this.title = title;
39 this.filePath = filePath;
40 this.playTime = playTime;
41 this.added = added;
42 this.volume = volume;
43 }
44
45 /** default constructor */
46 public Track( ) {
47 }
48
49 /** minimal constructor */
50 public Track(String title, String filePath) {
51 this.title = title;
52 this.filePath = filePath;
53 }
54
55 public Integer getId( ) {
56 return this.id;
57 }
58
59 protected void setId(Integer id) {
60 this.id = id;
61 }
62
63 public String getTitle( ) {
64 return this.title;
65 }
66
67 public void setTitle(String title) {
68 this.title = title;
69 }
70
71 public String getFilePath( ) {
72 return this.filePath;
73 }
74
75 public void setFilePath(String filePath) {
76 this.filePath = filePath;
77 }
78
79 /**
80 * Playing time
81 */
82 public Date getPlayTime( ) {
83 return this.playTime;
84 }
85
86 public void setPlayTime(Date playTime) {
87 this.playTime = playTime;
88 }
89
90 /**
91 * When the track was created
92 */
93 public Date getAdded( ) {
94 return this.added;
95 }
96
97 public void setAdded(Date added) {
98 this.added = added;
99 }
100
101 /**
102 * How loud to play the track
103 */
104 public short getVolume( ) {
105 return this.volume;
106 }
107
108 public void setVolume(short volume) {
109 this.volume = volume;
110 }
111
112 public String toString( ) {
113 return new ToStringBuilder(this)
114 .append("id", getId( ))
115 .toString( );
116 }
117
118 public boolean equals(Object other) {
119 if ( !(other instanceof Track) ) return false;
120 Track castOther = (Track) other;
121 return new EqualsBuilder( )
122 .append(this.getId( ), castOther.getId( ))
123 .isEquals( );
124 }
125
126 public int hashCode( ) {
127 return new HashCodeBuilder( )
128 .append(getId( ))
129 .toHashCode( );
130 }
131
132 }
What just happened?
Ant found all files in our source tree ending in
.hbm.xml (just one, so far) and fed it to the
Hibernate code generator, which analyzed it, and wrote a Java class
meeting the specifications we provided for the Track mapping.
NOTE
That can save a lot of time and fairly repetitive activity. I could get used to it.
You may find it worthwhile to compare the generated Java source with
the mapping specification from which it arose (Example 2-1). The source starts out with the proper
package declaration, which is easy for hbm2java
to figure out from the fully qualified class name required in the
mapping file. There are a couple of imports to make the source more
readable. The three potentially unfamiliar entries (lines 5-7) are utilities
from the Jakarta
Commons project that help in the creation of correctly implemented
and useful toString( ),
equals( ), and hashCode( )
methods.
The class-level JavaDoc at line 10 should
look familiar, since it comes right from the
"class-description"
meta tag in our mapping document. The field
declarations are derived from the id (line 17) and
property (lines 20-32) tags defined in
the mapping. The Java types used are derived from the property types
in the mapping document. We'll delve into the full
set of value types supported by Hibernate later on. For now, the
relationship between the types in the mapping document and the Java
types used in the generated code should be fairly clear.
One curious detail is that an Integer wrapper has
been used for id, while volume
is declared as a simple, unwrapped short. Why the
difference? It relates to the fact that the ID/key property has many
important roles to play in the O/R mapping process
(which is why it gets a special XML tag in the mapping document,
rather than being just another property). Although
we left it out in our specification, one of the choices you need to
make when setting up an ID is to pick a special value to indicate
that a particular instance has not yet been saved into the database.
Leaving out this unsaved-value attribute, as we
did, tells Hibernate to use its default interpretation, which is that
unsaved values are indicated by an ID of null.
Since native int values can't be
null, they must be wrapped in a
java.lang.Integer, and Hibernate took care of this
for us.
When it comes to the volume property, Hibernate
has no special need or use for it, so it trusts us to know what
we're doing. If we want to be able to store
null values for volume, perhaps
to indicate "no change," we need to
explicitly use java.lang.Short rather than
short in our mapping document. (Had we not been
sneakily pointing out this difference, our example would be better
off explicitly using java.lang.Integer in our ID
mapping too, just for clarity.)
Another thing you might notice about these field declarations is
that their JavaDoc is quite generic—you may be wondering what
happened to the "field-description"
meta tags we put in the mapping document for
playTime, added and
volume. It turns out they appear only later, in
the JavaDoc for the getter methods. They are not used in the setters,
the actual field declarations, nor as @param
entries for the constructor. As an avid user of a code-completing
Java editor, I count on pop-up JavaDoc as I fill in arguments to
method calls, so I'm a little disappointed by this
limitation. Of course, since this is an open source project, any of
us can get involved and propose or undertake this simple fix. Indeed,
you may find this already remedied by the time you read this book.
Once robust field and parameter documentation is in place,
I'd definitely advocate always providing a brief but
accurate field-description entry for your properties.
NOTE
I know, I'm a perfectionist. I only
bother to pick nits because I think Hibernate is so
useful!
After the field declarations come a trio of
constructors. The first (line 35) establishes values for all properties, the
second (line 44) allows instantiation
without any arguments (this is required if you want the class to be
usable as a bean, such as on a Java Server Page, a very common use
for data classes like this), and the last (line 48) fills in just the values
we've indicated must not be null.
Notice that none of the constructors set the value of
id; this is the responsibility of Hibernate when
we get the object out of the database, or insert it for the first
time.
Consistent with that, the setId( ) method on line
57 is protected, as
requested in our id mapping. The rest of the
getters and setters are not surprising; this is all pretty much
boilerplate code (which we've all written too many
times), which is why it's so nice to be able to have
the Hibernate extensions generate it for us.
WARNING
If you want to use Hibernate's generated code as a
starting point and then add some business logic or other features to
the generated class, be aware that all your changes will be silently
discarded the next time you run the code generator. In such a project
you will want to be sure the hand-tweaked classes are not regenerated
by any Ant build target.
Even though we're having Hibernate generate our data
classes in this example, it's important to point out
that the getters and setters it creates are more than a nice touch.
You need to put these in your persistent classes
for any properties you want to persist, since
Hibernate's fundamental persistence architecture is
based on reflective access to JavaBeans©-style
properties. They don't need to be
public if you don't want them to;
Hibernate has ways of getting at even properties declared
protected or private, but they
do need accessor methods. Think of it as enforcing good object
design; the Hibernate team wants to keep the implementation details
of actual instance variables cleanly separated from the
persistence mechanism.
Cooking Up a Schema
That was pretty easy, wasn't it?
You'll be happy to learn that creating database
tables is a very similar process. As with code generation,
you've already done most of the work in coming up
with the mapping document. All that's left is to set
up and run the schema generation tool.
How do I do that?
The first step
is something we alluded to in Chapter 1. We
need to tell Hibernate the database we're going to
be using, so it knows the specific
"dialect" of SQL to use. SQL is a
standard, yes, but every database goes beyond it in certain
directions and has a specific set of features and limitations that
affect real-life applications. To cope with this reality, Hibernate
provides a set of classes that encapsulate the unique features of
common database environments, in the package
net.sf.hibernate.dialect. You
just need to tell it which one you want to use. (And if you want to
work with a database that isn't yet supported
"out of the box," you can implement
your own dialect.)
In our case, we're working with HSQLDB, so we want
to use HSQLDialect. The easiest way to
configure Hibernate is to create a
properties file named hibernate.properties and
put it at the root level somewhere in the class path. Create this
file at the top level of your src directory, and
put the lines shown in Example 2-4 into it.
NOTE
You can use an XML format for the configuration information as well, but for the simple needs we have here, it doesn't buy you anything.
Example 2-4. Setting up hibernate.properties
1 hibernate.dialect=net.sf.hibernate.dialect.HSQLDialect
2 hibernate.connection.driver_class=org.hsqldb.jdbcDriver
3 hibernate.connection.url=jdbc:hsqldb:data/music
4 hibernate.connection.username=sa
5 hibernate.connection.password=
In addition to establishing the SQL dialect we are using, this tells
Hibernate how to establish
a
connection to the database using the JDBC driver that ships as part
of the HSQLDB database JAR archive, and that the data should live in
the data directory we've
created—in the database named music. The
username and empty password (indeed, all these values) should be
familiar from the experiment we ran at the end of Chapter 1.
TIP:
Notice that we're using a relative path to specify
the database filename. This works fine in our examples —
we're using ant to control the working directory. If
you copy this for use in a web application or other environment,
though, you'll likely need to be more explicit about
the location of the file.
You can put the properties file in other places, and give it other
names, or use entirely different ways of getting the properties into
Hibernate, but this is the default place it will look, so
it's the path of least resistance (or, I guess,
least runtime configuration).
We also need to add some new pieces to our build file,
shown in Example 2-5. This is a somewhat substantial
addition, because we need to compile our Java source in order to use
the schema generation tool, which relies on reflection to get its
details right. Add these targets right before the closing
</project> tag at the end of
build.xml.
Example 2-5. Ant build file additions for compilation and schema generation
1 <!-- Create our runtime subdirectories and copy resources into them -->
2 <target name="prepare" description="Sets up build structures">
3 <mkdir dir="${class.root}"/>
4
5 <!-- Copy our property files and O/R mappings for use at runtime -->
6 <copy todir="${class.root}" >
7 <fileset dir="${source.root}" >
8 <include name="**/*.properties"/>
9 <include name="**/*.hbm.xml"/>
10 </fileset>
11 </copy>
12 </target>
13
14 <!-- Compile the java source of the project -->
15 <target name="compile" depends="prepare"
16 description="Compiles all Java classes">
17 <javac srcdir="${source.root}"
18 destdir="${class.root}"
19 debug="on"
20 optimize="off"
21 deprecation="on">
22 <classpath refid="project.class.path"/>
23 </javac>
24 </target>
25
26 <!-- Generate the schemas for all mapping files in our class tree -->
27 <target name="schema" depends="compile"
28 description="Generate DB schema from the O/R mapping files">
29
30 <!-- Teach Ant how to use Hibernate's schema generation tool -->
31 <taskdef name="schemaexport"
32 classname="net.sf.hibernate.tool.hbm2ddl.SchemaExportTask"
33 classpathref="project.class.path"/>
34
35 <schemaexport properties="${class.root}/hibernate.properties"
36 quiet="no" text="no" drop="no" delimiter=";">
37 <fileset dir="${class.root}">
38 <include name="**/*.hbm.xml"/>
39 </fileset>
40 </schemaexport>
41 </target>
First we add a prepare target that is intended to
be used by other targets more than from the command line. Its purpose
is to create, if necessary, the classes
directory into which we're going to compile, and
then copy any properties and mapping files found in the
src directory hierarchy to corresponding
directories in the classes hierarchy. This
hierarchical copy operation (using the special
"**/*" pattern) is a nice feature
of Ant, enabling us to define and edit resources alongside to the
source files that use them, while making those resources available at
runtime via the class loader.
The aptly named compile target at line 14 uses the built-in java task
to compile all the Java source files found in the
src tree to the classes
tree. Happily, this task also supports the project class path
we've set up, so the compiler can find all the
libraries we're using. The
depends="prepare" attribute in the target
definition tells Ant that before running the
compile target, prepare must be
run. Ant manages dependencies so that when you're
building multiple targets with related dependencies, they are
executed in the right order, and each dependency gets executed only
once, even if it is mentioned by multiple targets.
If you're accustomed to using shell scripts to
compile a lot of Java source, you'll be surprised by
how quickly the compilation happens. Ant invokes the Java compiler
within the same virtual machine that it is using, so there is no
process startup delay for each compilation.
Finally, after all this groundwork, we can write the target we really
wanted to! The schema target (line 26) depends on compile, so all
our Java classes will be compiled and available for inspection when
the schema generator runs. It uses taskdef
internally at line 31 to define the
schemaexport task that runs the Hibernate
schema
export tool, in the same way we provided access to the code
generation tool at the top of the file. It then invokes this tool and
tells it to generate the database schema associated with any mapping
documents found in the classes tree.
There are a number of parameters you can give the schema export tool
to configure the way it works. In this example (at line 35) we're telling it to display
the SQL it runs so we can watch what it's doing
(quiet="no"), to actually interact with the
database and create the schema rather than simply writing out a DDL
file we could import later or simply deleting the schema
(text="no", drop="no"). For
more details about these and other configuration options, consult the
Hibernate reference manual.
TIP:
You may be wondering why the taskdef for the
schema update tool is inside our schema target,
rather than at the top of the build file, next to the one for
hbm2java. Well, I wanted it up there too, but I
ran into a snag that's worth explaining. I got
strange error messages the first time I tried to build the schema
target, complaining there was no
hibernate.properties on the class path and our
compiled Track class couldn't be
found. When I ran it again, it worked. Some detective work using
ant-verbose revealed
that if the classes directory
didn't exist when the taskdef was
encountered, Ant helpfully removed it from the class path. Since a
taskdef can't have its own
dependencies, the solution is to move it into the
schema target, giving it the benefit of that
target's dependencies, ensuring the
classes directory exists by the time the
taskdef is processed.
With these additions, we're ready to generate the
schema for our
TRACK table.
WARNING
You might think the drop="no" setting in our
schematask means you can use
it to update the schema—it won't drop the
tables, right? Alas, this is a misleading parameter name: it means it
won't just drop the tables,
rather it will go ahead and generate the schema
after dropping them. Much as you want to avoid
the codegen task after making any changes to the
generated Java source, you mustn't export the schema
if you've put any data into the database. Luckily,
there is another tool you can use for incremental schema updates that
works much the same way, as long as your JDBC driver is powerful
enough. This SchemaUpdate tool can be used with an
Ant taskdef too.
Because we've asked the schema export task not to be
"quiet," we want it to generate
some log entries for us. In order for that to work, we need to
configure log4j, the logging environment used by
Hibernate. The easiest way to do this is to
make a
log4j.properties file available at the root of
the class path. We can take advantage of our existing
prepare target to copy this from the
src to the classes
directory at the same time it copies Hibernate's
properties. Create a file named log4j.properties
in the src directory with the content shown in
Example 2-6. An easy way to do this is to copy the
file out of the src directory in the Hibernate
distribution you downloaded, since it's provided for
use by their own examples. If you're typing it in
yourself, you can skip the blocks that are commented out; they are
provided to suggest useful logging alternatives.
Example 2-6. The logging configuration file, log4j.properties
1 ### direct log messages to stdout ###
2 log4j.appender.stdout=org.apache.log4j.ConsoleAppender
3 log4j.appender.stdout.Target=System.out
4 log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
5 log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
6
7 ### direct messages to file hibernate.log ###
8 #log4j.appender.file=org.apache.log4j.FileAppender
9 #log4j.appender.file.File=hibernate.log
10 #log4j.appender.file.layout=org.apache.log4j.PatternLayout
11 #log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
12
13 ### set log levels - for more verbose logging change 'info' to 'debug' ###
14
15 log4j.rootLogger=warn, stdout
16
17 log4j.logger.net.sf.hibernate=info
18
19 ### log just the SQL
20 #log4j.logger.net.sf.hibernate.SQL=debug
21
22 ### log JDBC bind parameters ###
23 log4j.logger.net.sf.hibernate.type=info
24
25 ### log schema export/update ###
26 log4j.logger.net.sf.hibernate.tool.hbm2ddl=debug
27
28 ### log cache activity ###
29 #log4j.logger.net.sf.hibernate.cache=debug
30
31 ### enable the following line if you want to track down connection ###
32 ### leakages when using DriverManagerConnectionProvider ###
33 #log4j.logger.net.sf.hibernate.connection.DriverManagerConnectionProvider=trace
TIP:
With the log configuration in place, you might want to edit the
codegen target in build.xml
so that it, too, depends on our new prepare
target. This will ensure logging is configured whenever we use it,
preventing the warnings we saw when first running it. As noted in the
tip about class paths and task definitions in the previous section,
though, to make it work the very first time you'll
have to move the taskdef for
hbm2java inside the codegen
target, in the same way we put schemaexport inside
the schema target.
Time to make a schema! From the project directory, execute the
command antschema.
You'll see output similar to Example 2-7 as the classes directory
is created and populated with resources, the Java source is
compiled,
[1]
and the schema generator is run.
Example 2-7. Output from building the schema using HSQLDB's embedded database server
1 % ant schema
2 Buildfile: build.xml
3
4 prepare:
5 [mkdir] Created dir: /Users/jim/Documents/Work/OReilly/Hibernate/Examples/
6 ch02/classes
7 [copy] Copying 3 files to /Users/jim/Documents/Work/OReilly/Hibernate/
8 Examples/ch02/classes
9
10 compile:
11 [javac] Compiling 1 source file to /Users/jim/Documents/Work/OReilly/Hibernate/Examples/ch02/classes
12
13 schema:
14 [schemaexport] 23:50:36,165 INFO Environment:432 - Hibernate 2.1.1
15 [schemaexport] 23:50:36,202 INFO Environment:466 - loaded properties from
16 resource hibernate.properties: {hibernate.connection.username=sa, hibernate.
17 connection.password=, hibernate.cglib.use_reflection_optimizer=true, hibernate.
18 dialect=net.sf.hibernate.dialect.HSQLDialect, hibernate.connection.url=jdbc:
19 hsqldb:data/music, hibernate.connection.driver_class=org.hsqldb.jdbcDriver}
20 [schemaexport] 23:50:36,310 INFO Environment:481 - using CGLIB reflection
21 optimizer
22 [schemaexport] 23:50:36,384 INFO Configuration:166 - Mapping file: /Users/jim/
23 Documents/Work/OReilly/Hibernate/Examples/ch02/classes/com/oreilly/hh/Track.hbm.
24 xml
25 [schemaexport] 23:50:37,409 INFO Binder:225 - Mapping class: com.oreilly.hh.
26 Track -> TRACK
27 [schemaexport] 23:50:37,928 INFO Dialect:82 - Using dialect: net.sf.hibernate.dialect.HSQLDialect
28 [schemaexport] 23:50:37,942 INFO Configuration:584 - processing one-to-many association mappings
29 [schemaexport] 23:50:37,947 INFO Configuration:593 - processing one-to-one
30 association property references
31 [schemaexport] 23:50:37,956 INFO Configuration:618 - processing foreign key
32 constraints
33 [schemaexport] 23:50:38,113 INFO Configuration:584 - processing one-to-many
34 association mappings
35 [schemaexport] 23:50:38,124 INFO Configuration:593 - processing one-to-one
36 association property references
37 [schemaexport] 23:50:38,132 INFO Configuration:618 - processing foreign key
38 constraints
39 [schemaexport] 23:50:38,149 INFO SchemaExport:98 - Running hbm2ddl schema export
40 [schemaexport] 23:50:38,154 INFO SchemaExport:117 - exporting generated schema
41 to database
42 [schemaexport] 23:50:38,232 INFO DriverManagerConnectionProvider:41 - Using
43 Hibernate built-in connection pool (not for production use!)
44 [schemaexport] 23:50:38,238 INFO DriverManagerConnectionProvider:42 - Hibernate
45 connection pool size: 20
46 [schemaexport] 23:50:38,278 INFO DriverManagerConnectionProvider:71 - using
47 driver: org.hsqldb.jdbcDriver at URL: jdbc:hsqldb:data/music
48 [schemaexport] 23:50:38,283 INFO DriverManagerConnectionProvider:72 - connection
49 properties: {user=sa, password=}
50 [schemaexport] drop table TRACK if exists
51 [schemaexport] 23:50:39,083 DEBUG SchemaExport:132 - drop table TRACK if exists
52 [schemaexport] create table TRACK (
53 [schemaexport] TRACK_ID INTEGER NOT NULL IDENTITY,
54 [schemaexport] title VARCHAR(255) not null,
55 [schemaexport] filePath VARCHAR(255) not null,
56 [schemaexport] playTime TIME,
57 [schemaexport] added DATE,
58 [schemaexport] volume SMALLINT
59 [schemaexport] )
60 [schemaexport] 23:50:39,113 DEBUG SchemaExport:149 - create table TRACK (
61 [schemaexport] TRACK_ID INTEGER NOT NULL IDENTITY,
62 [schemaexport] title VARCHAR(255) not null,
63 [schemaexport] filePath VARCHAR(255) not null,
64 [schemaexport] playTime TIME,
65 [schemaexport] added DATE,
66 [schemaexport] volume SMALLINT
67 [schemaexport] )
68 [schemaexport] 23:50:39,142 INFO SchemaExport:160 - schema export complete
69 [schemaexport] 23:50:39,178 INFO DriverManagerConnectionProvider:137 - cleaning
70 up connection pool: jdbc:hsqldb:data/music
71
72 BUILD SUCCESSFUL
73 Total time: 10 seconds
Toward the end of the schemaexport section you can
see the actual SQL used by Hibernate to create the
TRACK table. If you look at the start of the
music.script file in the
data directory, you'll see
it's been incorporated into the database. For a
slightly more friendly (and perhaps convincing) way to see it,
execute ant db to fire up the HSQLDB graphical
interface, as shown in Figure 2-1.

Figure 2-1. The database interface with our new TRACK table expanded, and a query
What just happened?
We were able to use Hibernate to create a data table in which we can
persist instances of the Java class it created for us. We
didn't have to type a single line of SQL or Java! Of
course, our table is still empty at this point.
Let's change that! The next chapter will look at the
stuff you probably most want to see: using Hibernate from within a
Java program to turn objects into database entries and vice versa.
NOTE
It's about time? Yeah, I suppose. But at least you didn't have to figure out all these steps from scratch!
Before diving into that cool task, it's worth taking
a moment to reflect on how much we've been able to
accomplish with a couple of XML and properties files. Hopefully
you're starting to see the power and convenience
that make Hibernate so exciting.
What about...
. . . Other approaches to ID generation? Keys that are globally
unique across a database or the world? Hibernate can support a
variety of methods for picking the keys for objects it stores in the
database. This is controlled using the generator
tag, line 15 in Example 2-1. In this example we told Hibernate to use the
most natural kind of keys for the type of database that it happens to
be using. Other alternatives include the popular
"hi/lo" algorithm, global UUIDs,
leaving it entirely up to your Java code, and more. See the
"generator" section in the Basic
O/R Mapping chapter of the Hibernate reference documentation for
details. And, as usual, if none of the built-in choices are perfect
for your needs, you can supply your own class to do it exactly how
you'd like, implementing the interface
net.sf.hibernate.id.IdentifierGenerator and supplying your class name in the
generator tag.
Connecting Hibernate to MySQL
If you were skimming through this chapter (or, more likely, the table
of contents) you may not have even noticed that Hibernate connected
to and manipulated a database in the previous section, Section 2.3. Since working with
databases is the whole point of Hibernate, it makes this as easy as
possible. Once you've set up a configuration file
like the one in Example 2-4, the schema generation
tool can get in and work with your database, and your Java code can
use it for persistence sessions as demonstrated in Chapter 3.
NOTE
This example assumes you've already got a working MySQL instance installed and running, since explaining how to do that would be quite a detour.
In the interest of further clarifying this aspect of working with
Hibernate, let's take a look at what
we'd change in that example to set up a connection
with the popular, free, and open source MySQL database (available
fromhttp://www.mysql.com).
How do I do that?
Connect to your MySQL server and set up a new database to play with,
along the lines of Example 2-8.
Example 2-8. Setting up the MySQL database notebook_db as a Hibernate playground
1 % mysql -u root -p
2 Enter password:
3 Welcome to the MySQL monitor. Commands end with ; or \g.
4 Your MySQL connection id is 764 to server version: 3.23.44-Max-log
5
6 Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
7
8 mysql> CREATE DATABASE notebook_db;
9 Query OK, 1 row affected (0.00 sec)
10
11 mysql> GRANT ALL ON notebook_db.* TO jim IDENTIFIED BY "s3cret";
12 Query OK, 0 rows affected (0.20 sec)
13
14 mysql> quit;
15 Bye
Make a note of the database name you create, as well as the username
and password that can access to it. These will need to be entered
into hibernate.properties, as shown in Example 2-9.
NOTE
Hopefully you'll use a less guessable password than this in your real databases!
Next, you'll need a JDBC driver capable of
connecting to MySQL. If you're already using MySQL
for your Java projects, you'll have one. Otherwise,
you can download Connector/J from http://www.mysql.com/downloads/api-jdbc-stable.html.
However you obtain it, copy the driver library jar (which will be
named something like
mysql-connector-java-3.0.10-stable-bin.jar) to
your project's lib directory
alongside the HSQLDB, Hibernate, and other libraries that are already
there. It's fine to have drivers for several
different databases available to your code; they
won't conflict with each other, since the
configuration file specifies which driver class to use.
Speaking of which, it's time to edit
hibernate.properties to use the new driver and
database we've just made available. Example 2-9 shows how it is set up to connect to my MySQL
instance using the database created in Example 2-8.
You'll need to tweak these values to correspond to
your own server, database, and the login credentials you chose. (If
you're using MM.MySQL, the older incarnation of the
MySQL JDBC driver, the driver_class will need to
be com.mysql.jdbc.Driver.)
Example 2-9. Changes to hibernate.properties to connect to the new MySQL database
1 hibernate.dialect=net.sf.hibernate.dialect.MySQLDialect
2 hibernate.connection.driver_class=com.mysql.jdbc.Driver
3 hibernate.connection.url=jdbc:mysql://slant.reseune.pvt/notebook_db
4 hibernate.connection.username=jim
5 hibernate.connection.password=s3cret
The URL on the third line will need to reflect your server; you
won't be able to resolve my private internal domain
name, let alone route to it.
Once this is all set, you can rerun the schema creation example that
was set up in the previous section. This time it will build the
schema on your MySQL server rather than in the embedded HSQLDB world.
You'll see output like that
in
Example 2-10.
Example 2-10. Schema creation when connecting to MySQL
1 % ant schema
2 Buildfile: build.xml
3
4 prepare:
5
6 compile:
7
8 schema:
9 [schemaexport] 23:02:13,614 INFO Environment:462 - Hibernate 2.1.2
10 [schemaexport] 23:02:13,659 INFO Environment:496 - loaded properties from
11 resource hibernate.properties: {hibernate.connection.username=jim, hibernate.
12 connection.password=s3cret, hibernate.cglib.use_reflection_optimizer=true,
13 hibernate.dialect=net.sf.hibernate.dialect.MySQLDialect, hibernate.connection.
14 url=jdbc:mysql://slant.reseune.pvt/notebook_db, hibernate.connection.driver_
15 class=com.mysql.jdbc.Driver}
16 [schemaexport] 23:02:13,711 INFO Environment:519 - using CGLIB reflection
17 optimizer
18 [schemaexport] 23:02:13,819 INFO Configuration:166 - Mapping file: /Users/jim/
19 Documents/Work/OReilly/Hibernate/Examples/ch02/classes/com/oreilly/hh/Track.hbm.xml
20 [schemaexport] 23:02:15,568 INFO Binder:229 - Mapping class: com.oreilly.hh.
21 Track -> TRACK
22 [schemaexport] 23:02:16,164 INFO Dialect:82 - Using dialect: net.sf.hibernate.
23 dialect.MySQLDialect
24 [schemaexport] 23:02:16,175 INFO Configuration:595 - processing one-to-many
25 association mappings
26 [schemaexport] 23:02:16,188 INFO Configuration:604 - processing one-to-one
27 association property references
28 [schemaexport] 23:02:16,209 INFO Configuration:629 - processing foreign key
29 constraints
30 [schemaexport] 23:02:16,429 INFO Configuration:595 - processing one-to-many
31 association mappings
32 [schemaexport] 23:02:16,436 INFO Configuration:604 - processing one-to-one
33 association property references
34 [schemaexport] 23:02:16,440 INFO Configuration:629 - processing foreign key
35 constraints
36 [schemaexport] 23:02:16,470 INFO SchemaExport:98 - Running hbm2ddl schema export
37 [schemaexport] 23:02:16,488 INFO SchemaExport:117 - exporting generated schema
38 to database
39 [schemaexport] 23:02:16,543 INFO DriverManagerConnectionProvider:41 - Using
40 Hibernate built-in connection pool (not for production use!)
41 [schemaexport] 23:02:16,549 INFO DriverManagerConnectionProvider:42 - Hibernate
42 connection pool size: 20
43 [schemaexport] 23:02:16,583 INFO DriverManagerConnectionProvider:71 - using
44 driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://slant.reseune.pvt/notebook_db
45 [schemaexport] 23:02:16,597 INFO DriverManagerConnectionProvider:72 - connection
46 properties: {user=jim, password=s3cret}
47 [schemaexport] drop table if exists TRACK
48 [schemaexport] 23:02:18,129 DEBUG SchemaExport:132 - drop table if exists TRACK
49 [schemaexport] create table TRACK (
50 [schemaexport] TRACK_ID INTEGER NOT NULL AUTO_INCREMENT,
51 [schemaexport] title VARCHAR(255) not null,
52 [schemaexport] filePath VARCHAR(255) not null,
53 [schemaexport] playTime TIME,
54 [schemaexport] added DATE,
55 [schemaexport] volume SMALLINT,
56 [schemaexport] primary key (Track_id)
57 [schemaexport] )
58 [schemaexport] 23:02:18,181 DEBUG SchemaExport:149 - create table TRACK (
59 [schemaexport] TRACK_ID INTEGER NOT NULL AUTO_INCREMENT,
60 [schemaexport] title VARCHAR(255) not null,
61 [schemaexport] filePath VARCHAR(255) not null,
62 [schemaexport] playTime TIME,
63 [schemaexport] added DATE,
64 [schemaexport] volume SMALLINT,
65 [schemaexport] primary key (Track_id)
66 [schemaexport] )
67 [schemaexport] 23:02:18,311 INFO SchemaExport:160 - schema export complete
68 [schemaexport] 23:02:18,374 INFO DriverManagerConnectionProvider:137 - cleaning
69 up connection pool: jdbc:mysql://slant.reseune.pvt/notebook_db
70
71 BUILD SUCCESSFUL
72 Total time: 9 seconds
What just happened?
Hibernate configured itself to work with MySQL's
specific features, examined the mapping document for our
Track class, connected to the MySQL server, and
executed the commands necessary to build a database schema for
persisting Track instances.
It's interesting to compare Example 2-11 with Example 2-7. Most of the
output is the same, but there are subtle differences in the SQL used
to actually create the table. This is what Hibernate means by SQL
"dialects."
Back on the server, you can fire up the MySQL client again, and
confirm that the Track mapping schema has been
created.
Example 2-11. Checking the newly created MySQL schema
1 % mysql -u jim -p
2 Enter password:
3 Welcome to the MySQL monitor. Commands end with ; or \g.
4 Your MySQL connection id is 772 to server version: 3.23.44-Max-log
5
6 Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
7
8 mysql> USE notebook_db
9 Database changed
10 mysql> SHOW TABLES;
11 +-----------------------+
12 | Tables_in_notebook_db |
13 +-----------------------+
14 | TRACK |
15 +-----------------------+
16 1 row in set (0.03 sec)
17
18 mysql> DESCRIBE TRACK;
19 +----------+--------------+------+-----+---------+----------------+
20 | Field | Type | Null | Key | Default | Extra |
21 +----------+--------------+------+-----+---------+----------------+
22 | TRACK_ID | int(11) | | PRI | NULL | auto_increment |
23 | title | varchar(255) | | | | |
24 | filePath | varchar(255) | | | | |
25 | playTime | time | YES | | NULL | |
26 | added | date | YES | | NULL | |
27 | volume | smallint(6) | YES | | NULL | |
28 +----------+--------------+------+-----+---------+----------------+
29 6 rows in set (0.02 sec)
30
31 mysql> SELECT * FROM TRACK;
32 Empty set (0.00 sec)
33
34 mysql> quit;
35 Bye
It's not surprising to find the table empty.
We'll investigate how to populate it with data in
the first part of Chapter 3.
If you've followed this example and set up a MySQL
database, and you'd prefer to continue working with
it throughout the rest of the book, feel free to do so, but bear in
mind you'll need to know how to look at the results
of the examples yourself. The text will assume
you're still working with HSQLDB, and it will show
you how to check your progress in that context. You will also see
slight differences in the schema, as databases all have slightly
different column types and features. Apart from these minor details,
it really makes no difference what database you're
using — that's part of the appeal of an O/R
mapping layer like Hibernate.
If you do want to switch back to HSQLDB for the ease of following the
discussion, change your hibernate.properties
back to the values shown in Example 2-4.
What about...
. . . Connecting to Oracle, or another favorite, shared,
or legacy database that doesn't happen to be MySQL
or HSQLDB? You've probably figured out that
it's just as easy. All you need to do is change the
hibernate.dialect setting in your
hibernate.properties to reflect the kind of
database you want to use. There are many dialects available, covering
every free and commercial database I can think of. These built-in
dialects are listed in Appendix C. If you need
to work with a more obscure database, you may have to write your own
dialect to support it, but that seems unlikely (and check to see if
anyone's already started that effort).
Once you've chosen the dialect,
you'll also need to set the
hibernate.connection properties (driver, URL,
username, and password — the other entries in Example 2-4 and Example 2-9) to the
proper values for establishing a JDBC connection to your chosen
database environment. If you're porting an existing
project to use Hibernate, you'll be able to obtain
these from the code or configuration of that project. And, naturally,
you'll need to put the database's
JDBC driver into your project's library directory.
Of course, if you're connecting to an existing or
shared database, you won't be using Hibernate to
create the schema. Instead, you'll write the mapping
document to reflect the existing schema, either by hand or with the
help of a tool like Middlegen (http://boss.bekk.no/boss/middlegen), and then
start working with the data in the form of persistent objects, as
described in Chapter 3.
You can even use Hibernate to talk to multiple databases at the same
time; you just need to create multiple
SessionFactory instances with separate
configurations. This goes beyond the simple, automatic configuration
we demonstrate in Chapter 3, but there are
examples in the Hibernate reference documentation. Of course, a
persistent object can only be associated with a single session at a
time, which means it can only be linked to a single database at once.
With clever, careful coding, though, you can copy or move objects
between different database systems, even with a different schema to
represent them. That's way out
of scope for this notebook, though!
Footnotes
[1] We're assuming
you've already generated the code shown in Example 2-3, or there won't be any Java
source to compile, and the schema generation will fail. The
schema target doesn't invoke
codegen to automatically generate code, in case
you've manually extended any of your generated
classes.
|