Thursday, June 14, 2007

Share that POJO - Hibernate clustered and empowered

I haven't had much experience with OR mapping in Java apps so I wanted to study Hibernate tutorial and tried to cluster it at the same time. Starting with Terracotta 2.4 (currently at stable0), Hibernate is supported.

My first question to the engineer who worked on the feature is "What is being shared in Hibernate?" It turns out, the plain old Java objects (POJOs) that Hibernate constructs from the database are the ones that we're interested in sharing, in this case, across multiple JVMs. I told myself, that's pretty handy, so we don't have to hit the database again for the same information on another node when those objects have already been loaded and shared thanks to Terracotta DSO server. He told me that's not the only cool feature though. With those shared objects from Hibernate, one JVM can just reassociate them to its Hibernate session and start to access colletions that are mapped as one-to-many, many-to-many, that don't exist yet in memory due to lazy initalization. Wow. (Of course, that Hibernate session will have to hit the database for that new info you request. You don't always get free beer!)

With that peaked interest, I worked on this cool and thorough tutorial from Hibernate.org

The schema for this tutorial as follow:

EVENTS PERSON_EVENT PERSON
---------- ------------- ----------
*EVENT_ID *EVENT_ID *PERSON_ID
EVENT_DATE *PERSON_ID FIRSTNAME
TITLE LASTNAME


PERSON_EMAIL_ADDR
------------------
*PERSON_ID
*EMAIL_ADDR


For each table, there is a Javabean to represents it. Each javabean instance will be mapped to a row in the database. Well, that's Hibernate in a nutshell for me (speaking with my own ignorance and limited experience)

The xml mapping for Events table as follow:

<hibernate-mapping>

<class name="events.Event" table="EVENTS">
<id name="id" column="EVENT_ID">
<generator class="native"/>
</id>
<property name="date" type="timestamp" column="EVENT_DATE"/>
<property name="title"/>

<set name="participants" table="PERSON_EVENT" lazy="true" inverse="true" cascade="lock">
<key column="EVENT_ID"/>
<many-to-many column="PERSON_ID" class="events.Person"/>
</set>
</class>

</hibernate-mapping>


It's a many-to-many relationship between Events and Person. Notice I set the lazy loading to true, it is needed to demonstrate the point I mentioned earlier.

Here is Person's map file.


<class name="events.Person" table="PERSON">
<id name="id" column="PERSON_ID">
<generator class="native"/>
</id>
<property name="age"/>
<property name="firstname"/>
<property name="lastname"/>

<set name="events" table="PERSON_EVENT">
<key column="PERSON_ID"/>
<many-to-many column="EVENT_ID" class="events.Event"/>
</set>

<set name="emailAddresses" table="PERSON_EMAIL_ADDR">
<key column="PERSON_ID"/>
<element type="string" column="EMAIL_ADDR"/>
</set>

</class>


For hibernate.cfg.xml file, I changed the config from the tutorial a little bit to use Derby database. It is run as server mode. Here is the change:


<property name='connection.driver_class'>org.apache.derby.jdbc.ClientDriver</property>
<property name='connection.url'>jdbc:derby://localhost:1527/MyDbTest</property>
<property name='connection.username'>user1</property>
<property name='connection.password'>user1</property>


Derby is shipped with jdk1.6.0_01, under java-home/db/lib. To start it,


% java -jar derbyrun.jar server start

Then I created "MyDbTest" database by running this once:

% java -jar derbyrun.jar dblook -d "jdbc:derby://localhost:1527/MyDbTest;create=true"


Now on to the EventManager.java of the tutorial. I didn't follow it fully. I created 2 events, and 3 persons. First event is "Engineer meeting", second one is a "Document Meeting" contain these participants

// engMeeting = {steve, orion, tim}
// docMeeting = {steve, orion}

Here is a snippet of EventManager.java.


