Object-relational mapping (O/R mapping) is a common requirement of many software development projects. The activities involved in persisting data are tedious and error-prone. If we also take into consideration the inevitable change of requirements, we're in serious trouble: the data storage structure must be kept in sync with the source code. Add the portability issue, and things are becoming very, very complicated.
Hibernate will help us to painlessly store the data in permanent storage without too much hassle about choosing the kind of storage, installation, or configuration. Hibernate allows us to store any kind of objects; therefore, our application does not need to know that its data will be persisted using Hibernate. Of course, everything mentioned here can be applied in the opposite direction: fetching already prepared objects from a storage is now nearly trivial. Updating and deleting data is also available.
Before You Get Started
Before you get started you'll need the Hibernate distribution, available on the Hibernate web site, www.hibernate.org. We'll use version 2.0.3. For a database, we'll use Hypersonic SQL version 1.7.1, available at hsqldb.sourceforge.net. Hibernate also supports many open source and commercial databases, such as MySQL, PostgreSQL, Oracle, DB2, etc. It is easy to set up this tutorial for any supported database. See the official documentation for the complete list.
Note: If you don't want your classes to be persisted in a database, but say, serialized, the Hibernate API provides you with the net.sf.hibernate.persister.EntityPersister
class and the net.sf.hibernate.persister.ClassPersister
interface. By subclassing or implementing them, you can write your own persister classes and simply use them as needed.
After downloading all of the needed packages, we'll have to set up our testing environment. Basically, all we need to do is to put downloaded .jar files into our CLASSPATH
. This includes hibernate2.jar from the Hibernate distribution and hsqldb.jar from Hypersonic's lib/ directory. Hibernate also requires quite a few additional libraries, all of them available in the <hibernate-dist>/lib directory. Not all .jars from that directory are needed, but it won't hurt if you simply use them all. Before we start investigating Hibernate, we'll first define our problem domain.
Note: Hibernate uses commons-logging
from Apache. It's a smart tool and it will silently use log4j
if found. Log4j
is an excellent logging library and we'll use it in this tutorial. If you don't already have it (and you should!) download it from the Log4j homepage and add to your CLASSPATH
. Use the sample log4j.properties provided by the Hibernate team and available in <hibernate-dist>/src.
So, You Have A Problem?
Every developer has had to perform something like the following task at least once: create an Order
, put some Product
s in it that then become OrderItem
s, and then save the Order
.
We'll use these simple SQL commands to set up our database:
CREATE TABLE ORDERS(
ID VARCHAR NOT NULL PRIMARY KEY,
ORDER_DATE TIMESTAMP NOT NULL,
PRICE_TOTAL DOUBLE NOT NULL)
CREATE TABLE PRODUCTS(
ID VARCHAR NOT NULL PRIMARY KEY,
NAME VARCHAR NOT NULL,
PRICE DOUBLE NOT NULL,
AMOUNT INTEGER NOT NULL)
CREATE TABLE ORDER_ITEMS(
ID VARCHAR NOT NULL PRIMARY KEY,
ORDER_ID VARCHAR NOT NULL,
PRODUCT_ID VARCHAR NOT NULL,
AMOUNT INTEGER NOT NULL,
PRICE DOUBLE NOT NULL)
This data model is rather simple. For a real, "production quality" data model we would need foreign keys, indices, additional fields, etc. For this tutorial, the above data model will suffice.
Note: If you decided to use HypersonicSQL for this tutorial, you can use the provided orders.script and orders.properties files available in the source package attached to this article.
Java Code
Although these business requirements are simple and understandable, the traditional approach of writing a bunch of prepared statements will quickly become boring. Hibernate will save us from that. All we need is a set of simple mapping files. But first, we need to write our Java classes.
Note: We'll put all of our to-be-persisted classes in the test.hibernate
package, and all runner classes in the test
package.
Product
This simple class defines only the necessary fields: ID, product name, price of the product, and the current amount of this item available in stock. Since Hibernate works with plain, simple JavaBeans, all we need to do is to create getters and setters for every significant field (all fields are significant, in our case) and the default constructor.
package test.hibernate;
public class Product {
private String id;
private String name;
private double price;
private int amount;
public String getId() {
return id;
}
public void setId(String string) {
id = string;
}
// default constructor and other
// getters/setters not shown for brevity
// ...
}
Also, we override the toString()
method. This will help us to follow the application flow using simple System.out.println(obj)
calls:
public String toString() {
return
"[Product] " + name + "(" + id +
") price=" + price + " amount=" + amount;
}
That's it, no more, no less. How Hibernate will know to persist the objects of this type, since Product
doesn't implement any interface or extend any class? The answer is simple: Hibernate will work with any kind of Java object, as long as it follows JavaBeans convention.
Order
The next class we need to create is Order
. It's even simpler than Product
: it only contains ID, creation date, total price, and the Set
of OrderItem
s of which this Order
consists. Of course, create the getters and setters and default constructor.
package test.hibernate;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
public class Order {
private String id;
private Date date;
private double priceTotal;
private Set orderItems = new HashSet();
// Automatically set the creation time of
// this Order
public Order() {
this.date = new Date();
}
public String getId() {
return id;
}
public void setId(String string) {
id = string;
}
// other getters/setters not shown for
// brevity
// ...
}
Override toString()
as well. Don't forget to loop through orderItems
! Download the complete source code to see the example.
OrderItem
This class is a little bit more complicated, but it's still fairly straightforward. Our business demands say that we need a certain amount of Product
s, which we will put into an Order
. Those Product
s will automatically become OrderItem
s. It's a time for custom constructor.
package test.hibernate;
public class OrderItem {
/**
* Creates valid OrderItem. Automatically sets
* this OrderItem's price and corrects
* Product's stock availability amount.
*
* @param order to which this OrderItem belongs
* @param product from which this OrderItem is created
* @param amount
*/
public OrderItem(Order order,
Product product,
int amount) {
this.order = order;
this.product = product;
this.amount = amount;
product.setAmount(product.getAmount() - amount);
this.price = product.getPrice() * amount;
}
// we also need default constructor to keep
// Hibernate happy
/**
* Empty constructor to conform to JavaBeans
* convention.
*
*/
public OrderItem() {
// empty default constructor
}
// fields
private String id;
private Product product;
private Order order;
private String productId;
private String orderId;
private double price;
private int amount;
public String getId() {
return id;
}
public String getProductId() {
return product.getId();
}
public String getOrderId() {
return order.getId();
}
// other getters/setters not shown
// ...
// convenient way to display this OrderItem
public String toString() {
return
"[OrderItem] id=" + id + " amount=" +
amount + " price=" + price + "(" +
product + ")";
}
}
Now we have all of the classes that reflect the database structure. The only thing left unexplained is how we will put Product
s into an Order
. Simply add the following method to the Order
class:
/**
* Add a Product to this Order. Product
* automatically becomes an OrderItem.
* The priceTotal is automatically updated.
*
* @param p Product to add to this Order
* @param amount amount of products to add
*/
public void addProduct(Product p,
int amount) {
OrderItem orderItem = new OrderItem(this,
p, amount);
this.priceTotal = this.priceTotal
+ p.getPrice() * amount;
this.orderItems.add(orderItem);
}
Starting Up Hibernate
The basic usage pattern in our imaginary application is simple: we'll create
a Product
and then make it persistent (or in other words, save it),
we'll search for and load an already persisted Product
and make
sure it's usable, and we'll update and delete Product
s.
<script language="JavaScript" type="text/javascript">
//<//script>');
//]]>
</script><script language="JavaScript" src="http://ad.doubleclick.net/adj/ttm.onjava/jdbcart;sz=336x280;tile=3;ord=7625903970926686?" type="text/javascript"></script>
Create And Persist A Product
Now we'll finally use Hibernate. The usage scenario is fairly simple:
- Create a valid
Product
.
- Obtain
net.sf.hibernate.SessionFactory
using
net.sf.hibernate.cfg.Configuration
at the start of the application.
- Open
net.sf.hibernate.Session
by calling
SessionFactory#openSession()
.
- Save the
Product
and close the Session
.
As we can see, there's no mention of JDBC, SQL, or anything similar. Very
encouraging! The following sample follows the above-mentioned steps:
package test;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.Transaction;
import net.sf.hibernate.cfg.Configuration;
import test.hibernate.Product;
// use as
// java test.InsertProduct name amount price
public class InsertProduct {
public static void main(String[] args)
throws Exception {
// 1. Build a Product
Product p = new Product();
p.setName(args[0]);
p.setAmount(Integer.parseInt(args[1]));
p.setPrice(Double.parseDouble(args[2]));
// 2. Fire up Hibernate
Configuration cfg = new Configuration()
.addClass(Product.class);
SessionFactory sf = cfg.buildSessionFactory();
// 3. Open Session
Session sess = sf.openSession();
// 4. Save Product and close Session
Transaction t = sess.beginTransaction();
sess.save(p);
t.commit();
sess.close();
}
}
Let's run it for the first time! Insert 100 bottles of milk at 1.99 each by
issuing java test.InsertProduct Milk 100 1.99
. We get something
similar to this:
Nov 23, 2003 9:05:50 AM net.sf.hibernate.cfg.Environment <clinit>
INFO: Hibernate 2.0.3
Nov 23, 2003 9:05:50 AM net.sf.hibernate.cfg.Environment <clinit>
INFO: hibernate.properties not found
Nov 23, 2003 9:05:50 AM net.sf.hibernate.cfg.Environment <clinit>
INFO: using CGLIB reflection optimizer
Nov 23, 2003 9:05:50 AM net.sf.hibernate.cfg.Environment <clinit>
INFO: JVM proxy support: true
Nov 23, 2003 9:05:50 AM net.sf.hibernate.cfg.Configuration addClass
INFO: Mapping resource: test/hibernate/Product.hbm.xml
Exception in thread "main" net.sf.hibernate.MappingException:
Resource: test/hibernate/Product.hbm.xml not found
at net.sf.hibernate.cfg.Configuration.addClass(Configuration.java:285)
at test.FindProductByName.main(FindProductByName.java:24)
It doesn't work. Two lines are especially interesting:
INFO: hibernate.properties not found
and
Resource: test/hibernate/Product.hbm.xml not found
.
The INFO
line indicates that we need a
hibernate.properties configuration file, of course. That's the place
where we configure which database we use, the username and password, and many
other options. Use this provided sample to connect to the Hypersonic database
mentioned before:
hibernate.connection.username=sa
hibernate.connection.password=
hibernate.connection.url=jdbc:hsqldb:/home/davor/hibernate/orders
hibernate.connection.driver_class=org.hsqldb.jdbcDriver
hibernate.dialect=net.sf.hibernate.dialect.HSQLDialect
Modify it as appropriate (e.g., you'll probably need to change
hibernate.connection.url
) and save in your classpath.
This was an easy one, but what is that test/hibernate/Product.hbm.xml
resource? It's an XML file that defines how a Java object is persisted (mapped)
to a database. In that file, we define into which database table the data goes,
which field is mapped to which table column, how different objects relate to
each other, etc. Let's take a look at Product.hbm.xml.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="test.hibernate.Product"
table="products">
<id name="id" type="string"
unsaved-value="null">
<column name="id" sql-type="char(32)"
not-null="true"/>
<generator class="uuid.hex"/>
</id>
<property name="name">
<column name="name" sql-type="char(255)"
not-null="true"/>
</property>
<property name="price">
<column name="price" sql-type="double"
not-null="true"/>
</property>
<property name="amount">
<column name="amount" sql-type="integer"
not-null="true"/>
</property>
</class>
</hibernate-mapping>
It is very simple and understandable. A few details are especially
interesting:
-
<class name="test.hibernate.Product" table="products">
says that we're mapping a class named test.hibernate.Product
to the
table products
.
-
The <id>
element and its child elements define the
connection between our Java class and the database.
-
<property>
elements define into which column each field
goes, its type, name, etc.
The <generator class="uuid.hex"/>
element is not quite
understandable at first. Knowing that it is one of the child elements of
<id>
, its role become a little bit obvious: since our
application doesn't know how its data will be persisted (and we're saying that
all the time), we need a surrogate key, with no business meaning, to help
Hibernate to manipulate objects. Newly created Product
s don't have
that id
, and Hibernate will create them for us. We chose to use
UUID strings, but many different ID generators are provided (sequential, within
some specific range, or even user assigned, etc.) and you can always write your
own ID generator. See the documentation for details.
Now, create (copy and paste) the content of Product.hbm.xml and place
the file into the same package as the test.hibernate.Product
class
(e.g., in the same directory where your Product.java file is placed) and
re-run java test.InsertProduct Milk 100 1.99
. Now we see much more
logging information and ... nothing more! Is it working correctly? Add
System.out.println(p)
just before Session sess =
sf.openSession();
and after sess.close()
to see what's going
on with our Product
. Re-run. You'll see something similar to this
(that funny ID number will surely be different):
[Product] Milk(null) price=1.99 amount=100
[Product] Milk(40288081f907f42900f907f448460001) price=1.99 amount=100
Hibernate created the Product
's id
for us! Let's
see if the Product
is stored in our database. Issue select *
from products
. The database returns something similar to:
ID |NAME |PRICE |AMOUNT |
40288081f907f42900f907f448460001|Milk |1.99 |100 |
The Product
information is successfully inserted into our
database and we haven't written a single line of SQL!
Insert some other products, such as bread, coffee, beer, etc., to have some
data to work with in this tutorial.
Find And Load Products
Finding and loading already persisted objects is very simple with Hibernate.
Using its query language we can easily fetch an object (or set of
objects) by its ID, name, or some other property. We can fetch the complete
object or just some of its properties. Hibernate will take care of the rest, and
at the end we'll have completely useful hierarchy of objects. Let's take a look
at the test.FindProductByName
class.
package test;
import java.util.List;
import net.sf.hibernate.Hibernate;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.cfg.Configuration;
import test.hibernate.Product;
// use as
// java test.FindProductByName name
public class FindProductByName {
public static void main(String[] args) throws Exception {
// query to issue
String query =
"select product from product "
+ "in class test.hibernate.Product "
+ "where product.name=:name";
// search for what?
String name = args[0];
// init
Configuration cfg = new Configuration()
.addClass(Product.class);
SessionFactory sf = cfg.buildSessionFactory();
// open session
Session sess = sf.openSession();
// search and return
List list = sess.find(query, name,
Hibernate.STRING);
if (list.size() == 0) {
System.out.println("No products named "
+ name);
System.exit(0);
}
Product p = (Product) list.get(0);
sess.close();
System.out.println("Found product: " + p);
}
}
We have several interesting points in FindProductByName
:
-
There's a query
string with a where
clause. This is
very similar to standard SQL.
-
We initialize Hibernate in the same way as in our first example. This time,
we have the configuration and mapping files.
-
sess.find()
executes the query and sets the provided product
name as the search argument of type Hibernate.STRING
.
-
As the result, we get a java.util.List
full of found
Product
s.
-
With Product p = (Product) list.get(0);
we fetch the found
objects in the usual way, with casting.
Issue java test.FindProductByName Milk
and see what's shown in
the console.
Note: Queries are case-sensitive, so searching for lowercase
milk
won't return any results. Use the lower()
or
upper()
SQL functions to enable case insensitive searches. In that
case, we would have where lower(product.name)=lower(:name)
in our
query string. See the documentation for details on queries.
Also, if you don't want all the INFO
logging information displayed,
tweak log4j.properties
and set all log levels to
warn
.
Update And Delete Products
By now you should have basic understanding of how Hibernate works, so let us
make long examples shorter by showing only the important parts.
To increase the prices of all Product
s by 10% percent in a
single transaction, we would write something like this:
double percentage = Double.parseDouble(args[0])/100;
sess = sf.openSession();
Transaction t = sess.beginTransaction();
// list contains Products
Iterator iter = list.iterator();
while (iter.hasNext()) {
Product p = (Product) iter.next();
p.setPrice(p.getPrice() * (1 + percentage));
sess.saveOrUpdate(p);
}
t.commit();
sess.close();
And finally, to delete a Product
, we would, of course, call
sess.delete(product)
. Don't forget to commit()
the
Transaction
if autocommit
is turned off for your
database.
Now we have gone through all of the basic operations -- create, read, update,
and delete -- for a single object. It does look interesting, but things are
getting even better. We'll now learn how to manipulate a collection of objects
without writing a single SQL statement. All of the magic will be done through
the mapping files.
Order
s, OrderItem
s
Manipulating objects on one-by-one basis will definitely save as some time,
but what we really want are cascading loads and updates. We'll now see how to do
that.
We need to examine Order
s and OrderItem
s in
parallel. As mentioned before, we add a Product
to an
Order
and it then becomes an OrderItem
.
Order
internally keeps a set of OrderItem
s. What we
want is to save Order
and have Hibernate do the rest: save
OrderItem
s and to update the stock availability (amount) of the
added Product
s. Sounds complicated, but it is actually very simple.
Hibernate knows how to deal with related objects in one-to-one
,
one-to-many
, many-to-one
, and
many-to-many
fashion. We'll start with the mapping files.
Order.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="test.hibernate.Order" table="orders">
<id name="id" type="string" unsaved-value="null" >
<column name="id" sql-type="char(32)" not-null="true"/>
<generator class="uuid.hex"/>
</id>
<property name="date">
<column name="order_date"
sql-type="datetime" not-null="true"/>
</property>
<property name="priceTotal">
<column name="price_total"
sql-type="double" not-null="true"/>
</property>
<set name="orderItems" table="order_items" inverse="true" cascade="all">
<key column="order_id" />
<one-to-many class="test.hibernate.OrderItem" />
</set>
</class>
</hibernate-mapping>
This mapping file is quite understandable, except the last element,
<set>
. This represents connections between different classes;
in our case, those are Order
and OrderItem
. The
attributes and child elements are quite understandable: a field of type
Set
, named orderItems
(see the Order
source code above), contains objects of the type
test.hibernate.OrderItem
, as explained by
<one-to-many>
child element. Those objects are persisted in
the order_items
table, where the order_id
column
contains keys for OrderItem
type of objects.
The cascade="all"
attribute is a very important one. It explains
how Hibernate should act while manipulating connected objects. In our specific
situation, when an Order
is created, we definitely want all of its
OrderItem
s to be created as well, and, of course, when an
Order
is deleted, we also want all of its OrderItem
s
to be deleted. There are three more options cascade
attribute can
hold, none
, save-update
, and delete
, and
we'll see how to use them in the following example.
OrderItem.hbm.xml
This object is an interesting one. Its instances are created automatically
within Order
, and basically don't have life outside of it. However,
we need them, since they represent the Product
s at the time
Order
was created. So, if a Product
's price is
changed, we definitely don't want all the appropriate OrderItem
s'
and therefore Order
s' prices to be changed. But what we want is to
update the stock availability of a Product
whenever an
OrderItem
is created. And finally, when an Order
is
deleted, its OrderItem
s are deleted, as well, but we must not touch
the Product
s! Sounds complicated, especially when all of those SQL
statements need to be written. But Hibernate compresses all of them into two
lines in the mapping file!
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="test.hibernate.OrderItem"
table="order_items">
<id name="id" type="string" unsaved-value="null" >
<column name="id" sql-type="char(32)"
not-null="true"/>
<generator class="uuid.hex"/>
</id>
<property name="orderId" insert="false"
update="false">
<column name="order_id" sql-type="char(32)"
not-null="true"/>
</property>
<property name="productId" insert="false"
update="false">
<column name="product_id" sql-type="char(32)"
not-null="true"/>
</property>
<property name="amount">
<column name="amount" sql-type="int"
not-null="true"/>
</property>
<property name="price">
<column name="price" sql-type="double"
not-null="true"/>
</property>
<many-to-one name="order"
class="test.hibernate.Order"
column="order_id" />
<many-to-one name="product"
class="test.hibernate.Product"
cascade="save-update"
column="product_id"/>
</class>
</hibernate-mapping>
We know all about the <id>
and
<property>
elements by now, but
<many-to-one>
is a new one. It's fairly simple. The first use
of the <many-to-one>
element indicates that
OrderItem
's field named order
is of type
test.hibernate.Order
and is referenced through the
order_id
column from the table order_items
(see the
table
attribute of the element class
). The second
many-to-one
element is similar to the first one, except that it has
cascade="save-update"
attribute. It's explained before what it
defines. In this case, we say that Hibernate should propagate changes on
Product
s only when an OrderItem
is saved (created) or
updated (changed), and not on delete. So the above-mentioned concerns about
complicated SQL statements are compressed in one single attribute! Now beat
that!
Usage Examples
Create an Order
. In this example, we create and persist
an Order
. Run this example more than once to see how
Product
s' amounts change after each successful Order
creation.
// ...
Configuration cfg = new Configuration()
.addClass(Product.class)
.addClass(Order.class)
.addClass(OrderItem.class);
// ...
Order order = new Order();
order.addProduct(milk, 3);
order.addProduct(coffee, 5);
// ...
sess = sf.openSession();
Transaction t = sess.beginTransaction();
sess.save(order);
t.commit();
sess.close();
System.out.println(order);
// ...
Find Order
s within a price range. In this example, we
show how to use a query with two parameters. Hibernate correctly loads
Order
s with appropriate OrderItem
s and
Product
s.
// ...
String query = "select o from o "
+ "in class test.hibernate.Order "
+ "where o.priceTotal > :priceTotalLower "
+ "and o.priceTotal < :priceTotalUpper";
// ...
Query q = sess.createQuery(query);
q.setDouble("priceTotalLower",
Double.parseDouble(args[0]));
q.setDouble("priceTotalUpper",
Double.parseDouble(args[1]));
List list = q.list();
// ...
sess.close();
// ...
Delete Order
s within a price range. This is an important
example. Here we can see how intelligent a tool Hibernate is. As mentioned
above, when deleting an Order
, its OrderItem
s need to
be deleted, but the Product
s must not be changed. After this
example, check your database to ensure that products are intact.
// ...
String query = "select o from o "
+ "in class test.hibernate.Order "
+ "where o.priceTotal > :priceTotalLower "
+ "and o.priceTotal < :priceTotalUpper";
Transaction tx = sess.beginTransaction();
sess.delete(query,
new Object[]{new Double(args[0]),
new Double(args[1])},
new Type[]{Hibernate.DOUBLE,
Hibernate.DOUBLE}
);
tx.commit();
sess.close();
Conclusion
This article shows how powerful Hibernate is. You've learned how easy it is
to persist any kind of Java object, to manipulate a hierarchy of objects, handle
collections, and work with transactions. But the scope of Hibernate's
functionality is much wider. It handles full transactions with commit and
rollback, inheritance, a few types of collections, and offers very powerful
object-oriented query language, HQL, which supports associations and joins,
polymorphism, subqueries, etc. Your next step is to read the Hibernate Reference
Documentation and to start using Hibernate in your day-to-day work.