การใช้งาน Hibernate Transaction และ Session ที่ถูกวิธี

Hibernate ORM เป็น Framework ที่อินดี้มากที่สุดเท่าที่ผมเคยรู้จักมา
ในการพัฒนา web application การที่จะเปิดปิด Session กับ Transaction ยังเป็นเรื่องดราม่าระหว่างนักพัฒนากับ community ผู้ใช้งานได้เลย
วิธีการที่ผมจะนำเสนอนี้เป็นวิธีการที่ทางฝั่งนักพัฒนา Hibernate ต้องการให้ไปทางนี้
หากท่านที่หลงเข้ามาอ่านแล้วรู้สึกขัดใจก็ไม่ต้องแปลกใจนะครับ เขาดราม่ากันมานานหลายปีแล้ว

ที่มาของปัญหา


เริ่มมาจากปัญหา “LazyInitializationException: Session has been closed” ถ้าใครเคยดึงค่าตัวแปรจาก Entity ที่ถูกกำหนดค่าไว้แบบ proxy (lazy load) นอก scope ของ Transaction คงจะรู้จักกันดี
ทางทีมพัฒนาก็เลยเสนอวิธีการที่เรียกว่า Open Session in View เล่าแบบสรุปคือเมื่อมีการ request เข้ามาที่ server ก็ให้ทำการเปิด Session ทันที พอหน้า view ทำการ render เสร็จแล้วถึงจะปิด Session

Open Session in View


ในการที่จะดักว่ามีการ Request เข้ามาและทำงานเสร็จตอนใหนสามารถใช้ ServletFilter ดักได้ ทางทีมพัฒนาให้ตัวอย่างโค้ดมาดังนี้ (ผมแก้ log ที่เป็น debug ให้เป็น info เพื่อให้ดูการทำงานได้ง่ายขึ้น)

package com.magicalcyber.hellohibernate.web.filter;

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;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.StaleObjectStateException;

import com.magicalcyber.hellohibernate.util.HibernateUtil;

/**
 * Created by MagicalCyber on 10/30/2014.
 */
@WebFilter(filterName = "HibernateSessionRequestFilter", urlPatterns = {"*"})
public class HibernateSessionRequestFilter implements Filter {

    private static final Logger log = Logger.getLogger(HibernateSessionRequestFilter.class);

    private SessionFactory sf;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("Initializing filter...");
        log.info("Obtaining SessionFactory from static HibernateUtil singleton");
        sf = HibernateUtil.getSessionFactory();
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
        	String uri = ((HttpServletRequest)servletRequest).getRequestURI().toString();
        	log.info("request " + uri);
            
        	log.info("Starting a database transaction");
            sf.getCurrentSession().beginTransaction();

            // Call the next filter (continue request processing)
            filterChain.doFilter(servletRequest, servletResponse);

            // Commit and cleanup
            log.info("Committing the database transaction");
            sf.getCurrentSession().getTransaction().commit();

        } catch (StaleObjectStateException staleEx) {
            log.error("This interceptor does not implement optimistic concurrency control!");
            log.error("Your application will not work until you add compensation actions!");
            // Rollback, close everything, possibly compensate for any permanent changes
            // during the conversation, and finally restart business conversation. Maybe
            // give the user of the application a chance to merge some of his work with
            // fresh data... what you do here depends on your applications design.
            throw staleEx;
        } catch (Throwable ex) {
            // Rollback only
            ex.printStackTrace();
            try {
                if (sf.getCurrentSession().getTransaction().isActive()) {
                    log.info("Trying to rollback database transaction after exception");
                    sf.getCurrentSession().getTransaction().rollback();
                }
            } catch (Throwable rbEx) {
                log.error("Could not rollback transaction after exception!", rbEx);
            }

            // Let others handle it... maybe another interceptor for exceptions?
            throw new ServletException(ex);
        }
    }

    @Override
    public void destroy() {

    }
}

ทางทีมงานบอกว่าด้วยวิธีการนี้จะทำให้ส่วนของ DAO ไม่ต้องคุม Transaction เอง เพียงแค่ getCurrentSession ก็พอแล้ว
ผมได้ลองสร้างตัวอย่าง web application ที่ใช้ Servlet เรียกใช้ Hibernate ทำการบันทึกข้อมูลและทำการแสดงผลโดยดึงจากฐานข้อมูลขึ้นมาแสดงผล

package com.magicalcyber.hellohibernate.web;

import com.magicalcyber.hellohibernate.dao.PersonDAO;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Created by MagicalCyber on 10/30/2014.
 */
@WebServlet(name = "AddPersonServlet", urlPatterns = {"/AddPersonServlet"})
public class AddPersonServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        if(name != null){
            PersonDAO dao = new PersonDAO();
            dao.save(name);
        }
        resp.sendRedirect("/HomeServlet");
    }
}
package com.magicalcyber.hellohibernate.web;

import com.magicalcyber.hellohibernate.dao.PersonDAO;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Created by MagicalCyber on 10/30/2014.
 */
