#

Deeper into Hibernate

Ich habe mich vor kurzem entschlossen, mich etwas mehr mit Hibernate zu beschäftigen. Hier einige anfängliche Erfahrungen, die ich mit Unterstützung meines Hibernate-Meisters Toni machen konnte.

Mit Hibernate benutzt man klassischerweise DAOs, Data Access Objects: Klassen, welche die Persistenz von Objekten reguliert. Hier bedeutet Persitenz das Speichern, Lesen , Aktualisieren und Löschen von Objekten. Beispielsweise kann mit der Methode getMostRecent(int amount) durch Hibernate aus der Datenbank eine Menge Nachrichten (Klasse Post) geladen werden:

public List getMostRecent(int amount) throws PersistenceException {
        Session session = null;
        List recent = null;
        try{
                session = HibernateUtil.getSessionFactory().openSession();
                session.beginTransaction();
                recent = session.createCriteria(Post.class)
                      .addOrder( Order.desc("creationDate") )
                      .setMaxResults(amount).list();
                session.getTransaction().commit();
        }
        catch(HibernateEcception e){
                throw new PersistenceException(e);
        }
        finally{
                try{
                        session.flush();
                }
                catch(Throwable t){}
                try{
                        session.close();
                }
                catch(Throwable t){}
        }
 
        return recent;
}

Bei dieser Architektur ergibt sich ein Problem mit dem Lazyloading:
Hibernate unterstützt das Laden von abhängigen Klassen bei bedarf. Dies würde z. B. auf das vom Post referenzierte Objekt Category zutreffen. Wenn Hibernate die Posts lädt, kann man bei aktiviertem Layzloading dafür dorgen, dass die dazu gehörenden Category Objekte erst dann geladen werden, wenn man post.getCategories() aufruft. Sinnvoll, da man ja nicht immer alle abhängigen Objekte braucht, und besonders sinnvoll, wenn man recht komplexe Abhängigkeiten hat (Post 1<->* Comments, Post n<->m Category, Category <->1 Category (parent), man kann sehen, dass es sich hierbei um einen Blog handelt…), und man beim Holen eines Objektes fast alle anderen Objekte mitladen würde.

Doch für Lazyloading benötigt Hibernate dieselbe Session(So etwas, wie eine Connection bei JDBC, was es aber sehr schlecht trifft. Entscheiden ist nur: Alle Abfragen, Updates und Deletes zur Datenbank werden über dieses Objekt durchgeführt und man kann mehrere Sessions benutzen, diese öffnen und auch wieder schließen.) mit der das Hauptobjekt geladen wurde. In der oberen DAO-Methode wird jedoch für jede Objektabfrage eine Session erzeugt und wieder geschlossen, womit das Lazyloading nicht klappt.

Die Funktion session.currentSession() benutzt ThreadLocals, um für jeden Thread eine Session bereit zu stellen und diese Session automatisch beim Beenden des Threads zu commiten und schließen. Dies kam aber hier nicht in Frage, da Tomcat Threadpooling benutzt und die einzelnen Threads noch über dem Geltungsbereich der Abfrage hinaus weiter existieren.

Toni half mir hier aus: Eine Hilfsklasse (HibernateUtil) verwaltet die Sesions und Transaktionen über ThreadLocal Objekte und kann so die Session für die komplette Dauer einer Anfrage offen halten, und damit das Lazyloading problemlos ermöglichen.
Über eine statische Methode kann dann diese Session bearbeitet werden. Der Code stammt von Toni und wurde von mir für meinen Zwecke etwas modifiziert.

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
 
/**
 * Hibernate utility for session and transaction management
 * 
 * @author Based on <a href="http://keyboardsamurais.de">A. Agudo</a>, 
modified by <a href="http://narcanti.keyboardsamurais.de">M. S. Cinar</a>
 */
