3.1 Creating Persistent Objects

     

Let's start by creating some objects in Java 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 properties file, it's extremely easy to configure the Hibernate session factory and get things rolling.

To get started, set up the Hibernate environment and use it to turn some new Track instances into corresponding rows in the database table.

3.1.1 How do I do that?

This discussion assumes you've created the schema and generated Java code by following the examples in Chapter 2. If you haven't, you can start by downloading the examples archive from this book's web site, jumping in to the ch03 directory, and copying in the third-party libraries as instructed in Chapter 1. Once you've done that, use the commands ant codegen followed by ant schema to 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/command window whose current working directory is the top of your project tree, containing Ant's build.xml file.

NOTE

The examples in most chapters build on the previous ones, so if you are skipping around, you'll really want to download the sample code.

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 we started with. The source is shown in Example 3-1.

Example 3-1. CreateTest.java
 1  package com.oreilly.hh;  2  3  import net.sf.hibernate.*;  4  import net.sf.hibernate.cfg.Configuration;  5  6  import java.sql.Time;  7  import java.util.Date;  8  9  /** 10   * Create sample data, letting Hibernate persist it for us. 11   */ 12  public class CreateTest { 13 14    public static void main(String args[]) throws Exception { 15      // Create a configuration based on the properties file we've put 16      // in the standard place. 17      Configuration config = new Configuration(); 18 19      // Tell it about the classes we want mapped, taking advantage of 20      // the way we've named their mapping documents. 21      config.addClass(Track.class); 22 23      // Get the session factory we can use for persistence 24      SessionFactory sessionFactory = config.buildSessionFactory(); 25 26      // Ask for a session using the JDBC information we've configured 27      Session session = sessionFactory.openSession(); 28      Transaction tx = null; 29      try { 30          // Create some data and persist it 31          tx = session.beginTransaction(); 32 33          Track track = new Track("Russian Trance", 34                                  "vol2/album610/track02.mp3", 35                                   Time.valueOf("00:03:30"), new Date(), 36                                   (short)0); 37          session.save(track); 38 39          track = new Track("Video Killed the Radio Star", 40                            "vol2/album611/track12.mp3", 41                            Time.valueOf("00:03:49"), new Date(), 42                            (short)0); 43          session.save(track); 44 45 46          track = new Track("Gravity's Angel", 47                            "vol2/album175/track03.mp3", 48                            Time.valueOf("00:06:06"), new Date(), 49                            (short)0); 50          session.save(track); 51 52          // We're done; make our changes permanent 53          tx.commit(); 54 55        } catch (Exception e) { 56             if (tx != null) { 57                // Something went wrong; discard all partial changes 58                tx.rollback(); 59           } 60           throw e; 61        } finally { 62             // No matter what, close the session 63             session.close(); 64        } 65 66        // Clean up after ourselves 67        sessionFactory.close(); 68     } 69   } 

With all we've got in place by now it's quite easy to tell Ant how to run this test. Add the target shown in Example 3-2 right before the closing </project> tag at the end of build.xml .

Example 3-2. Ant target to invoke our data creation test
 <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> 

All right, we're ready to create some data! Example 3-3 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-3. Invoking the CreateTest class
 %  ant ctest  Buildfile: build.xml              prepare:       compile:           [javac] Compiling 1 source file to /Users/jim/Documents/Work/OReilly/       Hibernate/Examples/ch03/classes              ctest:           [java] 00:07:46,376 INFO Environment:432 - Hibernate 2.1.1           [java] 00:07:46,514 INFO Environment:466 - loaded properties from resource       hibernate.properties: {hibernate.connection.username=sa, hibernate.connection.       password=, hibernate.cglib.use_reflection_optimizer=true, hibernate.dialect=net.       sf.hibernate.dialect.HSQLDialect, hibernate.connection.url=jdbc:hsqldb:data/       music, hibernate.connection.driver_class=org.hsqldb.jdbcDriver}           [java] 00:07:46,644 INFO Environment:481 - using CGLIB reflection optimizer           [java] 00:07:46,691 INFO Configuration:318 - Mapping resource: com/oreilly/       hh/Track.hbm.xml           [java] 00:07:50,686 INFO Binder:225 - Mapping class: com.oreilly.hh.Track       -> TRACK           [java] 00:07:51,620 INFO Configuration:584 - processing one-to-many       association mappings           [java] 00:07:51,627 INFO Configuration:593 - processing one-to-one       association property references           [java] 00:07:51,628 INFO Configuration:618 - processing foreign key       constraints           [java] 00:07:51,869 INFO Dialect:82 - Using dialect: net.sf.hibernate.       dialect.HSQLDialect           [java] 00:07:51,886 INFO SettingsFactory:62 - Use outer join fetching: false           [java] 00:07:51,966 INFO DriverManagerConnectionProvider:41 - Using       Hibernate built-in connection pool (not for production use!)           [java] 00:07:52,036 INFO DriverManagerConnectionProvider:42 - Hibernate       connection pool size: 20           [java] 00:07:52,117 INFO DriverManagerConnectionProvider:71 - using driver:       org.hsqldb.jdbcDriver at URL: jdbc:hsqldb:data/music           [java] 00:07:52,135 INFO DriverManagerConnectionProvider:72 - connection       properties: {user=sa, password=}           [java] 00:07:52,171 INFO TransactionManagerLookupFactory:33 - No       TransactionManagerLookup configured (in JTA environment, use of process level       read-write cache is not recommended)           [java] 00:07:53,497 INFO SettingsFactory:89 - Use scrollable result sets:       true           [java] 00:07:53,504 INFO SettingsFactory:99 - Query language substitutions: {}           [java] 00:07:53,507 INFO SettingsFactory:110 - cache provider:       net.sf.ehcache.hibernate.Provider           [java] 00:07:53,528 INFO Configuration:1057 - instantiating and configuring       caches           [java] 00:07:54,533 INFO SessionFactoryImpl:119 - building session factory           [java] 00:07:56,721 INFO SessionFactoryObjectFactory:82 - no JNDI name       configured           [java] 00:07:57,357 INFO SessionFactoryImpl:527 - closing           [java] 00:07:57,370 INFO DriverManagerConnectionProvider:137 - cleaning up       connection pool: jdbc:hsqldb:data/music              BUILD SUCCESSFUL       Total time: 23 seconds 

3.1.2 What just happened ?

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 Command Select in the menu bar.)

Figure 3-1. Test data persisted into the TRACK table
figs/hibernate_0301.jpg

The first part of Example 3-1's code needs little explanation. Lines 3 and 4 import some useful Hibernate classes, including Configuration , which is used to set up the Hibernate environment. 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 run, we start by creating a Hibernate Configuration object on line 17. Since we don't tell it otherwise , it looks for a file named hibernate . properties at the root level in the class path. It finds the one we created in the previous chapter (see Example 2-4 ), which tells it we're using HSQLDB, and how to find the database. Next line 21 requests mapping services for the Track class. Because we've placed the mapping file Track.hbm.xml in the same package, and followed the standard naming convention, Hibernate is able to find and load it without requiring an explicit path. This approach is particularly handy when you want to distribute your application as a Jar archive, or when you are operating in a web application environment.

That's all the configuration we need in order to create and persist track data, so we're ready to create the SessionFactory on line 24. Its purpose is to provide us with Session objects, the main avenue for interaction with Hibernate. The SessionFactory is thread safe, and you only need 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 notebook 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 (line 27), 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. Like 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 are not thread safe, so they cannot be shared between threads. Each thread needs to obtain its own session from the factory.

We need to look more closely at 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 it is garbage collected or your application terminates.

Passing a transient mapped object to a Session 's save() method causes it to become persistent. It will survive garbage collection and termination of the Java VM, staying available until it is explicitly deleted. (There is a related distinction between entities and values discussed at the beginning of Appendix A. Mapped objects that have been persisted are called entities, even if they do not currently exist as an instance in any virtual machine.) If you've got a persistent object and you call Session 's delete() method on it, the object transitions back to a 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; it's ceased being an entity.

On the other hand, and this point is worth extra emphasis, if you haven't deleted an object (so it's still persistent), when you change its properties there is no need to save it again for those changes to be reflected in the database. 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 you worked with 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 the upcoming section, 'Finding Persistent Objects'). As noted above, 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; they're still entities. 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. 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. Line 31 sets 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 (lines 33-50), turning them from transient instances into persistent entities. Finally, line 53 commits 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, lines 56-60 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 at line 63, 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 on line 67. 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. [3.1] In this simple example, when the main() method returns, the application is ending.

