Section 20.4. The Hibernate API


20.4. The Hibernate API

Now that we've covered how to map your domain onto a database, we'll examine using the Hibernate API to actually perform database operations. The various portions of the API are treated separately. A Hibernate application will usually make use of most of what you see here, though Interceptors and Events are somewhat less common than the others.

20.4.1. Configuration and Hibernate

Hibernate provides a class, org.hibernate.cfg.Configuration, that serves as the entry point to your application's interaction with the database. You use the Configuration class to load mapping files, specify global options, and retrieve metadata about your schema and objects.

If you are using the hibernate.properties or hibernate.cfg.xml files and they are in the classpath of your application, the easiest way to configure Hibernate is to create an instance of Configuration and call the configure method:

 Configuration cfg = new Configuration(  ).configure(  ); 

The configure method can take no arguments, as shown, in which case Hibernate will search for the first matching configuration file. Optionally, you can pass a String, url, File, or Document to the method to load the configuration file from an alternate location.

This step needs to be taken only once per application lifetime. Parsing the configuration properties and establishing the global settings for Hibernate is a relatively lengthy and expensive process. The application should execute the step only once and cache the resulting instance of Configuration somewhere from which it can be retrieved later.

20.4.2. SessionFactory

After establishing the configuration settings, you use the configuration instance to create an instance of org.hibernate.SessionFactory. The SessionFactory generates actual connections to the data store. Usually, you should create only one SessionFactory per application lifetime. Keep a reference to that SessionFactory for use throughout the application.

 SessionFactory factory = cfg.buildSessionFactory(  ); 

In special cases, such as when you have to connect to multiple data stores, you will need multiple SessionFactorys. If your application must communicate with multiple databases, you must provide multiple sets of configuration properties and multiple connection wrappers. In this scenario, you would create multiple properties files (datastore1.hibernate.properties and datastore2.hibernate.properties) and load them each manually:

 Configuration cfg1 =     new Configuration.configure("datastore1.hibernate.properties"); Configuration cfg1 =     new Configuration.configure("datastore2.hibernate.properties"); 

From each instance of configuration, you would build a separate SessionFactory. For example:

 SessionFactory factory1 = cfg1.buildSessionFactory(  ); 

20.4.3. Session

The Session object is the real meat of the Hibernate API . Session instances wrap logical JDBC connections. We say "logical" because the Session does not necessarily have to have an open JDBC connection; the connection could be in a closed state or have been released to the connection pool. Regardless, the Session instance is the only way to access the actual database connection in Hibernate, and it is the primary vehicle through which persistence operations are performed.

You retrieve an instance of Session by calling SessionFactory.openSession( ). Hibernate creates an instance of Session but does not retrieve an open JDBC connection from the pool. That act is delayed until you use the Session in such a way as to require a connection. From that moment on, the Session has an open JDBC connection to use until you release the connection, by calling either Session.disconnect( ) or Session.close( ) (more on this in a minute).

Once the physical connection is established, you should use the Session to do your work as quickly as possible and then release the connection. The longer your application waits to release the connection, the higher the likelihood of a concurrency conflict on the databases. Remember always that JDBC connections are rare, valuable resources and should be used as quickly as possible and released for others to use.

Whenever you use the Session, you should use appropriate error-handling blocks. The standard usage pattern is shown in Example 20-13.