public class HibernateUtil {
private static SessionFactory sessionFactory = null;
 
private static final ThreadLocal sessionThreadLocal = new ThreadLocal();
 
private static final ThreadLocal transactionThreadLocal = new ThreadLocal();
 
static {
try {
// Create the SessionFactory from hibernate.cfg.xml
sessionFactory = new Configuration().configure().buildSessionFactory();
}
catch (Throwable e) {
Log.fatal(HibernateUtil.class, e);
throw new ExceptionInInitializerError(e);
}
}
 
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
 
// SESSION
 
/**
 * Returns the hibernate session from the threadlocal. If there is none,
 * creates a new session and attaches it to the thread local. If there is
 * one, checks if it's connected and reconnects if needed.
 * 
 * @return hibernate session
 * @throws Exception
 */
public static Session getSession() throws Exception {
Log.debug(HibernateUtil.class, "getSession()");
 
Session s = (Session) sessionThreadLocal.get();
if (s != null) {
if (!s.isConnected()) {
try {
s.reconnect();
}
catch (HibernateException e) {
Log.fatal(HibernateUtil.class, e);
throw new Exception("Session reconnect failed", e);
}
}
}
else {
// Open a new Session, if this thread has none yet
try {
s = sessionFactory.openSession();
sessionThreadLocal.set(s);
}
catch (HibernateException e) {
Log.fatal(HibernateUtil.class, e);
throw new Exception("Session creation failed", e);
}
}
 
return s;
}
 
/**
 * Closes the hibernate session of the associated threadlocal if it's not
 * null.
 * 
 * @throws Exception
 */
public static void closeSession() throws Exception {
Log.debug(HibernateUtil.class, "closeSession()");
 
Session s = (Session) sessionThreadLocal.get();
if (s != null && s.isOpen()) {
try {
s.close();
}
catch (HibernateException e) {
Log.fatal(HibernateUtil.class, e);
throw new Exception("Session closing failed", e);
}
finally {
sessionThreadLocal.set(null);
}
}
}
 
// TRANSACTION
 
/**
 * Returns the transaction from threadlocal. If there is none, it gets the
 * session from threadlocal and creates a new transaction.
 * 
 * @throws Exception
 */
public static Transaction beginTransaction() throws Exception {
Log.debug(HibernateUtil.class, "beginTransaction()");
 
Transaction tx = (Transaction) transactionThreadLocal.get();
if (tx == null) {
try {
tx = getSession().beginTransaction();
transactionThreadLocal.set(tx);
}
catch (HibernateException e) {
Log.fatal(HibernateUtil.class, e);
throw new Exception("Transaction begin failed", e);
}
}
return tx;
}
 
/**
 * Commits the transaction stred in the threadlocal. If commiting fails, a
 * rollback is done.
 * 
 * @throws Exception
 */
public static Transaction commitTransaction() throws Exception {
Log.debug(HibernateUtil.class, "commitTransaction()");
 
Transaction tx = (Transaction) transactionThreadLocal.get();
try {
if (tx != null && !tx.wasCommitted() && !tx.wasRolledBack()) {
tx.commit();
}
}
catch (HibernateException e) {
rollbackTransaction();
Log.fatal(HibernateUtil.class, e);
throw new Exception("Transaction commit failed", e);
}
finally {
transactionThreadLocal.set(null);
}
 
return tx;
}
 
/**
 * Rollback transaction from threadlocal and close session from threadlocal.
 * 
 * @throws Exception
 */
public static void rollbackTransaction() throws Exception {
Log.debug(HibernateUtil.class, "rollbackTransaction()");
 
Transaction tx = (Transaction) transactionThreadLocal.get();
try {
if (tx != null && !tx.wasCommitted() && !tx.wasRolledBack()) {
tx.rollback();
}
}
catch (HibernateException e) {
Log.fatal(HibernateUtil.class, e);
throw new Exception("Transaction rollback failed", e);
}
finally {
transactionThreadLocal.set(null);
closeSession();
}
}
}

In den Methoden, um die Session oder die Ttransaction zu schließen kommt ein wichtiger Aspekt hinzu: Toni machte mich auf den Artikel „ThreadLocals are EVIL!!!“aufmerksam, in dem berichtet wird, dass ThreadLocals zu MemoryLeaks führen können, da manchmal am Ende eines Threads die im ThreadLocal gespeicherten Objekte nicht gelöscht werden und sich daher akkumulieren können. Daher muss beim Schließen der Session der ThreadLocal auf jeden fall durch threadLocal.set(null) geleert werden.

Zu guter letzt kann ein ServletFilter benutzt werden, um sicherzustellen, dass am Ende einer Anfrage die Transaktion auch proper comitted wurde und die Session geschlossen wird.

import java.io.IOException;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
 
/**
 * @author Based on <a href="http://keyboardsamurais.de">A. Agudo</a>, 
modified by <a href="http://narcanti.keyboardsamurais.de">M. S. Cinar</a>
 */
public class HibernateFilter implements Filter {
 
public void init(FilterConfig conf) throws ServletException {}
 
public void doFilter(ServletRequest request, 
ServletResponse response, FilterChain chain) 
throws IOException, ServletException {
 
chain.doFilter(request, response);
 
try {
Log.debug(this, "Commiting transaction...");
HibernateUtil.commitTransaction();
}
catch (Throwable t) {
Log.error(this, t);
}
finally {
try {
HibernateUtil.closeSession();
}
catch (Exception e) {
Log.debug(this, e);
}
}
 
}
 
public void destroy() {}
}

Zum Schluß reduziert sich die obere Methode des DAOs auf die folgenden Zeilen. (Man beachte, dass ich inzwischen auch NamedQueries entdeckt habe…)

public List getMostRecent(int amount) throws PersistenceException {
try {
if (amount<=0) 
return HibernateUtil.getSession()
.getNamedQuery("singularity.blog.Post.getPostsOrderedByRecence")
.list();
else 
return HibernateUtil.getSession()
.getNamedQuery("singularity.blog.Post.getPostsOrderedByRecence")
.setMaxResults(amount).list();
}
catch (Exception e) {
Log.error(this, e);
throw new PersistenceException(e);
}
}
Tags:,

Leave a Reply »»

Note: All comments are manually approved to avoid spam. So if your comment doesn't appear immediately, that's ok. Have patience, it can take some days until I have the time to approve my comments.