All right, we’ve set up a whole bunch of infrastructure, defined an object/relational mapping, and used it to create a matching Java class and database table. But what does that buy us? It’s time to see how easy it is to work with persistent data from your Java code.
Before we can continue working with Hibernate, we need to get some busy work out of the way. In the previous chapter, we configured Hibernate’s JDBC connection using a hibernate.properties file in the src directory. In this chapter we introduce a way to configure the JDBC connection, SQL dialect, and much more, using a Hibernate XML Configuration file. Just like the hibernate.properties file, we’ll place this file in the src directory. Enter the content shown in Example 3-1 into a file called hibernate.cfg.xml within src, and delete the hibernate.properties file.
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.HSQLDialect</property> <!-- Database connection settings --> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.url">jdbc:hsqldb:data/music</property> <property name="connection.username">sa</property> <property name="connection.password"></property> <property name="connection.shutdown">true</property> <!-- JDBC connection pool (use the built-in one) --> <property name="connection.pool_size">1</property> <!-- Enable Hibernate's automatic session context management --> <property name="current_session_context_class">thread</property> <!-- Disable the second-level cache --> <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property> <!-- disable batching so HSQLDB will propagate errors correctly. --> <property name="jdbc.batch_size">0</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- List all the mapping documents we're using --> <mapping resource="com/oreilly/hh/data/Track.hbm.xml"/> </session-factory> </hibernate-configuration>
As you can see, hibernate.cfg.xml configures the dialect, the JDBC parameters, the connection pool, and the cache provider. This XML document also references the mapping document we wrote in the previous chapter, eliminating the need to reference the mapping documents from within our Java source. Let’s go through the details of this configuration file one section at a time:
Just like the hibernate.properties file from the
previous chapter, we’re defining the dialect needed to work with
HSQLDB. You might notice that the property
element’s name
attribute is dialect,
which is similar to the name of
the property in the properties file,
hibernate.dialect
. When you configure Hibernate
using the XML configuration file, you are passing the same properties
to Hibernate. In the XML you can omit the hibernate.
prefix from the name of the
property. This section (dialect) and the next section (connection) are
the same set of properties that were configured by the hibernate.properties file we used in Chapter 2.
The properties used to configure the JDBC connection
from Hibernate—connection.driver_class
, connection.url
, connection
.
username
, connection.
password
, and connection.
shutdown
—match
the properties set in hibernate.properties in Example 2-4.
The connection.pool_size
property is set to a value of 1
. This means that Hibernate will create a
JDBC Connection pool with only one connection.
Connection pooling is important in larger applications that need to
scale, but for the purposes of this book, we can safely configure
Hibernate to use the built-in connection pool with a single
JDBC connection. Hibernate allows for a great deal
of flexibility when it comes to connection pool implementations; it is
very easy to configure Hibernate to use other connection pool
implementations such as Apache Commons DBCP and
C3P0.
As things stand, when we start telling Hibernate to perform actual persistence operations for us, it is going to warn us that we haven’t properly configured its second-level caching systems. For a simple application like this, we don’t need any at all; this line turns off the second-level caching and sends every operation to the database.
Here we are turning off Hibernate’s JDBC
batching feature. This reduces efficiency slightly—although far less so for an
in-memory database like HSQLDB—but is necessary for
usable error reporting in current HSQLDB releases.
With batching turned on, if any statement in the batch has a problem,
the only exception you get from HSQLDB is a
BatchUpdateException
telling you that the batch
failed. This makes it almost impossible to debug your program. The
author of HSQLDB reports that this problem will be
fixed in the next major release; until then, we have to live without
batching when using HSQLDB, for our sanity’s
sake.
The show_sql
property is
a useful property to set when developing and debugging a
program that uses Hibernate. Setting show_sql
to true
tells Hibernate to print out every
statement it executes against a database. Set this property to
false
if you do not want to see
the SQL statements printed out to the console.
This final section lists all of the mapping documents in our
project. Note that the path contains forward slashes and is relative
to the src
directory. This path addresses the .hbm.xml files on the class path as
resources. By listing these files here, we no longer need to
explicitly find them within build.xml targets that manipulate mapped
classes (as you’ll see later), nor load them from within each
main()
method in our example source
code, as we did in the previous version of the book. We think you’ll
agree it’s nicer to keep this all in one place.
In addition to the hibernate.cfg.xml file in the src directory, you will also
need to alter your build.xml to
reference the XML configuration file. Change
the configuration
elements
within the codegen
and schema
targets to look like the boldfaced lines
in Example 3-2.
... <!-- Generate the java code for all mapping files in our source tree --> <target name="codegen" depends="prepare" description="Generate Java source from the O/R mapping files"> <hibernatetool destdir="${source.root}"><configuration configurationfile="${source.root}/hibernate.cfg.xml"/>
<hbm2java/> </hibernatetool> </target> ... <!-- Generate the schemas for all mapping files in our class tree --> <target name="schema" depends="prepare" description="Generate DB schema from the O/R mapping files"> <hibernatetool destdir="${source.root}"><configuration configurationfile="${source.root}/hibernate.cfg.xml"/>
<hbm2ddl drop="yes" /> </hibernatetool> </target> ...
These two lines tell the Hibernate Tools Ant tasks where to look for
the Hibernate XML configuration, and they find all the
information they need within that configuration (including the mapping
documents we’re working with; recall that in Chapter 2 we had to
explicitly build Ant fileset
elements
inside our configuration
elements to
match all mapping files found within the project source tree). We will be
using the Hibernate XML configuration throughout the
remainder of this book, which makes it easier to pass all kinds of
information to all the Hibernate-related tools.
Now that we’ve successfully configured Hibernate, let’s return to our main task for the chapter: making some persistent objects.
Let’s start by creating some new
Track
instances and persisting them to the database, so we can
see how they turn into rows and columns for us. Because of the way we’ve
organized our mapping document and configuration file, it’s extremely easy
to configure the Hibernate session factory and get things rolling.
This discussion assumes you’ve created the schema and generated Java code by following the preceding examples. If you haven’t, you can start by downloading the examples archive from this book’s web site, jumping into the ch03 directory, and using the commands ant prepare and ant codegen[1] followed by ant schema to automatically fetch the Hibernate and HSQLDB libraries and set up the generated Java code and database schema on which this example is based. (As with the other examples, these commands should be issued in a shell whose current working directory is the top of your project tree, containing Ant’s build.xml file.)
We’ll start with a simple example class,
CreateTest
, containing the necessary imports and
housekeeping code to bring up the Hibernate environment and create some
Track
instances that can be persisted using the
XML mapping document with which we started. Type the source of Example 3-3 in the directory
src/com/oreilly/hh.
package com.oreilly.hh; import org.hibernate.*; import org.hibernate.cfg.Configuration; import com.oreilly.hh.data.*; import java.sql.Time; import java.util.Date; /** * Create sample data, letting Hibernate persist it for us. */ public class CreateTest { public static void main(String args[]) throws Exception { // Create a configuration based on the XML file we've put // in the standard place. Configuration config = new Configuration(); config.configure(); // Get the session factory we can use for persistence SessionFactory sessionFactory = config.buildSessionFactory(); // Ask for a session using the JDBC information we've configured Session session = sessionFactory.openSession(); Transaction tx = null; try { // Create some data and persist it tx = session.beginTransaction(); Track track = new Track("Russian Trance", "vol2/album610/track02.mp3", Time.valueOf("00:03:30"), new Date(), (short)0); session.save(track); track = new Track("Video Killed the Radio Star", "vol2/album611/track12.mp3", Time.valueOf("00:03:49"), new Date(), (short)0); session.save(track); track = new Track("Gravity's Angel", "vol2/album175/track03.mp3", Time.valueOf("00:06:06"), new Date(), (short)0); session.save(track); // We're done; make our changes permanent tx.commit(); } catch (Exception e) { if (tx != null) { // Something went wrong; discard all partial changes tx.rollback(); } throw new Exception("Transaction failed", e); } finally { // No matter what, close the session session.close(); } // Clean up after ourselves sessionFactory.close(); } }
The first part of CreateTest.java needs a little explanation:
We import some useful Hibernate classes, including Configuration
, which is used
to set up the Hibernate environment. We also want to import any and
all data classes that Hibernate has generated for us based on our
mapping documents; these will all be found in our
data package. The Time
and
Date
classes are used in our data objects to
represent track playing times and creation timestamps. The only
method we implement in CreateTest
is the
main()
method that supports invocation
from the command line.
When this class is run, it starts by creating a Hibernate
Configuration
object. Since we don’t tell it
otherwise, Hibernate looks for a file named hibernate.cfg.xml at
the root level in the classpath. It finds the one we created
earlier, which tells it we’re using HSQLDB and how to find the
database. This XML configuration file also references the Hibernate
Mapping XML document for the Track
object.
Calling config.configure()
automatically adds the
mapping for the Track
class.
That’s all the configuration we need in order to create
and persist track data, so we’re ready to create the
SessionFactory
. Its purpose is to provide us
with Session
objects,
the main avenue for interaction with Hibernate. The SessionFactory
is thread-safe, and
you need only one for your entire application. (To be more precise,
you need one for each database environment for which you want
persistence services; most applications therefore need only one.)
Creating the session factory is a pretty expensive and slow
operation, so you’ll definitely want to share it throughout your
application. It’s trivial in a one-class application like this one,
but the reference documentation provides some good examples of ways
to do it in more realistic scenarios.
It’s worth getting a solid understanding of the purposes and lifecycles of these objects. This book gives you just enough information to get started; you’ll want to spend some time with the reference documentation and understand the examples in depth.
When it comes time to actually perform persistence, we
ask the SessionFactory
to open a
Session
for us, which establishes a
JDBC connection to the database and provides us
with a context in which we can create, obtain, manipulate, and
delete persistent objects. As long as the session is open, a
connection to the database is maintained, and changes to the
persistent objects associated with the session are tracked so they
can be applied to the database when the session is closed.
Conceptually, you can think of a session as a “large-scale
transaction” between the persistent objects and the database, which
may encompass several database-level transactions. As with a
database transaction, though, you should not think about keeping
your Hibernate session open over long periods of application
existence (such as while you’re waiting for user input). A single
session is used for a specific and bounded operation in the
application, something like populating the user interface or making
a change that has been committed by the user. The next operation
will use a new session. Also note that
Session
objects themselves are not
thread-safe, so they cannot be shared between threads. Each thread
needs to obtain its own session from the factory.
It’s worth going into a bit more depth about the lifecycle
of mapped objects in Hibernate and how this relates to
sessions because the terminology is rather specific and the concepts
are quite important. A mapped object, such as an instance of our
Track
class, moves back and forth between
two states with respect to Hibernate:
transient and
persistent. An object that is transient is
not associated with any session. When you first create a
Track
instance using
new()
, it is transient; unless you
tell Hibernate to persist it, the object will vanish when your
application terminates.
Passing a transient mapped object to a Session
’s
save()
method causes it to become
persistent. It will survive past its scope in the Java
VM, until it is explicitly deleted later. If
you’ve got a persistent object and you call Session
’s
delete()
method on it, the object
transitions back to transient state. The object still exists as an
instance in your application, but it is no longer going to stay
around unless you change your mind and save it again. On the other
hand, if you haven’t deleted it (so it’s still persistent) and you
make changes to the object, there’s no need to save it again in
order for those changes to be reflected. Hibernate automatically
tracks changes to any persistent objects and flushes those changes
to the database at appropriate times. When you close the session,
any pending changes are flushed.
Hang in there, we’ll be back to the example soon!
An important but subtle point concerns the status of persistent objects with which you worked in a session that has been closed, such as after you run a query to find all entities matching some criteria (you’ll see how to do this in “Finding Persistent Objects” later in this chapter). As noted earlier, you don’t want to keep this session around longer than necessary to perform the database operation, so you close it once your queries are finished. What’s the deal with the mapped objects you’ve loaded at this point? Well, they were persistent while the session was around, but once they are no longer associated with an active session (in this case, because the session has been closed), they are not persistent any longer. Now, this doesn’t mean that they no longer exist in the database; indeed, if you run the query again (assuming nobody has changed the data in the meantime), you’ll get back the same set of objects. It simply means that there is not currently an active correspondence being maintained between the state of the objects in your virtual machine and the database; they are detached. It is perfectly reasonable to carry on working with the objects. If you later need to make changes to the objects and you want the changes to stick, you will open a new session and use it to save the changed objects. Because each entity has a unique ID, Hibernate has no problem figuring out how to link the transient objects back to the appropriate persistent state in the new session.
Of course, as with any environment in which you’re making changes to an offline copy of information backed by a database, you need to think about application-level data-integrity constraints. You may need to devise some higher-level locking or versioning protocol to support them. Hibernate can offer help with this task, too, but the design and detailed implementation is up to you. The reference manual does strongly recommend the use of a version field, and there are several approaches available.
Armed with these concepts and terms, the remainder of the
example is easy enough to understand. We set up a database transaction using our open session. Within that, we
create a few Track
instances containing
sample data and save them in the session, turning them from
transient instances into persistent entities.
Finally, we commit our transaction, atomically (as a
single, indivisible unit) making all the database changes
permanent. The
try
/catch
/finally
block
wrapped around all this shows an important and useful idiom for
working with transactions. If anything goes wrong, the catch
block will roll back the transaction
and then bubble out the exception, leaving the database the way we
found it. The session is closed in the finally
portion,
ensuring that this takes place whether we exit through the “happy
path” of a successful commit or via an exception that caused
rollback. Either way, it gets closed as it should.
At the end of our method we also close the session
factory itself. This is something you’d do in the “graceful
shutdown” section of your application. In a web application
environment, it would be in the appropriate lifecycle event handler.
In this simple example, when the
main()
method returns, the application
is ending.
With all we’ve got in place, by now it’s quite easy to tell Ant
how to compile and run this test. Add the targets shown in Example 3-4 right before the
closing </project>
tag at the
end of build.xml.
<!-- 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> <target name="ctest" description="Creates and persists some sample data" depends="compile"> <java classname="com.oreilly.hh.CreateTest" fork="true"> <classpath refid="project.class.path"/> </java> </target>
The aptly named compile
target uses the built-in javac
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.
The ctest
target
uses compile
to make
sure the class files are built and then creates a new Java virtual
machine to run our CreateTest
class.
All right, we’re ready to create some data! Example 3-5 shows the results
of invoking the new ctest
target. Its
dependency on the compile
target
ensures the CreateTest
class gets compiled before
we try to use it. The output for ctest
itself shows the logging emitted by
Hibernate as the environment and mappings are set up and the connection
is shut back down.
%
ant ctest
prepare:
compile:
[javac] Compiling 2 source files to /Users/jim/svn/oreilly/hib_dev_2e/curren
t/examples/ch03/classes
ctest:
[java] 00:21:45,833 INFO Environment:514 - Hibernate 3.2.5
[java] 00:21:45,852 INFO Environment:547 - hibernate.properties not found
[java] 00:21:45,864 INFO Environment:681 - Bytecode provider name : cglib
[java] 00:21:45,875 INFO Environment:598 - using JDK 1.4 java.sql.Timestam
p handling
[java] 00:21:46,032 INFO Configuration:1426 - configuring from resource: /
hibernate.cfg.xml
[java] 00:21:46,034 INFO Configuration:1403 - Configuration resource: /hib
ernate.cfg.xml
[java] 00:21:46,302 INFO Configuration:553 - Reading mappings from resourc
e : com/oreilly/hh/data/Track.hbm.xml
[java] 00:21:46,605 INFO HbmBinder:300 - Mapping class: com.oreilly.hh.dat
a.Track -> TRACK
[java] 00:21:46,678 INFO Configuration:1541 - Configured SessionFactory: n
ull
[java] 00:21:46,860 INFO DriverManagerConnectionProvider:41 - Using Hibern
ate built-in connection pool (not for production use!)
[java] 00:21:46,862 INFO DriverManagerConnectionProvider:42 - Hibernate co
nnection pool size: 1
[java] 00:21:46,864 INFO DriverManagerConnectionProvider:45 - autocommit m
ode: false
[java] 00:21:46,879 INFO DriverManagerConnectionProvider:80 - using driver
: org.hsqldb.jdbcDriver at URL: jdbc:hsqldb:data/music
[java] 00:21:46,891 INFO DriverManagerConnectionProvider:86 - connection p
roperties: {user=sa, password=****, shutdown=true}
[java] 00:21:47,533 INFO SettingsFactory:89 - RDBMS: HSQL Database Engine,
version: 1.8.0
[java] 00:21:47,538 INFO SettingsFactory:90 - JDBC driver: HSQL Database E
ngine Driver, version: 1.8.0
[java] 00:21:47,613 INFO Dialect:152 - Using dialect: org.hibernate.dialec
t.HSQLDialect
[java] 00:21:47,638 INFO TransactionFactoryFactory:31 - Using default tran
saction strategy (direct JDBC transactions)
[java] 00:21:47,646 INFO TransactionManagerLookupFactory:33 - No Transacti
onManagerLookup configured (in JTA environment, use of read-write or transaction
al second-level cache is not recommended)
[java] 00:21:47,649 INFO SettingsFactory:143 - Automatic flush during befo
reCompletion(): disabled
[java] 00:21:47,650 INFO SettingsFactory:147 - Automatic session close at
end of transaction: disabled
[java] 00:21:47,657 INFO SettingsFactory:154 - JDBC batch size: 15
[java] 00:21:47,659 INFO SettingsFactory:157 - JDBC batch updates for vers
ioned data: disabled
[java] 00:21:47,664 INFO SettingsFactory:162 - Scrollable result sets: ena
bled
[java] 00:21:47,666 INFO SettingsFactory:170 - JDBC3 getGeneratedKeys(): d
isabled
[java] 00:21:47,668 INFO SettingsFactory:178 - Connection release mode: au
to
[java] 00:21:47,671 INFO SettingsFactory:205 - Default batch fetch size: 1
[java] 00:21:47,678 INFO SettingsFactory:209 - Generate SQL with comments:
disabled
[java] 00:21:47,680 INFO SettingsFactory:213 - Order SQL updates by primar
y key: disabled
[java] 00:21:47,681 INFO SettingsFactory:217 - Order SQL inserts for batch
ing: disabled
[java] 00:21:47,684 INFO SettingsFactory:386 - Query translator: org.hiber
nate.hql.ast.ASTQueryTranslatorFactory
[java] 00:21:47,690 INFO ASTQueryTranslatorFactory:24 - Using ASTQueryTran
slatorFactory
[java] 00:21:47,694 INFO SettingsFactory:225 - Query language substitution
s: {}
[java] 00:21:47,695 INFO SettingsFactory:230 - JPA-QL strict compliance: d
isabled
[java] 00:21:47,702 INFO SettingsFactory:235 - Second-level cache: enabled
[java] 00:21:47,704 INFO SettingsFactory:239 - Query cache: disabled
[java] 00:21:47,706 INFO SettingsFactory:373 - Cache provider: org.hiberna
te.cache.NoCacheProvider
[java] 00:21:47,707 INFO SettingsFactory:254 - Optimize cache for minimal
puts: disabled
[java] 00:21:47,709 INFO SettingsFactory:263 - Structured second-level cac
he entries: disabled
[java] 00:21:47,724 INFO SettingsFactory:283 - Echoing all SQL to stdout
[java] 00:21:47,731 INFO SettingsFactory:290 - Statistics: disabled
[java] 00:21:47,732 INFO SettingsFactory:294 - Deleted entity synthetic id
entifier rollback: disabled
[java] 00:21:47,734 INFO SettingsFactory:309 - Default entity-mode: pojo
[java] 00:21:47,735 INFO SettingsFactory:313 - Named query checking : enab
led
[java] 00:21:47,838 INFO SessionFactoryImpl:161 - building session factory
[java] 00:21:48,464 INFO SessionFactoryObjectFactory:82 - Not binding fact
ory to JNDI, no JNDI name configured
[java] Hibernate: insert into TRACK (TRACK_ID, title, filePath, playTime, a
dded, volume) values (null, ?, ?, ?, ?, ?)
[java] Hibernate: call identity()
[java] Hibernate: insert into TRACK (TRACK_ID, title, filePath, playTime, a
dded, volume) values (null, ?, ?, ?, ?, ?)
[java] Hibernate: call identity()
[java] Hibernate: insert into TRACK (TRACK_ID, title, filePath, playTime, a
dded, volume) values (null, ?, ?, ?, ?, ?)
[java] Hibernate: call identity()
[java] 00:21:49,365 INFO SessionFactoryImpl:769 - closing
[java] 00:21:49,369 INFO DriverManagerConnectionProvider:147 - cleaning up
connection pool: jdbc:hsqldb:data/music
BUILD SUCCESSFUL
Total time: 2 seconds
If you scan through all the messages Hibernate prints out because
we’ve turned on “info” logging, you can see that our test class fired up
Hibernate, loaded the mapping information for the
Track
class, opened a persistence session to the
associated HSQLDB database, and used that to create some instances and
persist them in the TRACK
table. Then
it shut down the session and closed the database connection, ensuring
the data was saved.
After running this test, you can use ant
db to take a look at the contents of the database. You should
find three rows in the TRACK
table
now, as shown in Figure 3-1. (Type your query in the text box at the
top of the window and click the button. You can get a command
skeleton and syntax documentation by choosing → in the menu bar.)
At this point, it’s worth pausing a moment to reflect on the fact
that we wrote no code to connect to the database or issue
SQL commands. Looking back to the preceding sections,
we didn’t even have to create the table ourselves, nor the
Track
object that encapsulates our data. Yet the
query output in Figure 3-1 shows nicely readable data representing the
Java objects created and persisted by our short, simple test program.
Hopefully you’ll agree that this reflects very well on the power and
convenience of Hibernate as a persistence service. For being free and
lightweight, Hibernate can certainly do a lot for you, quickly and
easily.
If you have been using JDBC directly, especially if you’re pretty new to it, you may be used to relying on the “auto-commit” mode in the database driver rather than always using database transactions. Hibernate is rightly opinionated that this is the wrong way to structure application code; the only place it makes sense is in database console experimentation by humans. So, you’ll always need to use transactions around your persistence operations in Hibernate.
As noted in Chapter 1, you can also look directly at the SQL statements creating your data in the music.script file in the data directory as shown in Example 3-6.
%
cat data/music.script
CREATE SCHEMA PUBLIC AUTHORIZATION DBA
CREATE MEMORY TABLE TRACK(TRACK_ID INTEGER GENERATED BY DEFAULT AS IDENTITY(STAR
T WITH 1) NOT NULL PRIMARY KEY,TITLE VARCHAR(255) NOT NULL,FILEPATH VARCHAR(255)
NOT NULL,PLAYTIME TIME,ADDED DATE,VOLUME SMALLINT NOT NULL)
ALTER TABLE TRACK ALTER COLUMN TRACK_ID RESTART WITH 4
CREATE USER SA PASSWORD ""
GRANT DBA TO SA
SET WRITE_DELAY 10
SET SCHEMA PUBLIC
INSERT INTO TRACK VALUES(1,'Russian Trance','vol2/album610/track02.mp3','00:03:3
0','2007-06-17',0)
INSERT INTO TRACK VALUES(2,'Video Killed the Radio Star','vol2/album611/track12.
mp3','00:03:49','2007-06-17',0)
INSERT INTO TRACK VALUES(3,'Gravity''s Angel','vol2/album175/track03.mp3','00:06
:06','2007-06-17',0)
The final three statements show our TRACK
table rows. The top contains the schema
and the user that gets provided by default when creating a new database.
(Of course, in a real application environment, you’d want to change
these credentials, unless you were only enabling in-memory
access.)
Tempted to learn more about HSQLDB? We won’t try to stop you!
…objects with relationships to other objects? Collections of objects? You’re right—these are cases where persistence gets more challenging (and, if done right, valuable). Hibernate can handle associations like this just fine. In fact, there isn’t any special effort involved on our part. We’ll discuss this in Chapter 4. For now, let’s look at how to retrieve objects that were persisted in earlier sessions.
It’s time to throw the giant lever into reverse and look at how you load data from a database into Java objects.
Let’s see how it works, using Hibernate Query Language to get an object-oriented view of the contents of mapped database tables. These might have started out as objects persisted in a previous session or might be data that came from completely outside your application code.
Example 3-7 shows a program that runs a simple query using the test data we just created. The overall structure will look very familiar because all the Hibernate setup is the same as in the previous program.
package com.oreilly.hh; import org.hibernate.*; import org.hibernate.cfg.Configuration; import com.oreilly.hh.data.*; import java.sql.Time; import java.util.*; /** * Retrieve data as objects */ public class QueryTest { /** * Retrieve any tracks that fit in the specified amount of time. * * @param length the maximum playing time for tracks to be returned. * @param session the Hibernate session that can retrieve data. * @return a list of {@link Track}s meeting the length restriction. */ public static List tracksNoLongerThan(Time length, Session session) { Query query = session.createQuery("from Track as track " + "where track.playTime <= ?"); query.setParameter(0, length, Hibernate.TIME); return query.list(); } /** * Look up and print some tracks when invoked from the command line. */ public static void main(String args[]) throws Exception { // Create a configuration based on the properties file we've put // in the standard place. Configuration config = new Configuration(); config.configure(); // Get the session factory we can use for persistence SessionFactory sessionFactory = config.buildSessionFactory(); // Ask for a session using the JDBC information we've configured Session session = sessionFactory.openSession(); try { // Print the tracks that will fit in five minutes List tracks = tracksNoLongerThan(Time.valueOf("00:05:00"), session); for (ListIterator iter = tracks.listIterator() ; iter.hasNext() ; ) { Track aTrack = (Track)iter.next(); System.out.println("Track: "" + aTrack.getTitle() + "", " + aTrack.getPlayTime()); } } finally { // No matter what, close the session session.close(); } // Clean up after ourselves sessionFactory.close(); } }
Once again, we add a target, shown in Example 3-8, at the end of build.xml (right before the closing project
tag) to run this test.
<target name="qtest" description="Run a simple Hibernate query" depends="compile"> <java classname="com.oreilly.hh.QueryTest" fork="true"> <classpath refid="project.class.path"/> </java> </target>
With this in place, we can simply type ant qtest to retrieve and display some data, with the results shown in Example 3-9. To save space in the output, we’ve edited log4j.properties to turn off all the “info” messages, since they’re no different than in the previous example. You can do this yourself by changing the line:
log4j.logger.org.hibernate=info
to replace the word info
with
warn
:
log4j.logger.org.hibernate=warn
%
ant qtest
Buildfile: build.xml
prepare:
[copy] Copying 1 file to /Users/jim/svn/oreilly/hib_dev_2e/current/examples
/ch03/classes
compile:
[javac] Compiling 1 source file to /Users/jim/svn/oreilly/hib_dev_2e/current
/examples/ch03/classes
qtest:
[java] Hibernate: select track0_.TRACK_ID as TRACK1_0_, track0_.title as ti
tle0_, track0_.filePath as filePath0_, track0_.playTime as playTime0_, track0_.a
dded as added0_, track0_.volume as volume0_ from TRACK track0_ where track0_.pla
yTime<=?
[java] Track: "Russian Trance", 00:03:30
[java] Track: "Video Killed the Radio Star", 00:03:49
BUILD SUCCESSFUL
Total time: 2 seconds
We started out by defining a utility method,
tracksNoLongerThan()
, which performs
the actual Hibernate query. It retrieves any tracks whose playing
time is less than or equal to the amount specified as a
parameter. Notice that HQL, Hibernate’s
SQL-inspired query language, supports parameter
placeholders, much like PreparedStatement
in
JDBC. And, just like in that environment, using them is preferable
to putting together queries through string manipulation (especially
since this protects you from SQL injection
attacks). As you’ll see, however, Hibernate offers even better ways
of working with queries in Java.
The query itself looks a little strange. It starts with
from
rather than select
, as you might
expect. While you can certainly use the more familiar format, and
will do so when you want to pull out individual properties from an
object in your query, if you want to retrieve entire objects, you
can use this more abbreviated syntax.something
Also note that the query is expressed in terms of the mapped Java objects and properties, rather than the tables and columns. It’s not obvious in this case, since the object and table have the same name, as do the property and column, but it is true. Keeping the names consistent is a fairly natural choice and will always be the case when you’re using Hibernate to generate the schema and the data objects, unless you tell it explicitly to use different column names.
When you’re working with preexisting databases and objects, it’s important to realize that HQL queries refer to object properties rather than to database table columns.
Also, just as in SQL, you can alias a column or table to another name. In HQL, you alias classes in order to select or constrain their properties. This won’t come up in this simple introduction, but if you dig into the resources mentioned in Appendix E, you’ll encounter it.
The rest of the program should look mighty familiar from the
previous example. Our try
block is simplified because
we don’t need explicit access to our transaction, as we’re not
changing any data. We still use try
so that we can have
a
clause to
close our session cleanly. The body is quite simple, calling our
query method to request any tracks whose playing time is five
minutes or less, and then iterating over the resulting
finally
Track
objects, printing their titles and
playing times.
Also, now that we’ve turned off “info” level logging from within
Hibernate, the SQL debugging output we configured in
hibernate.cfg.xml is much easier to
spot—the first line in the "qtest:"
section of the
output is not something we wrote ourselves in QueryTest.java; it’s Hibernate showing
us the SQL it generated to implement the
HQL query we requested. Interesting… and if you ever
get sick of seeing it, remember that you can set the show_sql
property value to false
.
…deleting objects? If you’ve made changes to your
data-creation script and want to start with a “clean slate” in the form
of an empty database so you can test them, all you need to do is run
ant schema again. This will drop and
recreate the Track
table in a
pristine and empty state. Don’t do it unless you mean it!
If you want to be more selective about what you delete, you can
either do it through SQL commands in the HSQLDB
UI (ant db), or
you can make a variant of the query-test example that retrieves the
objects you want to get rid of. Once you’ve got a reference to a
persistent object, passing it to the
Session
’s delete()
method will remove
it from the database:
session.delete(aTrack
);
You’ve still got at least one reference to it in your program,
until aTrack
goes out of scope or
gets reassigned, so conceptually the easiest way to understand what
delete()
does is to
think of it as turning a persistent object back into a transient
one.
Another way to delete is to write an HQL deletion query that matches multiple objects. This lets you delete many persistent objects at once, whether or not you have them as objects in memory, without writing your own loop. A Java-based alternative to ant schema, and a slightly less violent way of clearing out all the tracks, would therefore be something like this:
Query query = session.createQuery("delete from Track"); query.executeUpdate();
As mentioned earlier in this chapter, HQL lets you go beyond the use of JDBC-style query placeholders to get parameters conveniently into your queries. You can use named parameters and named queries to make your programs much easier to read and maintain.
Named parameters make code easier to understand because the purpose of the parameter is clear both within the query itself and within the Java code that is setting it up. This self-documenting nature is valuable in itself, but it also reduces the potential for error by freeing you from counting commas and question marks, and it can modestly improve efficiency by letting you use the same parameter more than once in a single query.
Named queries let you move the queries completely out of the Java code. Keeping queries out of your Java source makes them much easier to read and edit because they aren’t giant concatenated series of Java strings spread across multiple lines and interwoven with extraneous quotation marks, backslashes, and other Java punctuation. Typing them the first time is bad enough, but if you’ve ever had to perform significant surgery on a query embedded in a program in this way, you will have had your fill of moving quotation marks and plus signs around to try to get the lines to break in nice places again.
The key to both of these capabilities in Hibernate is
the Query
interface. We already began
using this interface in Example 3-7 because, starting with Hibernate 3, it is
the only nondeprecated way of performing queries. So today it’s even
less of an effort than it used to be to use the nice features described
in this section.
We’ll start by changing our query to use a named parameter, as shown in Example 3-10. (This isn’t nearly as big a deal for a query with a single parameter like this one, but it’s worth getting into the habit right away. You’ll be very thankful when you start working with the light-dimming queries that power your real projects!)
public static List tracksNoLongerThan(Time length, Session session) { Query query = session.createQuery("from Track as track " + "where track.playTime <= :length"); query.setTime("length", length); return query.list(); }
Named parameters are identified within the query body by prefixing
them with a colon. Here, we’ve changed the ?
to :length
. The
Session
object provides a
createQuery()
method that gives us back an
implementation of the Query
interface with which
we can work. Query
has a full complement of
type-safe methods for setting the values of named parameters. Here we
are passing in a Time
value, so we use setTime()
. Even
in a simple case like this, the syntax is more natural and readable than
the original version of our query. If we had been passing in anonymous
arrays of values and types (as would have been necessary with more than
one parameter), the improvement would be even more significant. And
we’ve added a layer of compile-time type checking, always a welcome
change.
Running this version produces exactly the same output as our original program.
So, how do we get the query text out of the Java source? Again,
this query is short enough that the need to do so isn’t as pressing as
usual in real projects, but it’s the best way to do things, so
let’s start practicing! As you may have predicted, the place we
can store queries is inside the mapping document. Example 3-11 shows what this
looks like. We have to use the somewhat clunky CDATA
construct, since our query contains
characters (like <
) that could
otherwise confuse the XML parser.
<query name="com.oreilly.hh.tracksNoLongerThan"> <![CDATA[ from Track as track where track.playTime <= :length ]]> </query>
Put this just after the closing tag of the class definition in
Track.hbm.xml (right before the
</hibernate-mapping>
line).
Then we can revise QueryTest.java
one last time, as shown in Example 3-12. Once again, the program produces exactly
the same output as the initial version. It’s just better organized now,
and we’re in great shape if we ever want to make the query more
complex.
public static List tracksNoLongerThan(Time length, Session session) { Query query = session.getNamedQuery( "com.oreilly.hh.tracksNoLongerThan"); query.setTime("length", length); return query.list(); }
The Query
interface has other useful
capabilities beyond what we’ve examined here. You can use it to control
how many rows (and which specific rows) you retrieve. If your
JDBC driver supports scrollable
ResultSet
s, you can access this capability as
well. Check the JavaDoc or the Hibernate reference manual for more
details.
…avoiding a SQL-like language altogether? Or diving into HQL and exploring more complex queries? These are both options that are covered later in this book.
Chapter 8 discusses criteria queries, an interesting mechanism that lets you express the constraints on the entities you want, using a natural Java API. This lets you build Java objects to represent the data you want to find, which is easier for people who aren’t database experts to understand; lets you leverage your IDE’s code completion as a memory aid; and even gives you compile-time checking of your syntax. It also supports a form of “query by example,” where you can supply objects that are similar to the ones you’re searching for, which is particularly handy for implementing search interfaces in an application.
SQL veterans who’d like to see more tricks with HQL can jump to Chapter 9, which explores more of its capabilities and unique features.
For now, we’ll continue our exploration of mapping by looking at how to cope with objects that are linked to each other, which you will need in any nontrivial program.
[1] Even though the codegen
target depends on the prepare
target, the very first time
you’re working with one of the example directories you need to
create the proper classpath structure for Ant to be happy by running
prepare
explicitly first, as
discussed in “Cooking Up a Schema” back in Chapter 2.