Chapter 3. Harnessing Hibernate

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.

Configuring Hibernate

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.

Example 3-1. Configuring Hibernate using XML: hibernate.cfg.xml
<?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> 1

    <!-- Database connection settings --> 2
    <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> 3
        
    <!-- Enable Hibernate's automatic session context management -->
    <property name="current_session_context_class">thread</property>
        
    <!-- Disable the second-level cache  --> 4
    <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> 5
        
    <!-- Echo all executed SQL to stdout -->
    <property name="show_sql">true</property> 6

    <!-- List all the mapping documents we're using --> 7
    <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:

1

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.

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.

3

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.

4

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.

5

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.

6

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.

7

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.

Example 3-2. Changes to build.xml to use XML configuration for Hibernate
... 

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

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

How do I do that?

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.

Example 3-3. Data creation test, CreateTest.java
package com.oreilly.hh;

import org.hibernate.*; 1
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(); 2
        config.configure();

        // Get the session factory we can use for persistence
        SessionFactory sessionFactory = config.buildSessionFactory(); 3

        // Ask for a session using the JDBC information we've configured
        Session session = sessionFactory.openSession(); 4
        Transaction tx = null;
        try {
            // Create some data and persist it
            tx = session.beginTransaction(); 5

            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(); 6

        } 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(); 7
    }
}

The first part of CreateTest.java needs a little explanation:

1

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.

2

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.

3

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.

Note

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.

4

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.

Note

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.

Tip

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.

5

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.

6

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.

7

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.

Example 3-4. Ant targets to compile all Java source and invoke data creation test
  <!-- Compile the java source of the project -->
  <target name="compile" depends="prepare" 1
          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"> 2
    <java classname="com.oreilly.hh.CreateTest" fork="true">
      <classpath refid="project.class.path"/>
    </java>
  </target>
1

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.

2

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.

Example 3-5. Invoking the CreateTest class
% 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

What just happened?

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 Execute button. You can get a command skeleton and syntax documentation by choosing CommandSelect in the menu bar.)

Test data persisted into the TRACK table
Figure 3-1. Test data persisted into the TRACK table

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.

Tip

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.

Example 3-6. Looking at the raw database script file
% 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.)

Note

Tempted to learn more about HSQLDB? We won’t try to stop you!

What about…

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

Finding Persistent Objects

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.

How do I do that?

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.

Example 3-7. Data retrieval test, QueryTest.java
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) { 1
        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 { 2
            // 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.

Example 3-8. Ant target to invoke our query 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
Example 3-9. Running the query test
% 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

What just happened?

1

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 something, 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.

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.

Note

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.

2

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

What about…

…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(); 

Warning

Don’t forget that regardless of which of these approaches you use, you’ll need to wrap the data-manipulation code inside a Hibernate transaction and commit the transaction if you want your changes to stick.

Better Ways to Build Queries

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.

Why do I care?

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.

How do I do that?

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

Example 3-10. Revising our query to use a named parameter
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.

Example 3-11. Our query in the mapping document
  <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.

Example 3-12. Final version of our query method
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 ResultSets, you can access this capability as well. Check the JavaDoc or the Hibernate reference manual for more details.

What about…

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

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

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