[3.1] If you're not familiar with these, read about the ServletContextListener interface in the Servlet 2.3 specification.

One pitfall I discovered in working with this test program is that it is necessary to explicitly close both the SessionFactory and Session . If you don't, the application won't exit when main() returns, and you have to kill it with Ctrl-C . This turns out to be caused by a non-daemon thread used in the current HSQLDB logging implementation. The HSQLDB developers have said they may fix this in the next release, in which logging is being heavily revamped . Even if they do, similar issues may exist with other databases; it's better to be safe and close things gracefully when you know you're done using them.


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 to issue SQL commands. Looking back to the preceding chapter, we didn't even have to create the table ourselves, nor the Track object that encapsulates our data. Yet the query 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.

Although the Hibernate reference documentation states that the use of transactions is optional, I found that (at least through Version 2.1.1, with the HSQLDB database) any changes I made outside the context of a Hibernate transaction would disappear when the application ended. Analyzing the SQL emitted by Hibernate revealed that even though I didn't request a transaction, auto-commit was being turned off, so my changes were getting rolled back when the application ended. So for now it's always necessary to use explicit transactions in your code, a good habit in any case.


An alternate way of seeing the results of our persistence test is to simply look at the database itself. Since we're using HSQLDB, the database is stored in a highly human-readable format: the file music.script contains a series of SQL statements that are used to reconstruct the in-memory database structures when the database is opened. The end of the file contains our persisted objects, as shown in Example 3-4.

Example 3-4. Looking at the raw database file
 %  tail data/music.script  CREATE ALIAS ATAN FOR "java.lang.Math.atan" CREATE ALIAS UPPER FOR "org.hsqldb.Library.ucase" CREATE ALIAS ASCII FOR "org.hsqldb.Library.ascii" CREATE ALIAS RAND FOR "java.lang.Math.random" CREATE ALIAS LENGTH FOR "org.hsqldb.Library.length" CREATE ALIAS ROUND FOR "org.hsqldb.Library.round" CREATE ALIAS REPLACE FOR "org.hsqldb.Library.replace" INSERT INTO TRACK VALUES(0,'Russian Trance','vol2/album610/track02.mp3', '00:03:30','2003-12-11',0) INSERT INTO TRACK VALUES(1,'Video Killed the Radio Star','vol2/album611/ track12.mp3','00:03:49','2003-12-11',0) INSERT INTO TRACK VALUES(2,'Gravity''s Angel','vol2/album175/track03.mp3', '00:06:06','2003-12-11',0) 

The final three statements show our TRACK table rows. The aliases that come before them are part of the normal HSQLDB environment you get when creating a new database.

NOTE

Tempted to learn more about HSQLDB? I won't try to stop you!

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



Hibernate. A Developer's Notebook
Hibernate: A Developers Notebook
ISBN: 0596006969
EAN: 2147483647
Year: 2003
Pages: 65
Authors: James Elliott

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net