In this chapter, we will take a look at the Hibernate support code in Spring. We will begin with a short description of the Hibernate versions and their support in Spring. Next, you will read about configuration of the Hibernate session factory and transaction manager. After that, we will take a look at how to write efficient data manipulation code, focusing especially on using Hibernate with other data access methods. Next, we will take a look at lazy loading and session management—these areas are not very difficult conceptually but can become very complex in their implementation. Finally, we will you show some integration testing approaches.
Before we start the discussion of Spring Hibernate support, we should take a quick look at how Hibernate works. Hibernate is an object-relational mapping (ORM) tool; it finds, saves, and deletes plain Java objects in a database with minimal impact on the Java code. Let's take a look at a simple LogEntry
object defined in Listing 11-1.
Example 11.1. LogEntry Domain Object
public class LogEntry { private Long id; private String name; private Date date; public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public Long getId() { return id; } public void setId(Long id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; } }
We need to tell Hibernate how we want to store the LogEntry
objects. Next, we define the table name and the column names for the properties we want to persist. We must pay special attention to the primary key. Listing 11-2 shows the final Hibernate mapping file.
Example 11.2. Hibernate Mapping for the LogEntry Object
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping default-lazy="false"> <class name="com.apress.prospring2.ch11.domain.LogEntry" table="t_log_entry"> <id name="id" type="long" unsaved-value="null"> <generator class="sequence"> <param name="sequence">s_log_entry_id</param> </generator> </id> <property name="name" column="name_" not-null="true"/> <property name="date" column="date_" not-null="true"/> </class> </hibernate-mapping>
Here, you see that the primary key (the id
column and property) is generated from a sequence named s_log_entry_id
. Hibernate can tell (again through the information from the configuration file) when to perform a SQL insert and when to do an update. In this particular case, if the value returned from the getId()
method is null, Hibernate will issue an insert; otherwise, it will issue an update. The name
property is stored in a not null
column called name_
and the date
property is stored in a not null
column called date_
.
Now, to insert a new LogEntry
object, we simply create its instance and call session.saveOrUpdate(Object)
. To find a mapped object identified by its primary key, we use session.load(Class, Serializable)
or session.get(Class, Serializable)
. We will explore the differences between these two calls later on in the chapter. To select more than one log entry, we use many of the Query
methods (we create the Query
instances by calling one of the session.createQuery()
methods). Finally, to delete objects, we use the session.delete
methods.
Before we get to the details of the Spring Hibernate support, we must take a look at how Spring organizes the support classes in its distribution.
There are currently two major versions of Hibernate: Hibernate 2.x and Hibernate 3.x. Since the packaging of the two Hibernate versions in Spring is very similar, the Spring distribution, by default, includes Hibernate 3 support. If you want to use Hibernate 2.x, you must include the spring-hibernate2.jar
package and be careful not to accidentally use one of the org.springframework.orm.hibernate3
classes. Alternatively, you may choose the Spring modular distribution and include only the specific Spring packages your application needs. However, for new applications, we recommend that you use Hibernate 3, which means that you can carry on using the default spring.jar
package.
The Spring Hibernate support works with versions 2.1.8 for Hibernate 2 support and version 3.2.5ga for Hibernate 3 support.
The main Hibernate class is Session
, which provides methods to find, save, and delete mapped objects. To create a Hibernate Session
, you must first create the SessionFactory
; creating and configuring SessionFactory
to create the Session
s can be quite complex and cumbersome. Luckily, Spring comes to your aid with its implementation of the AbstractSessionFactoryBean
subclasses: LocalSessionFactoryBean
and AnnotationSessionFactoryBean
. The LocalSessionFactoryBean
needs to be configured with the mapping file locations, and these file locations need to be local from the application's point of view. In most cases, this means that the mapping resources are on the classpath. The second subclass, the AnnotationSessionFactoryBean
, picks up Hibernate mappings from annotations on the classes we wish to use in Hibernate.
We do not encourage the use of annotations on the objects you will persist using Hibernate. In most cases, the persistent objects will be your application's domain objects. Using Hibernate-specific annotations exposes too much information to the tiers above the data access tier. We prefer the old-fashioned way of creating mapping files—leaving the domain objects completely independent of the DAO implementation you choose.
If you annotate your domain objects with Hibernate-specific annotations, you are implying that you have implemented the data access in Hibernate.
Regardless of the mapping information source, we need to supply some common dependencies to construct a SessionFactory
using the appropriate factory bean. Listing 11-3 shows the minimalist LocalSessionFactoryBean
configuration.
Example 11.3. Minimal Configuration of the LocalSessionFactoryBean
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/j2ee/spring-jee-2.5.xsd"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/prospring2/ch11" expected-type="javax.sql.DataSource"/> <bean id="hibernateSessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingLocations"> <list>
<value>classpath*:/com/apress/prospring2/ch11/dataaccess/ hibernate/*.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <value>hibernate.dialect=org.hibernate.dialect.Oracle9Dialect</value> </property> </bean> </beans>
First, we declare the dataSource
bean of type JndiObjectFactoryBean
(for a detailed explanation of why we use JNDI, see Chapter 22). Next, we declare the hibernateSessionFactory
bean and set its dataSource
, mappingLocations
, and hibernateProperties
properties. The dataSoruce
property is necessary but not really very interesting. The mappingLocations
property is much more intriguing. It is a list of values that can be converted into a Spring Resource
; in this particular case, we include all *.hbm.xml
files in the com.apress.prospring2.ch11.dataaccess.hibernte
package. Finally, we need to tell Hibernate what SQL dialect to use. To do that, we set the hibernate.dialect
property in the properties list of the hibernateProperties
property (we honestly have to use the word property four times!). In this particular case, the database can be Oracle 9 or later (we use Oracle 10g). To verify that everything works, we will run the code from Listing 11-4.
Example 11.4. Sample Application to Verify That the Hibernate Configuration Works
public class DaoDemo { public static void buildJndi() { try { TestUtils.NamingContextBuilder builder; builder = TestUtils.NamingContextBuilder.emptyActivatedContextBuilder(); String connectionString = "jdbc:oracle:thin:@oracle.devcake.co.uk" + ":1521:INTL"; builder.bind("java:comp/env/jdbc/prospring2/ch11", new DriverManagerDataSource( "oracle.jdbc.driver.OracleDriver", connectionString, "PROSPRING", "x******6")); } catch (NamingException ignored) { } } public static void main(String[] args) throws NamingException { buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext("datasource-context-minimal.xml", DaoDemo.class); } }
The sample application only creates and destroys the Hibernate beans. It is important to take note of the dependencies of this code. We need to set the following packages on the classpath; otherwise, we will get dreaded ClassNotFound
exceptions:
antlr-2.7.6.jar
cglib-2.1_3.jar
commons-logging-1.1.jar
spring-2.0.6.jar
hibernate-3.2.5ga.jar
log4j-1.2.14.jar
When we run the application with the correct classpath, we should see the following log entries as the application starts up:
INFO [main] SessionFactoryImpl.<init>(161) | building session factory DEBUG [main] SessionFactoryImpl.<init>(173) | Session factory constructed with filter configurations : {} DEBUG [main] SessionFactoryImpl.<init>(177) | instantiating session factory with properties: {java.runtime.name=Java(TM) 2 Runtime Environment, Standard Edition, ... hibernate.bytecode.use_reflection_optimizer=false, ... hibernate.dialect=org.hibernate.dialect.Oracle9Dialect, ... hibernate.connection.provider_class= org.springframework.orm.hibernate3.LocalDataSourceConnectionProvider, ... } ... DEBUG [main] SessionFactoryObjectFactory.<clinit>(39) | initializing class SessionFactoryObjectFactory DEBUG [main] SessionFactoryObjectFactory.addInstance(76) | registered: 40284884158164670115816467d30000 (unnamed) ...
Notice that Spring builds the Hibernate SessionFactory
by providing the properties in the Hibernate Spring context file and the hibernate.connection.provider_class
property, which allows Spring to provide an implementation of Hibernate's ConnectionProvider
so that Hibernate can access Spring-managed DataSource
beans.
If, for some reason, you need to access the low-level Hibernate SessionFactory
and Session
instances created by calls to the factory, you can easily reference the Spring-managed bean that extends the AbstractSessionFactoryBean
. Listing 11-5 shows you how to do that.
Example 11.5. Direct Access to Hibernate SessionFactory and Session
public class DaoDemo { public static void buildJndi() { /* same code */ } public static void main(String[] args) throws NamingException { buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext("datasource-context-minimal.xml", DaoDemo.class);
SessionFactory sessionFactory = (SessionFactory)ac.getBean("hibernateSessionFactory"); Session session = sessionFactory.openSession(); // use the Session session.close(); } }
Notice that you have to manually request a new session and that you have to explicitly close the session. The call to session.close()
should really happen even if the operations we perform on the session fail with an exception. In addition to this, Hibernate sessions should typically begin with a call to the beginTransaction()
method and end with a call to the Transaction.commit()
method. We would also like to roll back the transaction if there are exceptions. The refactored code of the main
method is shown in Listing 11-6.
Example 11.6. Refactrored Code of the main Method
public class DaoDemo { public static void main(String[] args) throws Exception { buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext("datasource-context-minimal.xml", DaoDemo.class); SessionFactory sessionFactory = (SessionFactory)ac.getBean("hibernateSessionFactory"); Session session = null; Transaction transaction = null; try { session = sessionFactory.openSession(); transaction = session.beginTransaction(); // do work transaction.commit(); } catch (Exception e) { if (transaction != null) transaction.rollback(); throw e; } finally { if (session != null) session.close(); } } }
The code in bold shows the important refactorings: first of all the // do work
line performs whatever database work we wish to do. If the work succeeds (i.e., there are no exceptions), we commit the transaction; otherwise, we perform a rollback. Finally, we close the session. This code is acceptable if it only appears in the main method. If we find that we're writing the same code (except for the actual database work represented by the // do work
marker here), we should look at using the template method pattern. This is exactly what we have done in Spring. The HibernateTemplate
has operations that take instances of the HibernateCallback
interface. This interface performs the actual work in the database, but the calling code takes care of session management. Figure 11-1 shows the concept behind the implementation of the HibernateTemplate
callback.
As you can see, all the hard work gets done in the HibernateTemplate
class. All you have to do is supply the HibernateCallback
implementation that performs the work you want to do in the session. Usually, the HibernateCallback
implementation is an anonymous class; Listing 11-7 shows an example of its use.
Example 11.7. Use of the HibernateTemplate Class
public class HibernateTemplateDemo { public static void main(String[] args) throws Exception { DaoDemoUtils.buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext("datasource-context-minimal.xml", DaoDemo.class); SessionFactory sessionFactory = (SessionFactory)ac.getBean("hibernateSessionFactory"); HibernateTemplate template = new HibernateTemplate(sessionFactory); template.execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { // do the work return null; } }); } }
The preceding listing illustrates most of the points we discussed. We create the HibernateTemplate
class and give it the SessionFactory
instance. Next, we call its execute
method and supply an anonymous implementation of the HibernateCallback
that does all the database work. We get the Session
as the argument of the doInHibernate
method in the callback. The session
argument is never null
, and we do not have to worry about closing it or about managing exceptions in the implementation. In addition to this, the code in the callback supplied to the doInHibernate
method can participate in Spring transactions (we discuss transactions in much more detail in Chapter 16). The value we return from the doInHibernate
method simply gets propagated to the caller as the result of the execute
method.
In most applications, we will set the HibernateTemplate
as a dependency; you can do this because all methods of the HibernateTemplate
are thread safe, and you can, therefore, share a single instance of HibernateTemplate
among many DAO beans. Listing 11-8 shows how to declare the hibernateTemplate
bean; it can then be used in the same manner as the manually created HibernateTemplate
instance.
Example 11.8. hibernateTemplate Bean and Its Use
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/j2ee/spring-jee-2.5.xsd"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/prospring2/ch11" expected-type="javax.sql.DataSource"/> <bean id="hibernateSessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingLocations"> <list> <value>classpath*:/com/apress/prospring2/ch11/dataaccess/ hibernate/*.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> org.hibernate.dialect.Oracle9Dialect </prop> </props> </property> </bean> <bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate"> <property name="sessionFactory" ref="hibernateSessionFactory"/> </bean> </beans> public class HibernateTemplateBeanDemo {
public static void main(String[] args) throws Exception { DaoDemoUtils.buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext("datasource-context-ht.xml", DaoDemo.class); HibernateTemplate template = (HibernateTemplate) ac.getBean("hibernateTemplate"); template.execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { // do the work return null; } }); } }
This change makes the implementation of any potential DAO interfaces quite straightforward; the HibernateTemplate
instance is a Spring-managed bean, which means that we do not have to worry about creating new instances in our DAO implementations. The only reservation we may have is that setting up the implementation simply requires too much typing: we need to declare a property of type HibernateTemplate
and its setter in every DAO. This is why Spring contains the HibernateDaoSuppot
convenience superclass. This class gives us access to the HibernateTemplate
and the Hibernate Session
and SessionFactory
.
The convenient, abstract HibernateDaoSupport
superclass allows us to implement our DAO interfaces in Hibernate with as little code as possible. Let's take a look at the code in Listing 11-9, which shows the LogEntryDao
interface and its Hibernate implementation.
Example 11.9. LogEntryDao and Its Implementation
public interface LogEntryDao { void save(LogEntry logEntry); List<LogEntry> getAll(); } public class HibernateLogEntryDao extends HibernateDaoSupport implements LogEntryDao { public void save(LogEntry logEntry) { getHibernateTemplate().saveOrUpdate(logEntry); } @SuppressWarnings({"unchecked"}) public List<LogEntry> getAll() { return getHibernateTemplate().find("from LogEntry"); } }
You can see that the implementation is incredibly simple—all the code we really need to insert or update a LogEntry
object to the database is really getHibernateTemplate().saveOrUpdate(logEntry)
. Selecting all LogEntry
objects from the database is equally simple.
The @SuppressWarnings({"unchecked"})
annotation tells the javac
compiler not to report the unchecked operations warning. Hibernate does not support generics (and how could it know what classes to return without first parsing the query?) and neither does HibernateTemplate
. You need to unit and integration test your DAO code to make sure you are getting the correct objects back.
Listing 11-10 shows the only configuration needed to create the HibernateLogEntryDao
.
Example 11.10. Spring Context File for the HibernateLogEntryDao
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/j2ee/spring-jee-2.5.xsd"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/prospring2/ch11" expected-type="javax.sql.DataSource"/> <bean id="hibernateSessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingLocations"> <list> <value>classpath*:/com/apress/prospring2/ch11/dataaccess/ hibernate/*.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> org.hibernate.dialect.Oracle9Dialect </prop> </props> </property> </bean> <bean id="logEntryDao" class="com.apress.prospring2.ch11.dataaccess.hibernate. HibernateLogEntryDao"> <property name="sessionFactory" ref="hibernateSessionFactory"/> </bean> </beans>
We can now use the logEntryDao
as a standard Spring-managed bean. This configuration shows only one HibernateSupportDao
subclass; in real-world applications, having tens of such DAO implementations is not unusual. We could repeat the <property name="sessionFactory"> . . .
line in every DAO bean definition, but we could save even this bit of additional typing by declaring an abstract hibernateDaoSupport
bean and creating the real DAO subbeans (see Listing 11-11).
Example 11.11. Abstract hibernateDaoSupport Bean and Its Use
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" ....> <bean id="hibernateDaoSupport" abstract="true" class="org.springframework.orm.hibernate3.support.HibernateDaoSupport"> <property name="sessionFactory" ref="hibernateSessionFactory"/> </bean> <bean id="logEntryDao" class="com.apress.prospring2.ch11.dataaccess.hibernate.HibernateLogEntryDao" parent="hibernateDaoSupport"/> </beans>
This is exactly what we need: we can now add a new HibernateDaoSupport
subclass with as little Spring configuration as possible.
Recall that the JdbcTemplate
's work is twofold: it provides common resource management and translates the JDBC checked exceptions to the Spring data access exceptions. Hibernate 3 uses runtime exceptions, and Spring 2.5's @Repository
annotation gives you resource management similar to what you get in HibernateTemplate
. Therefore, we can say that, in most cases, using the HibernateTemplate
is not necessary at all. Consider the code in Listing 11-12.
Example 11.12. Using Hibernate SessionFactory Instead of HibernateTemplate
@Repository public class TemplatelessHibernateInvoiceLogEntryDao implements LogEntryDao { private SessionFactory sessionFactory; public TemplatelessHibernateInvoiceLogEntryDao(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public LogEntry getById(Long id) { return (LogEntry) this.sessionFactory.getCurrentSession(). get(LogEntry.class, id); } public void save(LogEntry logEntry) { this.sessionFactory.getCurrentSession().saveOrUpdate(logEntry); } public List<LogEntry> getAll() { return this.sessionFactory.getCurrentSession(). createQuery("from LogEntry").list(); } }
This clearly shows that we are using the SessionFactory
interface from Hibernate, and we are using its getCurrentSession
method. This method, according to the Hibernate documentation, returns a Session
bound to the current thread. Listing 11-13 shows how we use the TemplatelessHibernateInvoiceLogEntryDao
bean in a sample application.
Example 11.13. Using the templatelessLogEntryDao Bean
public class TemplatelessHibernateLogEntryDaoDemo { public static void main(String[] args) throws Exception { DaoDemoUtils.buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext( "datasource-context-dao.xml", DaoDemo.class); LogEntryDao logEntryDao = (LogEntryDao) ac.getBean("templatelessLogEntryDao"); logEntryDao.getAll(); } }
When we run the example in Listing 11-13, it fails:
Exception in thread "main" org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here.
The exception makes sense: the SessionFactory.getCurrentSession
method tries to return the Session
bound to the current thread, but there is no such Session
, and the SessionFactory
configuration does not include information about how to create one. We can tell Hibernate how to create a new thread-bound Session
(and which transaction manager to use) by modifying the hibernateSessionFactory
bean definition. Listing 11-14 shows the modified datasource-context-dao.xml
file.
Example 11.14. The Modified hibernateSessionFactory Bean
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" ...> ... <bean id="hibernateSessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingLocations"> <list> <value>classpath*:/com/apress/prospring2/ch11/dataaccess/ hibernate/*.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> org.hibernate.dialect.Oracle9Dialect </prop>
<prop key="hibernate.current_session_context_class"> thread </prop> <prop key="hibernate.transaction.factory_class"> org.hibernate.transaction.JDBCTransactionFactory</prop> </props> </property> </bean> ... </beans>
This change allows Hibernate to create a new thread-bound Session
in the call to getCurrentSession
. However, the application still fails when we call the createQuery
method; the exception is
org.hibernate.HibernateException: createQuery is not valid without active transaction
We will need to make one last modification to our code and include an explicit call to create a Hibernate
transaction. Listing 11-15 shows the new TemplatelessHibernateLogEntryDao
class.
Example 11.15. Modified Templateless DAO
@Repository public class TemplatelessHibernateInvoiceLogEntryDao implements LogEntryDao { ... public List<LogEntry> getAll() { Transaction transaction = this.sessionFactory.getCurrentSession(). beginTransaction(); try { return this.sessionFactory.getCurrentSession(). createQuery("from LogEntry").list(); } finally { transaction.commit(); } } }
The text in bold shows the code we needed to add to make the sample work. But let's not stop here. We need to take a look at the exception handling and translation. To do this, we will add the getByName(String)
method to the LogEntryDao
interface but make a deliberate mistake in its implementation. Both implementations of the LogEntryDao
interface (HibernateLogEntryDao
and TemplatelessLogEntryDao
) are going to run the same Hibernate code (see Listing 11-16).
Example 11.16. Incorrect Hibernate Code in the LogEntryDao Implementations
public class HibernateLogEntryDao extends HibernateDaoSupport implements LogEntryDao { ... public LogEntry getByName(final String name) { return (LogEntry) getHibernateTemplate().execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { return session. createQuery("from LogEntry where name = :name"). setParameter("name", name).
uniqueResult(); } }); } ... } @Repository public class TemplatelessHibernateInvoiceLogEntryDao implements LogEntryDao { private SessionFactory sessionFactory; ... public LogEntry getByName(String name) { Transaction transaction = this.sessionFactory.getCurrentSession(). beginTransaction(); try { return (LogEntry) this.sessionFactory.getCurrentSession(). createQuery("from LogEntry where name = :name"). setParameter("name", name). uniqueResult(); } finally { transaction.commit(); } } ... }
You can see that the code in bold is the same in both classes. However, when we run it, with more than one row with the same value in the name_
column, we get the following exception when we use the HibernateTemplate
:
org.springframework.dao.IncorrectResultSizeDataAccessException: query did not return a unique result: 2
However, we get the following one when we use the Session
directly:
org.hibernate.NonUniqueResultException: query did not return a unique result: 2
Obviously, there was no exception translation. However, we can add a bean post processor that will instruct the framework to intercept all calls to the beans annotated with @Repository
and perform the standard exception translation. Listing 11-17 shows the one-line post processor definition.
Example 11.17. The Exception Translator Bean Post Processor
<bean class="org.springframework.dao.annotation. PersistenceExceptionTranslationPostProcessor"/>
With this bean in place, we can run TemplatelessHibernateLogEntryDaoDemo
, and even though we are not using the HibernateTemplate
at all, we still get the Spring data access exception when we call the getByName
method.
You can see that your DAO implementation does not need to use hardly any Spring code at all, and when you use the Spring declarative transaction management and your DAO templateless bean implementation in a transactional context, you do not even need to worry about the explicit Hibernate transaction management code. Even though this approach makes your Hibernate DAO classes almost completely independent of Spring (save for the @Repository
annotation), we still favor the HibernateTemplate
approach. Using HibernateTemplate
makes your code framework dependent, but chances are that you will not need to change the framework in the life cycle of the application.
The code in the examples in the previous section is perfectly functional Hibernate code. In fact, you can take the code we have written and use it in almost any enterprise application. But it will not work as efficiently as we would like.
The first problem is that we have done nothing to prevent updates of stale data. The code we have will simply update the rows in the database without any checks.
Next, we do not consider transactional behavior. Without any additional code, the code in the callback supplied to the doInHibernate
method does not automatically run in a transaction. Enterprise applications certainly need to support transactions in the traditional sense of the word, conforming to the ACID (atomicity, consistency, independence, and durability) rules. Moreover, enterprise applications often include other resources (a JMS queue, for example) in a transaction.
Next, the examples we have given work with LogEntry
objects. The LogEntry
class does not have any associations; it can be fully constructed using data from a single database row. Enterprise applications usually manipulate complex objects with many associations. Handling these associations efficiently is important; otherwise, you may end up with extremely complex SQL statements.
Finally, if the t_log_entry
table contained hundreds of thousands of rows, the examples we gave would simply crash with a java.lang.OutOfMemoryException
. Real applications need to be able to deal efficiently and elegantly with very large data sets.
In this section, we will take a look at how we can solve each of these problems in a way that is as close to real-world application code as possible.
Table 11-1 shows a scenario in which our application can overwrite data updated by another user.
Table 11.1. Stale Data Updates Scenario
Time | Thread A | Thread B | Database |
---|---|---|---|
0 |
|
| |
1 |
|
| |
2 |
|
| |
3 |
|
| |
4 |
|
| |
5 |
|
|
The critical operation happens in Thread B at time 5. Thread B is allowed to overwrite an update performed by Thread A at time 3. This is most likely a problem, because Thread B thinks it is changing Name
to Z
, while in fact it is changing X
to Z
. In this particular case, it is not a major problem, but if you replace the name
column for account_balance
, the problem becomes much more serious.
There are several ways to prevent this situation: Thread A can lock the entire row so that Thread B cannot read it until Thread A has finished updating it. This approach is called pessimistic locking—we believe that problems will happen so we lock the row to guarantee exclusive access to it. This approach will prevent the problem described in Table 11-1, but it will also introduce a significant performance bottleneck. If the application is performing a lot of updates, we may end up with the majority of the table locked, and the system will be forever waiting for rows to become unlocked.
If you perform more reads than writes, it is better to leave the rows unlocked and use optimistic locking. With optimistic locking, we do not explicitly lock the row for updates, because we believe that conflicts will not happen. To identify stale data, we add a version
column to the table and to our domain object. When we save, we check that the version in the database matches the version in our object. If it does, no other thread has modified the row we are about to update, and we can proceed. If the version in our object differs from the version of the row in the database, we throw an exception indicating that we attempted to save stale data. Hibernate makes optimistic locking very easy; Listing 11-18 shows the optimistic-locking-enabled LogEntry
object.
Example 11.18. Optimistic Locking LogEntry Object
public class LogEntry {
private Long id;
private String name;
private Date date;
private Long version;
// getters and setters
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
}
Next, take a look at Listing 11-19, where we tell Hibernate to perform optimistic locking checks on the LogEntry
object.
Example 11.19. Hibernate Mapping Configuration for the LogEntry Class
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping default-lazy="false">
<class name="com.apress.prospring2.ch11.domain.LogEntry" table="t_log_entry">
<id name="id" type="long" unsaved-value="null">
<generator class="sequence">
<param name="sequence">s_log_entry_id</param>
</generator>
</id>
<version name="version" column="version" unsaved-value="null" type="long" />
<property name="name" column="name_" not-null="true"/>
<property name="date" column="date_" not-null="true"/>
</class>
</hibernate-mapping>
The code in bold shows the only modification we needed to instruct Hibernate to perform optimistic-locking checks whenever we attempt to save the LogEntry
object. To test that Hibernate will do what we expect, we issue the SQL command alter table t_log_entry add version number(19, 0) null
and run the code in Listing 11-20.
Example 11.20. Testing the Optimistic Locking
public class VersioningDemo {
public static void main(String[] args) throws Exception {
DaoDemoUtils.buildJndi();
ApplicationContext ac =
new ClassPathXmlApplicationContext("datasource-context-dao.xml",
DaoDemo.class);
LogEntryDao logEntryDao = (LogEntryDao) ac.getBean("logEntryDao");
// save the original entry
LogEntry le = new LogEntry();
le.setName("Name");
le.setDate(Calendar.getInstance().getTime());
logEntryDao.save(le);
// load two instances of the same LogEntry object
LogEntry le1 = logEntryDao.getById(le.getId());
LogEntry le2 = logEntryDao.getById(le.getId());
// modify and save le1
le1.setName("X");
logEntryDao.save(le1);
// now, let's try to modify and save le2.
// remember, le2 represents the same row as le1
le2.setName("Z");
logEntryDao.save(le2);
}
}
The code simulates the behavior of the application from Table 11-1. It loads two LogEntry
objects that refer to the same row, makes a modification to one, and tries to modify the second one. However, once le1
is saved, le2
contains stale data, and the line in bold should not allow it to be persisted. When we run the application, we can see that that is exactly what happens:
* 1 logEntryDao.save(le1); DEBUG [main] AbstractBatcher.log(401) | update t_log_entry set version=?, name_=?, date_=? where id=? and version=? DEBUG [main] NullableType.nullSafeSet(133) | binding '1' to parameter: 1 DEBUG [main] NullableType.nullSafeSet(133) | binding 'X' to parameter: 2 DEBUG [main] NullableType.nullSafeSet(133) | binding '2007-10-12 10:35:06' to
parameter: 3 DEBUG [main] NullableType.nullSafeSet(133) | binding '1160' to parameter: 4 DEBUG [main] NullableType.nullSafeSet(133) | binding '0' to parameter: 5 ... * 2 logEntryDao.save(le2); DEBUG [main] AbstractBatcher.log(401) | update t_log_entry set version=?, name_=?, date_=? where id=? and version=? DEBUG [main] NullableType.nullSafeSet(133) | binding '1' to parameter: 1 DEBUG [main] NullableType.nullSafeSet(133) | binding 'Z' to parameter: 2 DEBUG [main] NullableType.nullSafeSet(133) | binding '2007-10-12 10:35:06' to parameter: 3 DEBUG [main] NullableType.nullSafeSet(133) | binding '1160' to parameter: 4 DEBUG [main] NullableType.nullSafeSet(133) | binding '0' to parameter: 5 ... ERROR [main] AbstractFlushingEventListener.performExecutions(301) | Could not synchronize database state with session org.hibernate.StaleObjectStateException: Row was updated or deleted by anothery transaction (or unsaved-value mapping was incorrect): [com.apress.prospring2.ch11.domain.LogEntry#1160] at org.hibernate.persister.entity.AbstractEntityPersister. check(AbstractEntityPersister.java:1765) ... at com.apress.prospring2.ch11.dataaccess.VersioningDemo. main(VersioningDemo.java:34)
The line marked * 1
in the output shows the point where we call logEntryDao.save(le1)
. The update operation includes both the primary key (id
) and the version. The version gets increased on update, and Hibernate checks that exactly one row has been modified. We try to run logEntryDao.save(le2)
at the line marked * 2
. We see a similar update statement, but the version in the database is now set to 1
. Therefore, the update affects zero rows, and at that point, Hibernate throws the StaleObjectStateException
.
Now that we can safely prevent stale data updates, we need to consider another important limitation. When we persist collections in Hibernate, we are likely to use some kind of Collection
. When we are modeling a 1-to-n relationship, we are most likely to use a Set
. A Set
is a collection that does not guarantee order of elements and does not allow duplicate elements; this is precisely what a 1-to-n relationship in a database represents. We will be adding our domain objects to the Set
s, so we need to consider their equality. We can implement natural equality or database equality. Natural equality means that two objects are equal if they contain the same data from the application logic point of view. Consider the domain object in Listing 11-21.
Example 11.21. The Customer Domain Object
public class Customer { private Long id; private String title; private String firstName; private String lastName; private String address; // getters and setters }
When should we consider two Customer
objects equal? Is it when their IDs are equal or when their title
, firstName
, lastName
, and address
fields are equal, regardless of the value of id
? When it comes to data persistence, we favor the first approach. The database does not care if it contains two rows with all other columns set to the same value as long as the rows' primary keys are unique and the rows do not violate any other constraints. If you need to enforce natural equality, consider using a unique index. With this in mind, we can take a look at the implementation of the equals
and hashCode
methods in our LogEntry
class; see Listing 11-22.
Example 11.22. LogEntry equals and hashCode Implementation
public class LogEntry { // same as before public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; //noinspection CastToConcreteClass final LogEntry that = (LogEntry) o; if (this.id != null ? !this.id.equals(that.id) : that.id != null) return false; //noinspection RedundantIfStatement if (this.version != null ? !this.version.equals(that.version) : that.version != null) return false; return true; } public int hashCode() { int result = super.hashCode(); result = 31 * result + (this.version != null ? this.version.hashCode() : 0); result = 31 * result + (this.id != null ? this.id.hashCode() : 0); return result; } }
We have implemented database equality with a twist: two objects are equal if they have the same id
and the same version
. There is one slight problem with this: it is likely that our domain will contain many other classes, not just LogEntry
. We will therefore refactor the code, move the common implementation of equals
and hashCode
to a new class, and have LogEntry
extend the new class. Listing 11-23 shows this refactoring.
Example 11.23. Refactored LogEntry Class
public abstract class AbstractIdentityVersionedObject<T> implements Serializable { protected Long version; protected T id; public AbstractIdentityVersionedObject() { } public AbstractIdentityVersionedObject(T id) { this.id = id; } protected final boolean idEquals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; //noinspection CastToConcreteClass final AbstractIdentityVersionedObject that = (AbstractIdentityVersionedObject) o;
if (this.id != null ? !this.id.equals(that.id) : that.id != null) return false; //noinspection RedundantIfStatement if (this.version != null ? !this.version.equals(that.version) : that.version != null) return false; return true; } protected final int idHashCode() { int result = super.hashCode(); result = 31 * result + (this.version != null ? this.version.hashCode() : 0); result = 31 * result + (this.id != null ? this.id.hashCode() : 0); return result; } @Override public boolean equals(final Object o) { return idEquals(o); } @Override public int hashCode() { return idHashCode(); } public T getId() { return id; } public void setId(final T id) { this.id = id; } public Long getVersion() { return version; } public void setVersion(final Long version) { this.version = version; } } public class LogEntry extends AbstractIdentityVersionedObject<Long> { private String name; private Date date; public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public String getName() { return name;
} public void setName(String name) { this.name = name; } }
The AbstractIdentityVersionedObject
class contains the id
and version
properties (and the id
is a generic type, which allows us to use any type for the primary key value, even a compound value!). It also implements database equality. The LogEntry
object simply extends AbstractIdentityVersionedObject<Long>
and adds only the columns it declares. This makes our domain code much more readable, and we do not have to worry about forgetting to implement the equals
and hashCode
methods in a new domain class. However, even though this code breaks the usual contract for the equals
and hashCode
methods, it is useful for objects that you expect to persist in a database. It assumes that an id
with the value null
represents an object that you have not yet inserted. Additionally, it assumes that you will not explicitly modify the values of the id
and version
properties.
This forms a good starting point for our discussion of lazy loading, but before we get to that, we need to take a look at one other crucial enterprise requirement.
The next area we need to look at is the transactional behavior of Hibernate support. Spring uses the PlatformTransactionManager
interface in its transactional support; for use with Hibernate, we have the HibernateTransactionManager
implementation. This bean needs the SessionFactory
reference; Listing 11-24 shows its configuration.
Example 11.24. Transactional Support in Hibernate
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
....>
<bean id="hibernateDaoSupport" abstract="true"
class="org.springframework.orm.hibernate3.support.HibernateDaoSupport">
<property name="sessionFactory" ref="hibernateSessionFactory"/>
</bean>
<bean id="logEntryDao"
class="com.apress.prospring2.ch11.dataaccess.hibernate.HibernateLogEntryDao"
parent="hibernateDaoSupport"/>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="hibernateSessionFactory"/>
</bean>
</beans>
We now have the transactionManager
bean, which allows us to control Hibernate transactions using the common Spring transaction framework. However, declaring only the transactionManager
bean does not mean we get transactional behavior, as code in Listing 11-25 shows.
Example 11.25. Still Nontransactional Behavior
public class HibernateLogEntryDaoTx1Demo { private static LogEntry save(LogEntryDao dao, String name) { LogEntry le = new LogEntry(); le.setName(name); le.setDate(Calendar.getInstance().getTime()); dao.save(le); return le; } public static void main(String[] args) throws Exception { DaoDemoUtils.buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext("datasource-context-dao.xml", DaoDemo.class); LogEntryDao logEntryDao = (LogEntryDao) ac.getBean("logEntryDao"); try { save(logEntryDao, "Hello, this works"); save(logEntryDao, null); } catch (Exception e) { // we don't want anything here, but Alas! System.out.println(logEntryDao.getAll()); } } }
The first Hibernate operation in the save
method succeeds, but the second one fails. Because we have not defined the boundary of the transaction, we will find one row in the catch block. We need to rethink our transaction strategy at this point: we could make the HibernateLogEntryDao.save(LogEntry)
method transactional, but that would not give us any benefit. In fact, making a single DAO method transactional is not a good practice—it is up to the service layer to define transactional boundaries. The DAO layer should be a relatively simple translator between domain objects and some data store.
Let's create a simple service interface and its implementation. The service implementation will use the log, and we will ensure that the service method is transactional. Figure 11-2 shows a diagram of the service we will create.
Figure 11-2 shows that the code in the SampleServiceDemo
example does not use the LoggingSampleService
implementation of the SampleService
; instead, it uses a dynamically generated proxy. This proxy, in turn, uses the HibernateTransactionManager
to maintain the transactional behavior of the work()
method. The LogEntryDao
and its HibernateLogEntryDao
implementation are shown for completeness. For further discussion on AOP and transactions, see Chapters 5, 6, and 16.
Listing 11-26 shows the service interface, the implementation, and the Spring context file.
Example 11.26. The Service Layer and Its Configuration
public interface SampleService {
void work();
}
public class LoggingSampleService implements SampleService {
private LogEntryDao logEntryDao;
private void log(String message) {
LogEntry entry = new LogEntry();
entry.setDate(Calendar.getInstance().getTime());
entry.setName(message);
this.logEntryDao.save(entry);
}
public void work() {
log("Begin.");
log("Processing...");
if (System.currentTimeMillis() % 2 == 0) log(null);
log("Done.");
}
public void setLogEntryDao(LogEntryDao logEntryDao) {
this.logEntryDao = logEntryDao;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="work"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="sampleServiceOperation"
expression="execution(* com.apress.prospring2.ch11.service.
SampleService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="sampleServiceOperation"/>
</aop:config>
<bean id="sampleService"
class="com.apress.prospring2.ch11.service.LoggingSampleService">
<property name="logEntryDao" ref="logEntryDao"/>
</bean>
</beans>
Looking at the code of the LoggingSampleService
, we can see that we deliberately try to save the LogEntry
object with a null name, which causes an exception. The advice catches the exception and rolls back the transaction and, therefore, the first two log entries. If the timing is favorable, we do not insert the LogEntry
object with null name and insert only the final LogEntry
object. So, for a successful call, we should get three rows in the t_log_entry
table, and we should get no rows for a failed call. Let's verify this in code (see Listing 11-27) as well as in SQL*Plus client.
Example 11.27. Using the SampleService
public class SampleServiceDemo { public static void main(String[] args) throws Exception { DaoDemoUtils.buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext(new String[] { "classpath*:/com/apress/prospring2/ch11/dataaccess/ datasource-context-tx.xml", "classpath*:/com/apress/prospring2/ch11/service/*-context.xml" }); SampleService sampleService = (SampleService)ac.getBean("sampleService"); LogEntryDao logEntryDao = (LogEntryDao) ac.getBean("logEntryDao"); int successCount = 0; int failureCount = 0; int before = logEntryDao.getAll().size(); for (int i = 0; i < 10; i++) { if (tryWork(sampleService)) { successCount++; } else { failureCount++; } } System.out.println("Inserted " + (logEntryDao.getAll().size() - before) + ", for " + successCount + " successes and " + failureCount + " failures"); } private static boolean tryWork(SampleService sampleService) { try { sampleService.work(); return true; } catch (Exception e) { // do nothing (BAD in production) } return false; } }
The result of running this class varies every time, but the numbers of inserted objects match the expected results:
Inserted 15, for 5 successes and 5 failures
We can also verify this in SQL*Plus using the code shown in Listing 11-28.
Example 11.28. Further Verification of the Transaction Code
$ sqlplus PROSPRING/x*****6@//oracle.devcake.co.uk/INTL SQL> truncate table t_log_entry 2 / Table truncated. SQL> exit $ java -cp $CLASSPATH com.apress.prospring2.ch11.service.SampleServiceDemo ... output truncated ... Inserted 15, for 5 successes and 5 failures $ sqlplus PROSPRING/x*****6@//oracle.devcake.co.uk/INTL SQL> select count(0) from t_log_entry 2 / COUNT(0) ---------- 15
It worked! We have run the sample application and verified that for five successful service calls we have 15 rows in the t_log_entry
table, exactly as we expected.
This section is just as important as the transactional behavior one; in fact, these two areas are closely connected. The principle of lazy loading is simple: fetch data only when it is needed. It means more trips to the database, but the throughput is better because Hibernate only fetches data the application needs. Lazy loading generally applies to associated collections; imagine an Invoice
object with a Set
of InvoiceLine
objects; each InvoiceLine
will have a Set
of Discount
objects.
Let's consider an application that loads all invoices in a particular date range, for example. Imagine that the number of matching records is 10,000; that means that we will get 10,000 Invoice
objects. On average, we can expect an Invoice
to have five lines; that means 50,000 InvoiceLine
objects. In addition to that, let's say that every other line has a discount applied to it. That means 25,000 Discount
objects. In total, a single Hibernate operation will create 85,000 objects! By using lazy loading, we can reduce the number of objects created to just 10,000; we instruct Hibernate to fetch the InvoiceLine
objects only when we need them. Hibernate does this by instrumenting the fetched objects' code and using its own implementations of the Collection
interfaces.
In theory, it should work quite nicely; let's write code that implements the model from Figure 11-3.
The domain objects are not difficult to implement; the only thing we want to show in Listing 11-29 are the Invoice.addInvoiceLine
and InvoiceLine.addDiscount
methods.
Example 11.29. The addInvoiceLine and addDiscount Methods
public class Invoice extends AbstractIdentityVersionedObject<Long> { public void addInvoiceLine(InvoiceLine invoiceLine) { invoiceLine.setInvoice(this); this.invoiceLines.add(invoiceLine); } } public class InvoiceLine extends AbstractIdentityVersionedObject<Long> { public void addDiscount(Discount discount) { discount.setInvoiceLine(this); this.discounts.add(discount); } }
This code clearly shows that the addInvoiceLine
and addDiscount
methods establish a bidirectional relationship between the object being added and its container. This is important for Hibernate, and it makes our code clearer; it is better and much less error-prone than the code in Listing 11-30.
Example 11.30. Avoiding the Error-Prone addInvoiceLine and addDiscount Methods
Invoice invoice = new Invoice(); InvoiceLine il = new InvoiceLine(); invoice.getInvoiceLines().add(il); // 1 Discount discount = new Discount(); discount.setInvoiceLine(invoiceLine); //2 invoiceLine.getDiscounts().add(discount); //3
The line marked //1
represents a bug: the il
instance we've added to the invoice
object does not contain a reference to the invoice object. Lines //2
and //3
are simply clumsy code. Even though the tables for the Invoice
domain model are straightforward, Listing 11-31 shows the SQL code to create them.
Example 11.31. SQL Code to Create the Invoice Domain Model Tables
create table t_supplier ( id number(19, 0) not null, version number(19, 0) null, name varchar2(200) not null, constraint pk_supplier primary key (id) ) / create sequence s_supplier_id start with 10000 / create table t_invoice ( id number(19, 0) not null, version number(19, 0) null, invoice_date date not null, delivery_date date not null, supplier number(19, 0) not null, constraint pk_invoice primary key (id), constraint fk_i_supplier foreign key (supplier) references t_supplier(id) ) / create sequence s_invoice_id start with 10000 / create table t_invoice_line ( id number(19, 0) not null, version number(19, 0) null, invoice number(19, 0) not null, price number(20, 4) not null, vat number(20, 4) not null, product_code varchar2(50) not null, constraint pk_invoice_line primary key (id), constraint fk_il_invoice foreign key (invoice) references t_invoice(id) ) /
create sequence s_invoice_line_id start with 10000 / create table t_discount ( id number(19, 0) not null, version number(19, 0) null, invoice_line number(19, 0) not null, type_ varchar2(50) not null, amount number(20, 4) not null, constraint pk_discount primary key (id), constraint fk_d_invoice_line foreign key (invoice_line) references t_invoice_line(id) ) / create sequence s_discount_id start with 10000 /
Because we will implement the DAOs in Hibernate, we have to create the Hibernate mapping files for our new domain objects. It is usual practice to keep one mapping file for each domain object; Listing 11-32, therefore, shows four mapping files (for the Supplier
, Invoice
, InvoiceLine
, and Discount
objects).
Example 11.32. Mapping Files for the Newly Created Domain Objects
<!—Supplier.hbm.xml --> <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping default-lazy="true"> <class name="com.apress.prospring2.ch11.domain.Supplier" table="t_supplier"> <id name="id" type="long" unsaved-value="null"> <generator class="sequence"> <param name="sequence">s_supplier_id</param> </generator> </id> <version name="version" column="version" unsaved-value="null" type="long" /> <property name="name" column="name" not-null="true" /> </class> </hibernate-mapping> <!—Invoice.hbm.xml --> <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping default-lazy="true"> <class name="com.apress.prospring2.ch11.domain.Invoice" table="t_invoice"> <id name="id" type="long" unsaved-value="null"> <generator class="sequence"> <param name="sequence">s_invoice_id</param> </generator>
</id> <version name="version" column="version" unsaved-value="null" type="long" /> <property name="deliveryDate" column="delivery_date" not-null="true" /> <property name="invoiceDate" column="invoice_date" not-null="true" /> <many-to-one name="supplier" not-null="true" class="com.apress.prospring2.ch11.domain.Supplier"/> <set name="lines" cascade="all" inverse="true"> <key column="invoice" not-null="true"/> <one-to-many class="com.apress.prospring2.ch11.domain.InvoiceLine"/> </set> </class> </hibernate-mapping> <!—InvoiceLine.hbm.xml --> <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping default-lazy="true"> <class name="com.apress.prospring2.ch11.domain.InvoiceLine" table="t_invoice_line"> <id name="id" type="long" unsaved-value="null"> <generator class="sequence"> <param name="sequence">s_invoice_line_id</param> </generator> </id> <version name="version" column="version" unsaved-value="null" type="long" /> <property name="price" column="price" not-null="true" /> <property name="productCode" column="product_code" not-null="true" /> <property name="vat" column="vat" not-null="true" /> <many-to-one name="invoice" class="com.apress.prospring2.ch11.domain.Invoice" not-null="true"/> <set name="discounts" inverse="true" cascade="all"> <key column="invoice_line" not-null="true"/> <one-to-many class="com.apress.prospring2.ch11.domain.Discount"/> </set> </class> </hibernate-mapping> <!—Discount.hbm.xml --> <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping default-lazy="true"> <class name="com.apress.prospring2.ch11.domain.Discount" table="t_discount"> <id name="id" type="long" unsaved-value="null"> <generator class="sequence"> <param name="sequence">s_discount_id</param> </generator>
</id> <version name="version" column="version" unsaved-value="null" type="long" /> <property name="amount" column="amount" not-null="true" /> <property name="type" column="type_" not-null="true" /> <many-to-one name="invoiceLine" column="invoice_line" class="com.apress.prospring2.ch11.domain.InvoiceLine" not-null="true" /> </class> </hibernate-mapping>
The listing shows the simplest mapping first: Supplier
has no references to any other objects. The mapping for the Invoice
object is quite complex: it shows a many-to-one mapping to the Supplier
object and the lines set, which represents one-to-many mapping to the InvoiceLine
objects. The InvoiceLine
and Discount
mappings follow the same pattern we have in the mapping for the Invoice
object. Also notice that the default-lazy
attribute of the hibernate-mapping
element is set to true
.
Now that we have the domain objects with convenience methods to set the dependencies and their Hibernate mappings, we need to create the appropriate DAOs and services. Figure 11-4 shows the UML diagram of the code we will write.
Finally, let's modify the dataaccess-context-tx.xml
and create the services-context.xml
Spring configuration files to wire up the new DAOs and services. We will use the code in Listing 11-33 in a sample application.
Example 11.33. The Changed dataaccess-context-tx.xml and New services-context.xml Files
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" ...> <bean id="invoiceDao" class="com.apress.prospring2.ch11.dataaccess.hibernate.HibernateInvoiceDao" parent="hibernateDaoSupport"/> <bean id="supplierDao" class="com.apress.prospring2.ch11.dataaccess.hibernate.HibernateSupplierDao" parent="hibernateDaoSupport"/> </beans> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" ....> <bean id="invoiceService" class="com.apress.prospring2.ch11.service.DefaultInvoiceService"> <property name="invoiceDao" ref="invoiceDao"/> </bean> <bean id="supplierService" class="com.apress.prospring2.ch11.service.DefaultSupplierService"> <property name="supplierDao" ref="supplierDao"/> </bean> </beans>
We have the Spring context files, and we are now ready to try to run the sample application shown in Listing 11-34. The sample application will only verify that the Spring configuration is correct; it does not actually do any database work yet.
Example 11.34. Sample Application
public class InvoiceServiceDemo { private SupplierService supplierService; private InvoiceService invoiceService; private void run() throws Exception { DaoDemoUtils.buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext(new String[] { "classpath*:/com/apress/prospring2/ch11/dataaccess/ datasource-context-tx.xml", "classpath*:/com/apress/prospring2/ch11/service/*-context.xml" }); this.invoiceService = (InvoiceService)ac.getBean("invoiceService"); this.supplierService = (SupplierService)ac.getBean("supplierService"); findAllInvoices(); }
private void findAllInvoices() { } public static void main(String[] args) throws Exception { new InvoiceServiceDemo().run(); } }
We will work on the findAllInvoices()
method to find out what Hibernate does behind the scenes. We will begin by creating some sample data by running the code in Listing 11-35.
Example 11.35. Stored Procedure to Create the Sample Data
CREATE OR REPLACE PROCEDURE "PROSPRING"."CREATE_SAMPLE_DATA" is i number; j number; l number; begin dbms_output.put_line('begin'), for i in 1 .. 50 loop insert into t_supplier (id, version, name) values ( s_supplier_id.nextval, 1, 'Supplier '||i); for j in 1 .. 100 loop insert into t_invoice (id, version, invoice_date, delivery_date, supplier) values (s_invoice_id.nextval, 1, sysdate, sysdate, s_supplier_id.currval); for l in 1 .. 5 loop insert into t_invoice_line (id, version, invoice, price, vat, product_code) values (s_invoice_line_id.nextval, 1, s_invoice_id.currval, dbms_random.value(1, 1000), dbms_random.value(1, 100), 'Product '||l); end loop; end loop; end loop; end;
This code simply inserts 50 suppliers; each invoice has 100 invoices; and each invoice, five lines. This should give us a representative data set for our experiments. Let's modify the findAllInvoices()
method to actually go to the database and return all Invoice
objects (see Listing 11-36).
Example 11.36. Finding All Invoices
private void findAllInvoices() { List<Invoice> invoices = this.invoiceService.findAll(); System.out.println(invoices.size()); }
We expect this code to print 5,000 lines, and running it indeed prints out 5,000. This is perhaps the most inefficient way to count the rows in the t_invoice
table. The benefit, however, is that we have not loaded 25,000 InvoiceLine
objects! Hibernate runs the SQL statement only on the t_invoice
table:
select invoice0_.id as id1_, invoice0_.version as version1_, invoice0_.delivery_date as delivery3_1_, invoice0_.invoice_date as invoice4_1_, invoice0_.supplier as supplier1_ from t_invoice invoice0_
Let's write a method that gets an invoice
by its id
, the InvoiceService.findById(Long)
method, and examine the structure of the Invoice
object we get back. Figure 11-5 shows the debugger view of the returned Invoice
object.
The debugger shows that we have successfully loaded the Invoice
object, but it also shows that when we try to access the supplier
and lines
properties, we get the LazyInitializationException
. The reason is that the Session
that has loaded the Invoice
object has ended. Remember that the HibernateTemplate
ensures that the session closes correctly at the end of the work performed by the callback. This is where the Hibernate transactional support comes into play. Whenever application code requests Spring Hibernate support to get a session, Spring registers the Session
as a transaction synchronization object. Therefore, code that executes in a single transaction always operates under the same Session
, even if it uses more than one HibernateTemplate
call. You must, however, be careful with how you deal with the transactional nature of your beans. Take a look at Table 11-2, which shows an application that declared its service layer and DAO layer to be transactional and, to make matters even worse, it has declared that operations on both the DAO and the service beans require a new transaction.
Table 11.2. Nested Transactions
Service Call | DAO Call | Hibernate Session |
---|---|---|
| Session 1 | |
| Session 2 | |
| Session 2 | |
| Session 1 |
Because we have configured the service bean with the REQUIRES_NEW
transaction propagation, the service call starts a new transaction and gets Hibernate session one. It then proceeds to call the DAO's getId
method. Because we have configured the DAO bean with the REQUIRES_NEW
transaction propagation for every call, it gets Hibernate session two. Using session two, the DAO bean loads the Invoice
object: i.getSupplier()
will work, because i
was loaded in session two. However, when the DAO bean returns back to the service layer, the call to i.getSupplier
in the service call will fail. Even though we still have a Hibernate Session
open, it is not the same Session
that loaded the original Invoice
object. Do not worry if you do not fully understand the transaction propagation, we cover this in more detail in Chapter 16.
You can argue that you would not make such a mistake, but imagine that the service call is actually a web tier call and that the DAO call is the service call. Suddenly, you are dealing with the same situation. The problem becomes even worse, because all your integration and DAO tests will work. The solution we take is to perform an explicit eager fetch when we need it in the DAO or to access the objects we need in an active transaction in the service layer. An eager fetch is the opposite of lazy fetch: we instruct Hibernate to select the associations in a single select
statement. The service call should always return all data that the nontransactional presentation tier needs.
Explicit Eager Fetching
Let's take a look at the first way to prevent lazy loading exceptions. If we expect that the subsequent layers will require the entire object graph, we can instruct Hibernate to eagerly fetch all objects. This is frequently useful when implementing DAO calls—typically, the getById
calls—that return a single result, or a very small number of results. Listing 11-37 shows the modification to the HibernateInvoiceDao
's getById()
method.
Example 11.37. Eager getById Method
public class HibernateInvoiceDao extends HibernateDaoSupport implements InvoiceDao { // other methods omitted public Invoice getById(Long id) { return (Invoice) DataAccessUtils.uniqueResult( getHibernateTemplate().find("from Invoice i inner join fetch" + "i.supplier inner join fetch i.lines il " + "left outer join fetch il.discounts where i.id = ?", id) ); } }
The eager fetch looks just like a SQL statement with explicit inner and left-outer joins. Figure 11-6 shows the Invoice
object we get back in the debugger view.
This is exactly what we need: we have a DAO that returns an eagerly fetched object only when we need it.
Other Lazy Loading Considerations
Even if your code is running in a clean service layer transaction, you should consider the performance hit caused by using lazy loading. Consider the code in Listing 11-38, where the InvoiceDao.getByIdLazy(Long)
method returns the Invoice
object without eager fetching.
Example 11.38. Very Slow Performance of Lazy Loading
public class Invoice extends AbstractIdentityVersionedObject<Long> { ... public BigDecimal getLinesTotalPrice() { BigDecimal total = new BigDecimal(0); for (InvoiceLine line : this.lines) { total = total.add(line.getPrice()); } return total; } ... } public class DefaultInvoiceService implements InvoiceService { // rest of the code omitted public void recalculateDiscounts(Long id) { Invoice invoice = this.invoiceDao.getByIdLazy(id); BigDecimal total = invoice.getLinesTotalPrice(); if (total.compareTo(BigDecimal.TEN) > 0) { // do something special } } }
The seemingly innocent call to invoice.getLinesTotalPrice()
forces Hibernate to fetch all InvoiceLines
for this invoice.
Some applications use the Open Session in View (anti)pattern. The reasoning behind this pattern is that the application keeps the Session
open while it displays a view (a JSP page, for example). This simplifies the decision between lazy and eager fetches: the session is open in the view, and it can fetch any lazy association. We sometimes call this an antipattern because it may lead to inconsistent data views. Imagine you display the Invoice
object and show only a count of its invoice lines. You then rely on lazy loading to get the invoice lines. In the meantime, the system (or other users) might have updated the invoice lines of the displayed invoice, thus making the actually lazily loaded InvoiceLine
objects inconsistent with the count.
Most applications need to be able to handle very large data sets; the code you have seen so far works quite nicely with hundreds of records in the database, but when we start to approach larger record set sizes, our application may crash with a java.lang.OutOfMemoryException
—we may simply select too much data. Worse, a web application would most likely discard the vast majority of the result set to display only one page of results to the users. We need to make the DAO layer aware of paging. Hibernate supports paging in its Query
object; it provides the setFirstResult(int)
and setMaxResults(int)
methods. Our first attempt at implementing paging might look like the code in Listing 11-39.
Example 11.39. The First Attempt at Implementing Paging
public interface InvoiceService { List<Invoice> search(int firstResult, int pageSize); ... } public class DefaultInvoiceService implements InvoiceService { private InvoiceDao invoiceDao; public List<Invoice> search(int firstResult, int pageSize) { return this.invoiceDao.search(firstResult, pageSize); } .... } public interface InvoiceDao { ... List<Invoice> search(int firstResult, int pageSize); } public class HibernateInvoiceDao extends HibernateDaoSupport implements InvoiceDao { ... @SuppressWarnings({"unchecked"}) public List<Invoice> search(final int firstResult, final int pageSize) { return (List<Invoice>) getHibernateTemplate().execute( new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { Query query = session.createQuery("from Invoice"); query.setFirstResult(firstResult); query.setMaxResults(pageSize);
return query.list(); } }); } }
We can now use the search method and give it the first result and the maximum number of results to be returned. This certainly works, but the downside is that we can't return the maximum number of results. We can, therefore, show a link to only the next page; we can't show the total number of pages (or results). One solution would be to implement a method that would return the total number of rows in the Invoices
table. What if we made our search more complicated, perhaps by introducing conditions that can result in selecting only a subset of all rows? We would have to implement a matching counting method for every search method. In addition to this, the code that uses our service would have to explicitly call two methods. There is a better solution; we illustrate it with the UML diagram in Figure 11-7.
The search method performs all necessary search operations, taking the SearchArgumentSupport
subclass as its argument and returning SearchResultSupport
subclass. The returned SearchResultSupport
contains the page of fetched objects as well as the total number of results. In addition to containing the results, it implements Iterable<T>
. That means that we can use the ResultSupport
subclass in a JSP's c:forEach
loop (see Listing 11-40).
Example 11.40. JSP Page That Lists the Invoice Objects
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.w3.org/MarkUp/SCHEMA/xhtml11.xsd" xml:lang="en" > <head> <title>Result</title> </head> <body> <h2>Invoices found</h2> <c:forEach items="${invoices}" var="invoice"> ${invoice.invoiceDate} <!-- etc --> </c:forEach> </body> </html>
As you can see, no additional effort was needed in our JSP views; in fact, if we change the returned type from a Collection<T>
to the ResultSupport<T>
subclass later on in the development of a web application, we do not have to change the views in any way.
It is possible to use Hibernate to fetch objects from tables that use large objects (LOBs). There are two types of LOBs: character large binary objects (CLOBs) and binary large objects (BLOBs). CLOBs are usually mapped to String
, while BLOBs are usually mapped to byte[]
. Because the standard JDBC infrastructure does not deal with large binary objects, we must use an appropriate LobHandler
implementation for the database we are using. Take a look at Listing 11-41, which shows a table with BLOB and CLOB fields.
Example 11.41. Table with Large Object Columns
create table t_lob_test ( id number(19, 0) not null, version number(19, 0) null, text_content clob not null, binary_content blob not null, mime_type varchar2(200) not null, constraint pk_lob_test primary key (id) ) / create sequence s_lob_test_id start with 10000 /
This class is very simple, so its domain object is going to be very simple as well (see Listing 11-42).
Example 11.42. Domain Object for the t_lob_test Table
public class LobTest extends AbstractIdentityVersionedObject<Long> { private String textContent; private byte[] binaryContent; private String mimeType;
public String getTextContent() { return textContent; } public void setTextContent(String textContent) { this.textContent = textContent; } public byte[] getBinaryContent() { return binaryContent; } public void setBinaryContent(byte[] binaryContent) { this.binaryContent = binaryContent; } public String getMimeType() { return mimeType; } public void setMimeType(String mimeType) { this.mimeType = mimeType; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("LobTest { id=").append(this.id).append(", "); sb.append("textContent=").append(this.textContent).append(", "); sb.append("binaryContent="); for (int i = 0; i < this.binaryContent.length && i < 50; i++) { sb.append(String.format("%x", (int)this.binaryContent[i])); } sb.append("}"); return sb.toString(); } }
The only addition to the usual getters and setters is the toString()
method, but that is really only for our convenience. Next, we need to create the Hibernate mapping file for the LobTest
domain object. Listing 11-43 shows that we use subclasses of the AbstractLobType
: BlobByteArrayType
and ClobStringType
.
Example 11.43. Mapping File for the LobTest Domain Object
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping default-lazy="true"> <class name="com.apress.prospring2.ch11.domain.LobTest" table="t_lob_test"> <id name="id" type="long" unsaved-value="null"> <generator class="sequence"> <param name="sequence">s_lob_test_id</param>
</generator> </id> <version name="version" column="version" unsaved-value="null" type="long" /> <property name="binaryContent" column="binary_content" not-null="true" type="org.springframework.orm.hibernate3.support.BlobByteArrayType" /> <property name="textContent" column="text_content" not-null="true" type="org.springframework.orm.hibernate3.support.ClobStringType"/> <property name="mimeType" column="mime_type" not-null="true" /> </class> </hibernate-mapping>
The final complex part of using this domain object is setting up the Hibernate mapping and Spring configuration. We need to tell Hibernate how our database (Oracle 10g) handles LOBs. To do that, we create an instance of the LobHandler
implementation and reference it in the HibernateSessionFactoryBean
; Listing 11-44 shows how to do this.
Example 11.44. Hibernate Configuration for LOB Handling
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" ...> <bean id="nativeJdbcExtractor" class="org.springframework.jdbc.support.nativejdbc. SimpleNativeJdbcExtractor"/> <bean id="lobHandler" class="org.springframework.jdbc.support.lob.OracleLobHandler"> <property name="nativeJdbcExtractor" ref="nativeJdbcExtractor"/> </bean> <bean id="hibernateSessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="lobHandler" ref="lobHandler"/> <property name="mappingLocations"> <list> <value>classpath*:/com/apress/prospring2/ch11/dataaccess/ hibernate/*.hbm.xml</value> </list> </property> </bean> ... <bean id="lobTestDao" class="com.apress.prospring2.ch11.dataaccess.hibernate.HibernateLobTestDao" parent="hibernateDaoSupport"/> </beans>
The final difficulty we need to deal with is that the LOB support requires an active Spring transaction or a JTA transaction synchronization. Therefore, we must create a service layer interface and its implementation (remember, we do not ever want to make our DAOs transactional!). Listing 11-45 shows the service interface and its implementation.
Example 11.45. Service Code for the LOB Demonstration
public interface LobTestService { void save(LobTest lobTest); LobTest findById(Long id); } public class DefaultLobTestService implements LobTestService { private LobTestDao lobTestDao; public void save(LobTest lobTest) { this.lobTestDao.save(lobTest); } public LobTest findById(Long id) { return this.lobTestDao.getById(id); } public void setLobTestDao(LobTestDao lobTestDao) { this.lobTestDao = lobTestDao; } }
The Spring configuration for the service layer simply defines the LobTestService
bean with its dependencies; Listing 11-46 shows only the sample application that uses the service implementation and verifies that the LOB handling works as expected.
Example 11.46. LOB Sample Application
public class LobTestServiceDemo { private void run() throws Exception { DaoDemoUtils.buildJndi(); ApplicationContext ac = new ClassPathXmlApplicationContext(new String[] { "classpath*:/com/apress/prospring2/ch11/dataaccess/ datasource-context-tx.xml", "classpath*:/com/apress/prospring2/ch11/service/*-context.xml" }); LobTestService lobTestService = (LobTestService)ac.getBean("lobTestService"); LobTest lobTest = new LobTest(); lobTest.setTextContent("Hello, world"); lobTest.setBinaryContent("Hello, world".getBytes()); lobTest.setMimeType("text/plain"); lobTestService.save(lobTest); LobTest lobTest2 = lobTestService.findById(lobTest.getId()); System.out.println(lobTest2); } public static void main(String[] args) throws Exception { new LobTestServiceDemo().run(); new BufferedReader(new InputStreamReader(System.in)).readLine(); } }
Running this application produces output that verifies that the LobTest
object gets inserted and fetched without any problems:
DEBUG [main] ConnectionManager.cleanup(380) | performing cleanup DEBUG [main] ConnectionManager.closeConnection(441) | releasing JDBC connection [ (open PreparedStatements: 0, globally: 0) (open ResultSets: 0, globally: 0)] DEBUG [main] JDBCContext.afterTransactionCompletion(215) | after transa version 3.2.5ga ction completion DEBUG [main] ConnectionManager.afterTransaction(302) | transaction completed on session with on_close connection release mode; be sure to close the session to release JDBC resources! DEBUG [main] SessionImpl.afterTransactionCompletion(422) | after transaction completion LobTest { id=10004, textContent=Hello, world, binaryContent=48656c6c6f2c20776f726c64}
The only difficulty with this approach is that the LOB columns usually contain fairly large amounts of data, and unfortunately, setting lazy="true"
on the column does not work. Using the AbstractLobType
subclasses, Hibernate will always fetch the entire contents of the column. This is why, in our large applications, we tend to use Spring JDBC to handle the LOBs and Hibernate to handle all other data access code.
You can use Hibernate in almost every application; it can deal with complex object structures, and its lazy loading will decrease the performance of your application only slightly. There is one situation in which we found Hibernate more difficult to use efficiently: if you are dealing with tables with large objects and the large objects are significant in size (10kB and above). In that case, it is best to combine Hibernate with Spring-managed JDBC code. Even though Hibernate supports column-level lazy loading, support for this is not always possible for all column types and all databases.
There is nothing stopping you from implementing part of your DAO in Hibernate and part in JDBC. Because you still need to declare the DataSource
bean, you can use it in your Spring JDBC DAOs. In addition to this, both the Hibernate and JDBC DAO implementations participate in the same transaction. This is very useful but can lead to a problem when you insert a row with Hibernate, then use JDBC, and expect to see the inserted row. The HibernateTransactionManager
typically does not flush the Hibernate Session
until the transaction commits; therefore, the row we have inserted using session.saveOrUpdate()
will not appear in the database. This will cause the JDBC operation that expects to see the row to fail. The solution is to call session.flush
before performing JDBC DAO operations that depend on rows inserted as a result of the Hibernate session operations.
In this chapter, you have learned what Hibernate does and how to use it in Spring applications. We started by looking at the simplest code and quickly moved on to more complicated examples. Now, you know how to prevent stale data updates, and we paid special attention to creating efficient data access code. Lazy loading plays an important role in this, and to properly use lazy loading, you must understand how Hibernate and Spring cooperate in handling transactions.
Finally, we discussed how we can use Hibernate to store large object data in a database, even though, as you saw, it may not be the most appropriate solution. We will end the chapter by saying that the best combination is Hibernate with Spring-managed JDBC; this way, you can implement critical pieces of data access code in a very low-level way using JDBC and use Hibernate's incredible convenience and power for all other DAO code.
3.128.78.30