Step 7: Products Contain Components
Welcome back! I hope you feel refreshed. In the previous
section, we tested the Hibernate configuration for a single class.
Now we will test an area of Hibernate that takes many developers a
long time to become comfortable with: associations.
The basic association is that of a parent-child relationship.
For example, let's say that a product has several "components."
Here is the component in pseudo-code:
class no.brodwall.demo.domain.Component {
Product product;
String componentName;
Long id;
}
Now you need to modify Product, so that a product
has a Set of Components associated with
it. Just add getters and setters for a components set
property.
Again, be sure to create a separate unit test for all
functionality in the Component class. Pay special
attention to hashCode and equals. The
first time you do this, you probably will end up with a mutually
recursive call between Component.equals and
Product.equals. You don't want to do that.
Here is an example of a test of the domain objects:
public void testProductComponents() {
Product p1 = new Product("p1");
Component c1 = new Component("c1");
Component c2 = new Component("c2");
p1.addComponent(c1);
assertEquals("c1.parent", p1, c1.getProduct());
assertNull("c2.parent", c2.getProduct());
p1.addComponent(c2);
assertEquals("c2.parent", p1, c2.getProduct());
assertEquals("p1.components.size", 2, p1.getComponents().size());
}
Step 8: Extending the Hibernate Mapping Test
Now we know that the classes work, and we're ready to try out
the Hibernate mapping. But before we write it, let's test it. Add
the following test method to your ProductDAOTest:
public void testComponent() {
Product product = new Product("My product");
product.addComponent(new Component("c1"));
product.addComponent(new Component("c2"));
long id = productDAO.store(product);
Product retrivedProduct = productDAO.get(id);
assertEquals("retrievedProduct.components.size", 2,
retrievedProduct.getComponents().size());
}
In time, we'll probably want to expand on this, but it's good for
now. Here's the error: junit.framework.AssertionFailedError:
retrivedProduct.components.size expected:<2> but
was:<0>.
Pretty mysterious at first, but the answer is quite simple: if a
property is not listed in the Hibernate mapping, it's simply
omitted while writing to the database. So we have to include the
component set in the Product.hbm.xml mapping file.
This is what the file currently looks like:
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true"
package="no.brodwall.demo.domain">
<class name="Product">
<id name="id">
<generator class="native" />
</id>
<property name="productName"/>
</class>
</hibernate-mapping>
We can't just use <property> for the components
property. If we did, Hibernate would choke on the mapping file with
the following message: org.hibernate.MappingException: Could
not determine type for: java.util.Set, for columns:
[org.hibernate.mapping.Column(components)]. Fair
enough.
Step 9: Creating the One-to-Many Association
The Hibernate reference manual is excellent when it comes to
describing mapping relationships (see Chapter 8, "
Association Mappings "). What we want is a bidirectional
one-to-many association (Chapter 8.4.1). We need to make two
mapping files, then.
Here is the first,
/no/brodwall/demo/domain/Component.hbm.xml:
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true"
package="no.brodwall.demo.domain">
<class name="Component">
<id name="id">
<generator class="native" />
</id>
<property name="componentName"/>
<many-to-one name="parent"
column="parentId"
not-null="true" />
</class>
</hibernate-mapping>
And here is the second,
/no/brodwall/demo/domain/Product.hbm.xml:
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true"
package="no.brodwall.demo.domain">
<class name="Product">
<id name="id">
<generator class="native" />
</id>
<property name="productName"/>
<set name="components" cascade="all">
<key column="productId"/>
<one-to-many class="Component"/>
</set>
</class>
</hibernate-mapping>
This is taken directly from the Hibernate reference
documentation;
Chapter 8.4.1 .
So far, so good. We also have to make sure to include the
Component class in the list of classes known to
Hibernate in ProductDAOTest.setUp:
protected void setUp() throws Exception {
...
configuration.addClass(Product.class);
configuration.addClass(Component.class);
Now run it again, and testComponent fails with the
following: org.hibernate.LazyInitializationException: failed
to lazily initialize a collection of role:
no.brodwall.demo.domain.Product.components - no session or session
was closed.
Step 10: Eager Fetching
This was an error I struggled with for a long time when I tried
to migrate my code to Hibernate 3. As it turns out, the default
behavior for Hibernate 2.1 was to eagerly load associations. The
default with Hibernate 3 is to load them lazily. Lazy loading is
good, and we want to keep it, but just as an experiment, let's try
loading Component eagerly. Make the following change
in Component.hbm.xml:
<hibernate-mapping auto-import="true"
package="no.brodwall.demo.domain">
<class name="Product">
...
<set name="components" lazy="false"
cascade="all">
...
At this point, the tests should succeed. But this actually isn't the
right answer.
Step 11: Lazy Loading and Sessions
We want the relationship between Product and
Component to be lazily loaded, so remove the
lazy="false" attribute on the
Product.components, and let's try another way.
What we need to do is to ensure that the Hibernate session stays
open for the whole time between the call to
productDAO.get and the access of the collection. Here
is how to do this.
Spring's HibernateTemplate uses a clever mechanism
for getting the Hibernate Session (
HibernateTemplate.getSession ). If a session is associated
with the current thread, HibernateTemplate uses that
one, otherwise save and similar methods will open a
new session from the SessionFactory, execute its
commands, and close it again. When Hibernate is used in servlets, a
ServletFilter is set up to intercept all HTTP
requests, open the session, and then close it when the request is
done processing. This pattern is called "Open Session in View."
We're not running in a servlet but in a JUnit test. To
make the session stay open, we have to associate a session with the
current thread. We can do this by using the Spring class
SessionFactoryUtils. Change
ProductDAOTest.testComponent to the following:
public void testComponent() {
Product product = new Product("My product");
product.addComponent(new Component("c1"));
product.addComponent(new Component("c2"));
Long id = productDAO.store(product);
Session session = SessionFactoryUtils.
getSession(this.sessionFactory, true);
TransactionSynchronizationManager.
bindResource(this.sessionFactory,
new SessionHolder(session));
Product retrievedProduct = productDAO.get(id);
assertEquals("retrievedProduct.components.size",
2,
retrievedProduct.getComponents().size());
TransactionSynchronizationManager.
unbindResource(sessionFactory);
SessionFactoryUtils.
releaseSession(session, sessionFactory);
}
Notice that you will also need to change the
sessionFactory we initialize in ProductDAOTest.setUp
to an instance variable instead of a local variable.
The test should now run successfully, and lazy loading is
implemented. I would love to tell you what the deal is with the
TransactionSynchronizationManager, but I honestly don't know. It has to be present, though,
or we will still get LazyInitializationException.
Now it's time for a refactoring: opening and closing the session
is something that we want to do for all test methods, so it's a
good candidate for moving into setUp and
tearDown. After refactoring, the methods look like this:
protected void setUp() throws Exception {
Configuration configuration =
new Configuration();
configuration.setProperty(
Environment.DRIVER,
"org.hsqldb.jdbcDriver");
configuration.setProperty(
Environment.URL,
"jdbc:hsqldb:mem:ProductDAOTest");
configuration.setProperty(
Environment.USER, "sa");
configuration.setProperty(
Environment.DIALECT,
HSQLDialect.class.getName());
configuration.setProperty(
Environment.SHOW_SQL, "true");
configuration.setProperty(
Environment.HBM2DDL_AUTO, "create-drop");
configuration.addClass(Product.class);
configuration.addClass(Component.class);
this.sessionFactory =
configuration.buildSessionFactory();
HibernateTemplate hibernateTemplate =
new HibernateTemplate(sessionFactory);
productDAO.setHibernateTemplate(hibernateTemplate);
this.session = SessionFactoryUtils.
getSession(sessionFactory, true);
TransactionSynchronizationManager.
bindResource(sessionFactory,
new SessionHolder(session));
}
protected void tearDown() throws Exception {
TransactionSynchronizationManager.
unbindResource(sessionFactory);
SessionFactoryUtils.
releaseSession(session, sessionFactory);
}
Now something curious happens: testStoreRetrieve
from the first part of the article fails! This is the code:
public void testStoreRetrieve() {
Product product = new Product("My product");
Long id = productDAO.store(product);
Product retrievedProduct = productDAO.get(id);
assertEquals(product, retrievedProduct);
assertNotSame(product, retrievedProduct);
}
It fails because Hibernate's Session will ensure
that only one instance of the "My Product" Product exists per
session. We store and get the
Product in the same session, so get
returns the object we stored, and consequently
assertNotSame fails. We need to do more work.
Step 12: The Session Must Be Flushed
We saw that Hibernate ensures that two copies of the same data
loaded in the same session resolve to the same object instance.
This defeats the purpose of our test, as the object we save is
never really retrieved from the data store. The cache of data in
the session used to implement this is called Hibernate's "first-level cache" or "session cache."
To avoid getting retrievedProduct from the
session cache, we have to ask Hibernate to clear it, which we can do by calling HibernateSession.clear. We have to
make hibernateSession into an instance variable on the
test so we can call it from the test method. Here is the final
test:
public void testStoreRetrieve() {
Product product = new Product("My product");
Long id = productDAO.store(product);
hibernateTemplate.flush();
hibernateTemplate.clear();
Product retrievedProduct = productDAO.get(id);
assertNotSame(product, retrievedProduct);
assertEquals(product, retrievedProduct);
}
public void testComponent() {
Product product = new Product("My product");
product.addComponent(new Component("c1"));
product.addComponent(new Component("c2"));
Long id = productDAO.store(product);
hibernateTemplate.flush();
hibernateTemplate.clear();
Product retrievedProduct = productDAO.get(id);
assertNotSame(product, retrievedProduct);
assertEquals("retrievedProduct.components.size", 2,
retrievedProduct.getComponents().size());
}
The test now passes, and we have a complete pattern for testing to ensure
that our objects are persisted correctly.
Using this test class, you can test all kinds of contortions
with Hibernate mapping. There are many challenges to getting an
advanced mapping right, and having a test framework gives you a
good place to start.
Summary
Using the magic of Spring's Hibernate support, we were able to
test a parent-child relationship. To be able to fetch the lazy
association, we had to ensure that the Session stayed
open for the whole test method. To avoid using the same
Session, and therefore the same objects when reading and
writing, we had to be sure we flushed Hibernate's session cache
between writing and reading our objects.
Unit tests for Hibernate configuration are very useful whenever
the mapping or the mapped classes change. A properly written test
of the Hibernate configuration will help you if you
add, rename, or remove a field in the class but forget to
update the Hibernate mapping file. During development, testing the
configuration early can help you get the trickier bits of
Hibernate, such as inheritance and relationships, right from the
start, saving you lots of debugging effort down the line.
- Sample code for this article, developed using JUnit 3.8.1, Spring 1.2.5 and Hibernate
3.0.5
- Hibernate
- The JUnit
test framework
- Hypersonic (HSQLDB) : A pure
Java database that can run embedded in our test application.
- The Spring Framework : Provides a
set of classes that makes it a lot easier to write DAO
implementations.
- The GSBase library: Contains helper
classes to test the correctness of
equals and
hashCode.
Johannes Brodwall is currently lead Java architect at BBS, the company that
operates Norway's banking infrastructure