Example 20-13. Standard Session try/catch/finally usage pattern
 Session session = null; try {     session = factory.openSession(  );     // Use the Session } catch (Exception ex) {     // Handle exceptions } finally {     if(null! = session) session.close(  ); } 

20.4.3.1. Loading objects

The primary uses of the Session are to load and save first-class Hibernate objects. To load an instance of a persistent class, you have to tell Hibernate what kind of object you are looking for as well as the unique ID that allows Hibernate to find the specific instance:

 // Load a UniversityClass UniversityClass class =     (UniversityClass)session.load(UniversityClass.class, new Long(1)); 

If you use the Session to load instances from an inheritance hierarchy, you specify the specific subclass you want to load or the base class if you don't care. Hibernate will not load the wrong type in response to such a request; if you ask for an instance of Professor, but provide the ID of a Student, Hibernate will return an ObjectNotFoundException.

 // Load a Professor Professor prof =     (Professor)session.load(Professor.class, new Long(2)); // Load a Person Person person =     (Person)session.load(Person.class, new Long(1)); 

When any of these load commands is issued, the lazy initialization rules you established in your mapping files are applied. Loading the UniversityClass will cause all its Students to be loaded at the same time unless the students collection is designated as a lazy collection.

20.4.3.2. Saving new objects

Adding new objects is just as straightforward as loading them. First, create an instance of the given persistent class. Then, use the Session to save the object. If there are conflicts when you attempt the save, Hibernate will throw detailed exceptions to explain the problem.

 // Create a new Professor Professor prof = new Professor(  ); prof.setFirstName("Jim"); prof.setLastName("Jacobs"); Session session = null; try {     session = factory.openSession(  );     session.save(prof); } catch (Exception ex) {     // Handle the exception } finally {     if(null! = session) session.close(  ); } 

Earlier, we established that our persistent class IDs were going to be managed by the database, which means we don't have to set our new Professor's ID property. Hibernate will recognize that the ID property is in an uninitialized state and fill in the value for us during the save procedure. After the successful execution of the save, the value is populated.

We also know that our Professor can have a collection of email addresses. Adding them is simple:

 Professor prof = new Professor(  ); prof.setFirstName("Jim"); prof.setLastName("Jacobs"); prof.getEmails.add("jjacobs@directional.edu"); prof.getEmails.add("jim_j@other.obscure.journal.com"); // Then save as before 

The emails table will now have two new entries, each with a foreign key relationship back to the new record of the person table where our new Professor resides.

We also know that the Professor can teach one or more UniversityClasses. We can create them all at once using a single Session:

 Professor prof = new Professor(  ); prof.setClasses(new HashSet(  )); // empty initial collection // Set prof properties ... UniversityClass english = new UniversityClass(  ); english.setName("English 101"); UniversityClass lit = new UniversityClass(  ); lit.setName("Literature 105"); session.save(english); session.save(lit); prof.getClasses.add(english); prof.getClasses.add(lit); session.save(prof); 

In this example, we could have called session.save( ) on the three new objects in any order we desired; it is easier to keep track of them if you call save on the children first, then the parent.

20.4.3.3. Updating existing objects

The simplest scenario for managing updates is to perform modifications to a loaded instance while the loading Session is still open. Modifications will be made to the underlying record when the session is flushed or the transaction is committed (we'll discuss the details later in this chapter). There is no need to tell Hibernate to perform the update; it will notice if the state of the persistent object has changed and push the update automatically.

 Session session = null; try {     session = factory.openSession(  );     Professor prof = (Professor)session.load(Professor.class, new Long(3));     // Just got her doctorate     prof.setDegree("PhD");     session.flush(  ); } catch (Exception ex) {     // Handle exception } finally {     if(null! = session) session.close(  ); } 

20.4.3.4. Retrieving multiple objects

Sometimes, loading a single object at a time is not sufficient. You need to load an entire collection of objects, whether that is the entirety of a table or just a selected portion. The Session object exposes a set of methods that allows you to execute a query against the database (for more details, see "Query" and "HQL (Hibernate Query Language)," later in this chapter). In its simplest format, the query API allows you to specify a persistent class and some criteria for loading from its table(s). Using this query, you can retrieve a List of the results, an iterator to the list, or a scrollable result set.

 List professors = session.createQuery("from Professor").list(  ); Iterator studentIterator = session.createQuery("from Student").iterate(  ); 

What if you wanted to find a filtered set of results? For example, all Professors with a PhD? You could iterate through the list of all Professor objects, query the property using geTDegree( ), and just keep the matches, or you could use the createQuery( ) method.

 List professors =     session.createQuery("from Professor where degree = 'PhD'").list(  ); 

This is much faster than iterating over the objects in Java because the database is optimized to provide this kind of filtering, the number of rows returned is minimized, and the number of objects created is likewise kept to a minimum. For more details about the kinds of queries you can execute, see "Query and "HQL (Hibernate Query Language)," later in this chapter.

20.4.3.5. Connection management

While you are using a Session, you can perform three distinct operations to cause the state of the Session to be written to the database. They are:

  • Session.flush( )

  • Session.connection( ).commit( )

  • Session.close( )

Flushing the Session causes the state of the objects associated with the Session to be synchronized with the state of the database. This is critical to understand: saves, updates, and deletes do not get persisted to the data store until the Session is flushed. Flushing can occur multiple times during the lifetime of a single Session. Updates between flushes are cached in the Session and executed as a batch when a flush is triggered (either explicitly, by calling Session.flush( ), or implicitly, as part of another call, like Session.close( )).

Using a previous example, the three saves are all cached until the flush happens at the end of the method:

 Professor prof = new Professor(  ); prof.setClasses(new HashSet(  )); // empty initial collection // Set prof properties ... UniversityClass english = new UniversityClass(  ); english.setName("English 101"); UniversityClass lit = new UniversityClass(  ); lit.setName("Literature 105"); session.save(english); session.save(lit); prof.getClasses.add(english); prof.getClasses.add(lit); session.save(prof); session.flush(  ); 

Conversely, several calls to Session.flush( ) could be interspersed throughout the method to keep the state of the Session synchronized with the database during long-running methods.

If you attempt to flush a Session and the underlying state of the data has changed, say because another user has modified the record you are working with after you originally loaded it, Hibernate will throw a StaleStateException. In order to recover from this, you must refresh the state of the object you were persisting by calling Session.refresh(staleobject). This will update the object in memory to match the current values in the database. Then, you must reapply your changes and attempt the flush again.

When you are through using the Session, it is imperative that you close it. That's why, in previous code examples, the Session.close( ) appears in a finally block, ensuring that it gets called. Closing the Session not only released the JDBC connection back to the pool but also released references to persistent objects that were associated with the session, allowing the garbage collector to collect them (as long as your application code has released its references as well). Any call to Session.close( ) automatically invokes Session.flush( ) as well.

If you would like to be able to release connections to the connection pool but not terminate the Session (and therefore orphan all the persistent objects associated with the session), you can use the lighter-weight Session.disconnect( )/Session.reconnect( ) pair of methods. Disconnect simply drops the JDBC connection back into the pool. Persistent objects are still associated with the Session, and you can still manipulate their properties, but nothing will be flushed back to the database. Calling Session.flush( ) while the session is disconnected will result in a HibernateException. Likewise, manipulating lazy associations that haven't been initialized while disconnected will raise a LazyInitializationException. To get a new JDBC connection from the pool, simply call Session.reconnect and all your functionality will be returned.

20.4.3.6. Disconnected objects

It is perfectly acceptable to load objects from the database and then close the Session that was used to load them. Objects in this state are considered disconnected. You may pass them around, access and modify their properties, and call methods on them in this state. The only exception is that you may not access lazily loaded associations that have not been initialized yet since there is no open Session to use to retrieve the lazy data.

If you need to then persist the changes to the disconnected object, you can associate it with a new Session. You have several choices for doing so. First, if the state of the object is unchanged since the original Session was closed, you can use the Session.lock( ) method to associate the object with the new session. You must specify the lock mode you wish to use (none, read, upgrade, or write):

 Session session1 = factory.openSession(  ); Professor prof = (Professor)session.load(Professor.class, new Long(2)); session1.close(  ); // Do other work Session session2 = factory.openSession(  ); session2.lock(prof, LockMode.WRITE); prof.setDegree("PhD"); session2.close(  ); // Causes a flush 

If the state of the object has changed, or has the possibility of having changed, since the original Session was closed, but the new Session has not loaded its own version of that instance, you can use the Session.update( ) method to associate the object with the new Session. If that Session turns out to have its own loaded copy of that instance, Hibernate will throw a HibernateException explaining that there is an identity conflict in the new Session.

 Session session1 = factory.openSession(  ); Professor prof = (Professor)session.load(Professor.class, new Long(2)); session1.close(  ); // Do other work Session session2 = factory.openSession(  ); session2.update(prof); prof.setDegree("PhD"); session2.close(  ); // Causes a flush 

Finally, if the new Session is known to have its own version of the disconnected instance, you can use Session.merge to merge the values from the disconnected copy into the new Session's connected copy.

20.4.4. Query

Hibernate's Query class gives you an object-oriented way to manage repeatable, parameterized queries to the database. For full details about what is possible using HQL, see the HQL section later in this chapter. To create a Query, use the Session.createQuery( ) method we used earlier:

 Query getprofs = session.createQuery("from Professor"); 

While such a query might be useful (it can be cached and executed multiple times), it does not provide much benefit over directly executing it. What would be more useful is if the query required one or more parameters. Creating a cached, parameterized query is convenient programmatically (it is easy to set the parameters and reexecute the query), more efficient since the statement has been prepared already, and more secure, because parameter values are never treated as potentially executable code (thus they mitigate the risk of SQL injection attacks).

 Query getprofsbydegree =     session.createQuery("from Professor where degree = :degree"); getprofsbydegree.setString("degree", "PhD"); List results = getprofsbydegree.list(  ); 

The named-parameter strategy is the most convenient, as you can specify the parameter values in any order. If you prefer, you can use standard JDBC-style numbered parameters, but bear in mind that the first parameter in a Hibernate query is parameter 0, not 1.

20.4.5. Interceptors

It seems these days that if a framework doesn't offer some kind of aspect-oriented programming technique, then it can't be considered "modern" or "complete." Therefore, Hibernate offers the Interceptor interface and a strategy for connecting the interceptor to a session. Bear in mind, interceptors are configured either per-session or once globally, and each session can have only a single interceptor associated with it. Writing good, general interceptors can then become an exercise in cramming a lot of code into a single class.

The interceptor interface is large and complex. It allows you to trap a variety of different events in the Session: for example, onLoad, onDelete, onFlushDirty, and beforeTransactionCompletion. The interceptor gets the first crack at these events; if you need to cancel the rest of the event from the interceptor, you have to throw a CallbackException, which will unwind the stack before the Session can complete the action.

Let's just look at the onSave( ) method of the interface:

 public boolean onSave(Object o, Serializable serializable,                       Object[] objects, String[] strings,                       Type[] types) throws CallbackException {     return false; } 

The method returns a boolean value, which is to notify Hibernate if you have modified any of the values passed into the method (the object or any of its properties). The parameters themselves seem complex but are in fact quite straightforward:


Object o

The persistent object being operated on


Serializable serializable

The ID of the object


Object[] objects

An array of (only) the persistent property values of the object


String[] strings

An array of (only) the persistent property names of the object


Type[] types

An array of (only) the persistent property types of the object

Interceptors are often used for security or logging purposes. A common security interceptor looks like this:

 public boolean onSave(Object o, Serializable serializable,     Object[] objects, String[] strings, Type[] types)     throws CallbackException {       // A custom Authorize class that implements     // your business security rules     if(!Authorize.isAuthorized(o))         throw new CallbackException             ("Current user not authorized to save this object.");     return false; } 

To associate the interceptor with a specific Session, you specify it during the call to SessionFactory.openSession( ):

 Session session = factory.openSession(new SecurityInterceptor(  )); 

Conversely, you can associate it with every Session by setting it on the Configuration object:

 Configuration cfg = new Configuration(  ).configure(  ); cfg.setInterceptor(new SecurityInterceptor(  )); 

20.4.6. Events

Brand new in Hibernate 3.0, Events replace the old Lifecycle interface for providing an alternate event-listening strategy. The Lifecycle interface was implemented by your domain objects to provide event callbacks on individual persistent instances. This made your domain objects decidedly nontransparent; they had direct references to the Hibernate libraries and were now cluttered with extraneous, non-domain-related code.

Instead, Hibernate now supplies a series of interfaces, one for each event published by the Session. To create a listener, you write a class that implements the chosen event interface or derives from one of the supplied default implementations of those interfaces. Then, you configure Hibernate to recognize your event.

Each event interface provides a single method that you implement to handle the inbound call. The methods all have slightly different signatures based on the activity they represent, but they all start with a parameter called event that allows you access to the entity being acted on and any context data associated with the call. Example 20-14 shows our security interceptor, rewritten as a SaveOrUpdate event listener.

Example 20-14. SecureUpdateListener.java
 public class SecureUpdateListener     extends org.hibernate.event.DefaultUpdateEventListener {     public Serializable onSaveOrUpdate(SaveOrUpdateEvent event)         throws HibernateException {         if(!Authorize.isAuthorized(event.getEntity(  ))            throw new HibernateException("Not authorized.");         return super.onSaveOrUpdate(event);     } } 

To configure Hibernate to utilize the listener, you can set it programmatically:

 Configuration cfg = new Configuration(  ).configure(  ); cfg.setListener("saveOrUpdate", new SecureUpdateListener(  )); 

Or declaratively (say, in hibernate.cfg.xml):

 <hibernate-configuration>     <session-factory>         ...         <listener type="saveOrUpdate"           />     </session-factory> </hibernate-configuration> 

You can use the same class to listen for multiple events. Just implement all the appropriate interfaces on that class, implement the methods on those interfaces, and use the same class name for each event that you are listening on. Hibernate creates a singleton instance of each listener to use, so the same instance will be used across the different event calls. Bear in mind that, as with interceptors, you can configure only a single listener per event type in your application. Setting the listener again replaces the original listener with the new one and does not append it to the call chain.



Java Enterprise in a Nutshell
Java Enterprise in a Nutshell (In a Nutshell (OReilly))
ISBN: 0596101422
EAN: 2147483647
Year: 2004
Pages: 269

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