When you enter the Hibernate world, you hear a lot about sessions, transactions, session-per-request, etc., to the point you start mixing the concepts. Let's face it:
Fact 1: Hibernate is big
Fact 2: Hibernate is easy if you understand the fundamentals; everything else builds on top of those
I'll try to explain these fundamental concepts and then I'll cover the famous OpenSessionInView pattern with an example.
So, what's a session ? If I tell you "a session is a unit of work bla bla bla", you'll say "I've read that in about 1,200,000 hits returned by Google and yet I don't really understand the concept". Let me put it this way: from the developer's perspective (what can I do, I usually incarnate this perspective), a session is actually an API that abstracts the notion of persistence service. Thus, whenever you want to do something with the objects (representing the O in the ORM), like synchronize them with the DB, persist them to the DB, etc., you need a session instance.
However, as we all know, each operation to the DB - be it a read or a write - needs a transaction. Thus, because you use the session in order to perform DB operations, it means whenever we're using the session we need a transaction (with a noteworthy exception, but we'll cover that later). If you're trying to run an operation on the session without being inside a transaction, you'll get a runtime exception of the type org.hibernate.TransactionException: Transaction not successfully started.
In other words, it's quite logical that the scope of a session (i.e. from the moment you create/open it to the moment you close it) and the scope of a transaction coincide. Without further ado, this is the session-per-request pattern.
Now let's take a painfully real example. Suppose I have two tables in my DB: a Patients table and a PatientPhones table (basically each patient from the first table has a number of telephone numbers recorded in the second table). They are linked by PATIENT_ID, a foreign key in PatientPhones table and primary key in the Patients table.
The Hibernate file that describes these tables is:
<?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 package="model">
<class name="Patient2" table="PATIENTS">
<id name="id" column="PATIENT_ID">
<generator class="assigned" />
</id>
<property name="name"/>
<property name="sex"/>
<property name="birth_day"/>
<property name="birth_month"/>
<property name="birth_year"/>
<property name="address"/>
<property name="cnp"/>
<bag name="phoneNumbers" table="PATIENT_PHONES" cascade="all" lazy="true">
<key column="PATIENT_ID"/>
<element type="string" column="PHONE_NUMBER"/>
</bag>
</class>
</hibernate-mapping>
Notice the lazy="true" attribute above ? This means that actually the phone numbers will be retrieved from the DB lazily, i.e. when we request them. For example, suppose I do this:
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
// note that in this query we use the name of the class, Patient2,
// rather than the name of the table (Patients)
ArrayList patients =
new ArrayList(session.createQuery("from Patient2").list());
session.getTransaction().commit();
HibernateUtil.getSessionFactory().close();
Then, after the session has been closed, I do this:
for (Patient2 p : patients) {
System.out.println("Patient " + p.getName() + " " + p.getId());
}
This will work, because the patients list has been eagerly populated with the DB data inside the transaction above.
But if I try:
for (Patient2 p : patients) {
if ((p.getPhoneNumbers() != null) && (p.getPhoneNumbers().size() > 0)) {
System.out.print(" * phones :");
Iterator iter = p.getPhoneNumbers().iterator();
while (iter.hasNext()) {
System.out.print(" " + iter.next());
}
System.out.println();
}
}
This fails with the error LazyInitializationException (or something like this) because in the xml file above I asked the phone numbers to be lazily retrieved, i.e. when I do p.getPhoneNumbers(). However, at this moment I am outside the session, so I'm working with the detached object p. In order to be able to retrieve the phones, I have 2 choices:
1. either do lazy = "false" in the xml above
2. or reattach each object p to which I'm asking getPhoneNumbers(), like this:
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
for (Patient2 p : patients) {
System.out.println("Patient " + p.getName() + " " + p.getId());
session.lock(p, LockMode.NONE);
if ((p.getPhoneNumbers() != null) && (p.getPhoneNumbers().size() > 0)) {
System.out.print(" * phones :");
Iterator iter = p.getPhoneNumbers().iterator();
while (iter.hasNext()) {
System.out.print(" " + iter.next());
}
System.out.println();
}
}
session.getTransaction().commit();
HibernateUtil.getSessionFactory().close();
This situation is extremely relevant, because in a typical web application you'd do exactly this:
whenever you show the results of a DB query (via Hibernate) in your view (a jsp page for example), your objects are detached (the session is closed, the transaction committed), so you can get the exception LazyInitializationException.
The solution is the Open Session in View pattern, which simply states that the session should remain open and the transaction uncommitted until the results have been processed in the view. This can be easily done with a filter, e.g.:
public class HibernateSessionRequestFilter implements Filter {
private SessionFactory sf;
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
sf.getCurrentSession().beginTransaction();
// Call the next filter (continue request processing)
chain.doFilter(request, response);
// Commit and cleanup
sf.getCurrentSession().getTransaction().commit();
} catch (StaleObjectStateException staleEx) {
throw staleEx;
} catch (Throwable ex) {
// Rollback only
try {
if (sf.getCurrentSession().getTransaction().isActive()) {
sf.getCurrentSession().getTransaction().rollback();
}
} catch (Throwable rbEx) {
}
// Let others handle it... maybe another interceptor for exceptions?
throw new ServletException(ex);
}
}
public void init(FilterConfig filterConfig) throws ServletException {
sf = HibernateSession.getSessionFactory();
}
(NOTE: this pattern only works when the jsp and Hibernate code run on the same JVM, i.e. when the presentation and business logic tiers run on the same machine)
I will register this filter in my web.xml:
<filter>
<filter-name>HibernateFilter</filter-name>
<filter-class>view.backing.HibernateSessionRequestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HibernateFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Now, in one of my JSF backing beans, PatientsSearch.java, I could have:
public void getAllPatients() {
Session session = null;
try {
session = HibernateSession.getSessionFactory().getCurrentSession();
Query query = session.createQuery("from Patient2 p order by p.name asc");
int pageNumber = 1;
int pageSize = rowsPerPage;
query.setFirstResult((pageNumber - 1) * pageSize);
query.setMaxResults(pageSize);
patients = new ArrayList(query.list());
totalRows = count();
} catch (Exception e) {
if (session != null) session.getTransaction().rollback();
e.printStackTrace();
}
}
And in the view, patientSearch.jsp, I could have:
<h:dataTable value="#{PatientsSearch.patients}" var="patient"
binding="#{PatientsSearch.dataTable1}" id="dataTable1"
border="2" bgcolor="White" frame="border"
">
<h:column>
<f:facet name="header">
<h:outputText value="Name"/>
</f:facet>
<h:commandLink action="#{PatientsSearch.edit}">
<h:outputText value="#{patient.name}" style="font-family:Garamond;"/>
</h:commandLink>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="ID"/>
</f:facet>
<h:outputText value="#{patient.id}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Address"/>
</f:facet>
<h:outputText value="#{patient.address}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Fix phone"/>
</f:facet>
<h:outputText value="#{patient.phoneFix}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Mobile phone"/>
</f:facet>
<h:outputText value="#{patient.phoneMobile}"/>
</h:column>
</h:dataTable>
<h:commandButton value="All patients" id="commandButton1"
action="#{PatientsSearch.getAllPatients}"/>
This works just as described above: in the view I don't need to worry about the session; even if lazy = "true", I know that the calls patient.phoneMobile and patient.phoneFix will retrieve the data, because everything happens between the two important statements in the filter:
sf.getCurrentSession().beginTransaction();
....
sf.getCurrentSession().getTransaction().commit();
By the way, in Patient2.java, I have:
public String getPhoneFix() {
if (phoneNumbers.size() > 0) return (String)phoneNumbers.get(0);
return null;
}
Simple, isn't it ?
However, there is potentially something wrong with this approach. Check out the next article to see what and how we can correct it.
No comments:
Post a Comment