@WebServlet(name = "HomeServlet", urlPatterns = {"/HomeServlet"})
public class HomeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PersonDAO dao = new PersonDAO();
        req.setAttribute("persons", dao.listPerson());
        req.getRequestDispatcher("home.jsp").forward(req, resp);
    }
}
package com.magicalcyber.hellohibernate.dao;

import com.magicalcyber.hellohibernate.domain.Person;
import com.magicalcyber.hellohibernate.util.HibernateUtil;
import org.hibernate.Session;

import java.util.List;

/**
 * Created by MagicalCyber on 10/30/2014.
 */
public class PersonDAO {
    private Session session;

    public PersonDAO() {
        session = HibernateUtil.getSessionFactory().getCurrentSession();
    }

    public List<Person> listPerson(){
        List list = session.createCriteria(Person.class).list();
        return list;
    }

    public void save(String name){
        Person person = new Person();
        person.setName(name);
        session.save(person);
    }
}

ผลลัพธ์ที่ได้เป็นดังนี้
hibernate-open-transaction-favicon

จะเห็นว่าทุก Request ที่ส่งเข้า Server จะมีการ Open และ Commit Transaction เสมอแม้กระทั่ง favicon!!!! (ลองนึกภาพว่าถ้าเว็บเรามีรูปประกอบด้วยคง Open และ Commit Transaction กันสนุกสนานเลยทีเดียว)
ซึ่งวิธีแก้ไขสำหรับผมคือให้ดักใน Filter ว่าถ้าเป็นการส่ง Request จาก Servlet เท่านั้นถึงจะทำการ Open และ Commit Transaction ดังนี้

package com.magicalcyber.hellohibernate.web.filter;

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;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.StaleObjectStateException;

import com.magicalcyber.hellohibernate.util.HibernateUtil;

/**
 * Created by MagicalCyber on 10/30/2014.
 */
@WebFilter(filterName = "HibernateSessionRequestFilter", urlPatterns = {"*"})
public class HibernateSessionRequestFilter implements Filter {

    private static final Logger log = Logger.getLogger(HibernateSessionRequestFilter.class);

    private SessionFactory sf;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("Initializing filter...");
        log.info("Obtaining SessionFactory from static HibernateUtil singleton");
        sf = HibernateUtil.getSessionFactory();
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
        	String uri = ((HttpServletRequest)servletRequest).getRequestURI().toString();
        	log.info("request " + uri);
            if(uri.endsWith("Servlet")){
            	log.info("Starting a database transaction");
                sf.getCurrentSession().beginTransaction();

                // Call the next filter (continue request processing)
                filterChain.doFilter(servletRequest, servletResponse);

                // Commit and cleanup
                log.info("Committing the database transaction");
                sf.getCurrentSession().getTransaction().commit();
            }else{
            	filterChain.doFilter(servletRequest, servletResponse);
            }

        } catch (StaleObjectStateException staleEx) {
            log.error("This interceptor does not implement optimistic concurrency control!");
            log.error("Your application will not work until you add compensation actions!");
            // Rollback, close everything, possibly compensate for any permanent changes
            // during the conversation, and finally restart business conversation. Maybe
            // give the user of the application a chance to merge some of his work with
            // fresh data... what you do here depends on your applications design.
            throw staleEx;
        } catch (Throwable ex) {
            // Rollback only
            ex.printStackTrace();
            try {
                if (sf.getCurrentSession().getTransaction().isActive()) {
                    log.info("Trying to rollback database transaction after exception");
                    sf.getCurrentSession().getTransaction().rollback();
                }
            } catch (Throwable rbEx) {
                log.error("Could not rollback transaction after exception!", rbEx);
            }

            // Let others handle it... maybe another interceptor for exceptions?
            throw new ServletException(ex);
        }
    }

    @Override
    public void destroy() {

    }
}

และทำการทดสอบอีกรอบ ก็พบว่าไม่มีการ Open และ Commit Transaction ของ favicon แล้ว
hibernate-not-open-transaction-favicon

ถ้ามีการทำงานที่ไม่จบใน Action เดียวจะทำอย่างไร?


ทางทีมงานเขาก็ทำตัวอย่างขึ้นมาให้แล้วครับ ตามไปดูได้ในส่วนอ้างอิงด้านล่างหัวข้อ เมื่อเปิดเว็บแล้วให้ดูคลาสที่ชื่อ HibernateSessionConversationFilter
สรุปสั้นๆ คือเก็บ Hibernate Session ไว้ใน Http Session ครับ จะได้อยู่ยาวๆ

มันก็ดูดีนะ แล้วเขาดราม่าอะไรกัน?


ปัญหาหลักๆ ที่ผมเห็นคือ “ทำไมถึงไม่ทำให้เสร็จในส่วนของ Data Layer ไปเลย”
ปัญหาย่อยๆ ก็จะมี Memory เต็มเพราะเขาทำการ load เข้า session เยอะ (http://stackoverflow.com/questions/4758574/hibernate-cache1-outofmemory-with-opensessioninview มันก็เป็นปกตินะถ้าจัดการไม่ดี) และปัญหาการ Query แบบ N+1 (http://stackoverflow.com/questions/97197/what-is-the-n1-selects-issue)

อ้างอิง


One thought on “การใช้งาน Hibernate Transaction และ Session ที่ถูกวิธี

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s