public class EventManager {

// shared object - declared as a root in tc-config.xml
private static final List events = new ArrayList();

public static void main(String[] args) {
EventManager mgr = new EventManager();

// create 3 persons Steve, Orion, Tim
Long steveId = mgr.createAndStorePerson("Steve", "Harris");
mgr.addEmailToPerson(steveId, "steve@terracottatech.com");

...

// create 2 events
Long engMeetingId = mgr.createAndStoreEvent("Eng Meeting", new Date());
mgr.addPersonToEvent(steveId, engMeetingId);
mgr.addPersonToEvent(orionId, engMeetingId);
mgr.addPersonToEvent(timId, engMeetingId);

Long docMeetingId = mgr.createAndStoreEvent("Doc Meeting", new Date());

....

// store our events into a shared object
synchronized (events) {
events.addAll(mgr.listEvents());
}

HibernateUtil.getSessionFactory().close();
}

private List listEvents() {

Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();

List result = session.createQuery("from Event").list();

session.getTransaction().commit();
return result;
}


Most of it is straight forward and copied from the Hibernate tutorial. Terracotta comes into the picture with the presence of the shared-object "events". It is declared as a root in Terracotta config and it will hold the list of events that Hibernate loaded from database. Here is how declare it in tc-config.xml:

<root>
<field-name>events.EventManager.events</field-name>
<root-name>tcEvents</root-name>
</root>

The synchronized block of shared object "events" is necessary. It's in a context of multithreads and multi-JVM.

So that's enough for us to start the first JVM. I turned on SQL log from log4j so we can see the SQL being generated by Hibernate. There are a lot of sql statements printed out, both "insert" and "select" as you would expect Hibernate to
generate. After it's finished, our "events" list will contain events object, and since it's set with "lazy=true", their participant set will be empty. We can confirm this by looking at Terracotta Admin console:



Under root, we have 1 root, namely "tcEvents" that has 2 objects. Expanding them, and we'll see the participant set is empty. Hibernate internal set is null.

org.hibernate.collection.PersistentSet.set=null

So that is my first app. Here comes my second app that run in a totally seperate VM. I caled it EventChecker and it will list emails of people in the first meeting.

First, it has to know about the same shared object "tcEvents" that we put onto Terracotta DSO Server. We can do this easily by specify it in tc-config.xml:


<roots>
<root>
<field-name>events.EventManager.events</field-name>
<root-name>tcEvents</root-name>
</root>
<root>
<field-name>events.EventChecker.events</field-name>
<root-name>tcEvents</root-name>
</root>
</roots>


By using the same root-name, it's mapped to the same list of EventManger. Its code is pretty short so I list all of it here:


public class EventChecker {
// shared object
private static final List events = new ArrayList();

public static void main(String[] args) {

synchronized (events) {
// list events before even opening a Hibernate session
System.out.println("** events: " + events);

Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();

// reassociate transient pojos to this session
for (Iterator it = events.iterator(); it.hasNext(); ) {
session.lock((Event)it.next(), LockMode.NONE);
}

// list people in first event
Event event = (Event)events.get(0);
Set people = event.getParticipants();
System.out.println("** people: " + people);

// list emails of people from first event
Set emails = new HashSet();
for (Iterator it = people.iterator(); it.hasNext(); ) {
Person person = (Person)it.next();
emails.addAll(person.getEmailAddresses());
}
System.out.println("** emails: " + emails);

session.getTransaction().commit();
HibernateUtil.getSessionFactory().close();
}
}
}


Here it is you see the usage of synchorized block on the shared object. We're accessing it in brand new JVM. Without Terracotta, the events list will be empty. With Terracotta, the events list contains events from EventManager, faulting by DSO server transparently.
I then reassociated the pojos to my newly created Hibernate session. This is always needed when you work with Hibernate. I didn't know about this until I got an exception saying I don't have a session or session might have been closed by Hibernate.

After that, I can list the pariticipants and their emails using Hibernate. This is all Hibernate doing. I love it :)

Here is the output, it will show that Hibernate didn't generate SQL to load in the events since it's already loaded and stored by Terracotta. It only loaded from Person and Person_Email_Addr:

** events: [Eng Meeting: 2007-06-14 11:06:12.0, Doc Meeting: 2007-06-14 11:06:12.0]

Hibernate: select participan0_.EVENT_ID as EVENT1_1_, participan0_.PERSON_ID as PERSON2_1_, person1_.PERSON_ID as PERSON1_2_0_, person1_.age as age2_0_, person1_.firstname as firstname2_0_, person1_.lastname as lastname2_0_ from PERSON_EVENT participan0_ left outer join PERSON person1_ on participan0_.PERSON_ID=person1_.PERSON_ID where participan0_.EVENT_ID=?

** people: [Orion Letizi, Steve Harris, Tim Eck]

Hibernate: select emailaddre0_.PERSON_ID as PERSON1_0_, emailaddre0_.EMAIL_ADDR as EMAIL2_0_ from PERSON_EMAIL_ADDR emailaddre0_ where emailaddre0_.PERSON_ID=?
Hibernate: select emailaddre0_.PERSON_ID as PERSON1_0_, emailaddre0_.EMAIL_ADDR as EMAIL2_0_ from PERSON_EMAIL_ADDR emailaddre0_ where emailaddre0_.PERSON_ID=?
Hibernate: select emailaddre0_.PERSON_ID as PERSON1_0_, emailaddre0_.EMAIL_ADDR as EMAIL2_0_ from PERSON_EMAIL_ADDR emailaddre0_ where emailaddre0_.PERSON_ID=?

** emails: [teck@terracottatech.com, steve@terracottatech.com, orion@terracottatech.com]

Looking at the Admin console again, we see that the participant set is now loaded in by Hibernate and shared by Terracotta.



There is one gotcha that I ran into that I thought I should mention. In hibernate.cfg.xml, there is an option to clean out and create the schema every time Hibernate start.

<!-- Drop and re-create the database schema on startup -->
<property name=\"hbm2ddl.auto\">create</property>


When you run EventManger, you need it to create the schema for you. But when you run EventChecker, you don't want Hibernate to wipe out your database. You should comment out that line before running it.

I had fun working on Hibernate tutorial and playing it Terracotta. I don't have much experience with J2EE and Hibernate so I can't really comment on a practical use case. Maybe you will have better idea how to use it :)
Post a Comment