Hibernate中的Open Session In View的资料(英语版)

The problem

A common issue in a typical (web-)application is the rendering of the view, after the main logic of the action has been completed, and therefore, the Hibernate Session has already been closed and the database transaction has ended. If you access detached objects that have been loaded in the Session inside your JSP (or any other view rendering mechanism), you might hit an unloaded collection or a proxy that isn't initialized. The exception you get is: LazyInitializationException: Session has been closed (or a very similar message). Of course, this is to be expected, after all you already ended your unit of work.

 

A first solution would be to open another unit of work for rendering the view. This can easily be done but is usually not the right approach. Rendering the view for a completed action is supposed to be inside the first unit of work, not a separate one. The solution, in two-tiered systems, with the action execution, data access through the Session, and the rendering of the view all in the same virtual machine, is to keep the Session open until the view has been rendered.

 

Using an interceptor

If you implement your Session handling with Hibernates built-in support for automatic Session context management, see Sessions and transactions, you have half of the code for this already. Now you only need some kind of interceptor that runs after the view has been rendered, and that will then commit the database transaction, hence close the Session. In other words, in most applications you need the following: when an HTTP request has to be handled, a new Session and database transaction will begin. Right before the response is send to the client, and after all the work has been done, the transaction will be committed, and the Session will be closed.

 

A good standard interceptor in a servlet container is a ServletFilter. It's rather trivial to put some lines into a custom filter that runs on every request and before every response, taken from CaveatEmptor:

public class HibernateSessionRequestFilter implements Filter {
 
    private static Log log = LogFactory.getLog(HibernateSessionRequestFilter.class);
 
    private SessionFactory sf;
 
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {
 
        try {
            log.debug("Starting a database transaction");
            sf.getCurrentSession().beginTransaction();
 
            // Call the next filter (continue request processing)
            chain.doFilter(request, response);
 
            // Commit and cleanup
            log.debug("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.debug("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);
        }
    }
 
    public void init(FilterConfig filterConfig) throws ServletException {
        log.debug("Initializing filter...");
        log.debug("Obtaining SessionFactory from static HibernateUtil singleton");
        sf = HibernateUtil.getSessionFactory();
    }
 
    public void destroy() {}
 
}

 

If you combine this filter with the automatic Session context support, writing a DAO becomes as trivial as this:

public class ItemDAO {
 
    Session currentSession;
 
    public ItemDAO() {
        currentSession = HibernateUtil.getSessionFactory().getCurrentSession();
    }
 
    public Item getItemById(Long itemId) {
        return (Item) currentSession.load(Item.class, itemId);
    }
}

 

Alternatively, a DAO could require that a Session is a constructor argument, so responsibility of setting the current Session would be moved to the client of the DAO (which could be a factory). For more information about DAOs, see Generic Data Access Objects.

Now your application controllers can use the DAOs and don't have to bother at all with sessions or transactions, e.g. in your servlets:

public String execute(HttpRequest request) {
 
    Long itemId = request.getParameter(ITEM_ID);
 
    ItemDAO dao = new ItemDAO();
 
    request.setAttribute( RESULT, dao.getItemById(itemId) );
  
    return "success";
}

 

To enable the filter to run for all Http requests, add this to your web.xml configuration file:

    <filter>
        <filter-name>HibernateFilter</filter-name>
        <filter-class>my.package.HibernateThreadFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>HibernateFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

 

If you don't want to start a Hibernate Session (and a transaction, which obtains a database connection from the pool) for every Http request, adjust the filter mapping to an appropriate URL pattern (e.g. only URLs that require database access). Or, add a switch to the filter that computes if a database session/transaction is needed, based on some arbitrary criteria (e.g. request parameter).

 

Caveat: Since the Session is flushed after the view has been rendered, database exceptions might occur after a successful output has been generated. If you use plain JSP and servlets the page output will be rendered into a buffer, usually 8K. If the buffer is full, it is flushed to the client browser! So, your user might see a successful page (200 OK) but in fact an exception occurred. To avoid this, don't render into the servlet engine buffer or increase the buffer size to a safe value. Most web frameworks avoid this issue by not rendering into the standard servlet engine buffer, but into their own.

 

What about three-tier environments?

It's clear that this pattern only makes sense if you can actually use a local Session when rendering the view. In a three-tier environment the view might be rendered on the presentation virtual machine, not on the service virtual machine with the business and data access layer. Therefore, keeping the Session and transaction open is not an option. In this case you have to send the right "amount" of data to the presentation layer, so a view can be constructed with the Session already closed. It depends on your architecture if you better use Detached Objects, Data Transfer Objects, or maybe a mix of both with the Command Pattern. These options are discussed in Hibernate in Action.

 

What about the extended Session pattern for long Conversations?

If you'd like to use a single Session for several database transactions, you either have to implement it completely yourself, or you have to use the built-in "application managed" strategy:

 

 

 

To enable the application-managed "current" Session strategy, set your hibernate.current_session_context_class configuration property to org.hibernate.context.ManagedSessionContext (or simply "managed" in Hibernate 3.2). You can now bind and unbind the "current" Session with static methods, and control the FlushMode and flushing manually. The Servlet filter shown earlier has to do this binding and flushing, during a conversation:

public class HibernateSessionConversationFilter
        implements Filter {
 
    private static Log log = LogFactory.getLog(HibernateSessionConversationFilter.class);
 
    private SessionFactory sf;
 
    public static final String HIBERNATE_SESSION_KEY = "hibernateSession";
    public static final String END_OF_CONVERSATION_FLAG = "endOfConversation";
 
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {
 
        org.hibernate.classic.Session currentSession;
 
        // Try to get a Hibernate Session from the HttpSession
        HttpSession httpSession =
                ((HttpServletRequest) request).getSession();
        Session disconnectedSession =
                (Session) httpSession.getAttribute(HIBERNATE_SESSION_KEY);
 
        try {
 
            // Start a new conversation or in the middle?
            if (disconnectedSession == null) {
                log.debug(">>> New conversation");
                currentSession = sf.openSession();
                currentSession.setFlushMode(FlushMode.NEVER);
            } else {
                log.debug("< Continuing conversation");
                currentSession = (org.hibernate.classic.Session) disconnectedSession;
            }
 
            log.debug("Binding the current Session");
            ManagedSessionContext.bind(currentSession);
 
            log.debug("Starting a database transaction");
            currentSession.beginTransaction();
 
            log.debug("Processing the event");
            chain.doFilter(request, response);
 
            log.debug("Unbinding Session after processing");
            currentSession = ManagedSessionContext.unbind(sf);
 
            // End or continue the long-running conversation?
            if (request.getAttribute(END_OF_CONVERSATION_FLAG) != null ||
                request.getParameter(END_OF_CONVERSATION_FLAG) != null) {
 
                log.debug("Flushing Session");
                currentSession.flush();
 
                log.debug("Committing the database transaction");
                currentSession.getTransaction().commit();
 
                log.debug("Closing the Session");
                currentSession.close();
 
                log.debug("Cleaning Session from HttpSession");
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);
 
                log.debug("<<< End of conversation");
 
            } else {
 
                log.debug("Committing database transaction");
                currentSession.getTransaction().commit();
 
                log.debug("Storing Session in the HttpSession");
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, currentSession);
 
                log.debug("> Returning to user in conversation");
            }
 
        } 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
            try {
                if (sf.getCurrentSession().getTransaction().isActive()) {
                    log.debug("Trying to rollback database transaction after exception");
                    sf.getCurrentSession().getTransaction().rollback();
                }
            } catch (Throwable rbEx) {
                log.error("Could not rollback transaction after exception!", rbEx);
            } finally {
                log.error("Cleanup after exception!");
 
                // Cleanup
                log.debug("Unbinding Session after exception");
                currentSession = ManagedSessionContext.unbind(sf);
 
                log.debug("Closing Session after exception");
                currentSession.close();
 
                log.debug("Removing Session from HttpSession");
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);
 
            }
 
            // Let others handle it... maybe another interceptor for exceptions?
            throw new ServletException(ex);
        }
 
    }
 
    public void init(FilterConfig filterConfig) throws ServletException {
        log.debug("Initializing filter...");
        log.debug("Obtaining SessionFactory from static HibernateUtil singleton");
        sf = HibernateUtil.getSessionFactory();
    }
 
    public void destroy() {}
 
}

 

This filter is transparent for the rest of your application, no DAO or any other code that uses the "current" Session has to be changed. However, at some point the conversation has to end, and the Session has to be finally flushed and closed. The example above uses a special marker flag that has to be present in the request scope. You could set this flag during processing of the request, based on some arbitrary criteria. Maybe you have another interceptor layer for Conversation demarcation? Or a workflow engine?

 

I don't want to write servlet filters, they are so old school!

You are right, servlet filters are not really hot technology. However, they work very well as wrap-around interceptors in web applications and there is nothing wrong with servlet filters per se. As mentioned already, if you use a web framework that has its own interceptors, you will probably find them more flexible. Many containers also provide custom interceptors these days - note that if these containers do not implement a Java EE standard (such as EJB), you are locked into a proprietary runtime environment. Finally, a very flexible approach are AOP interceptors - see Session handling with AOP.

 

How do I handle exceptions?

Clearly, whenever an exception happens, the database transaction has to be rolled back. Another rule in Hibernate is that the current Session has to be closed and discarded immediately, it can't be re-used. Hence, this is all you have to do to handle exceptions from a Hibernate perspective. Of course you might want to retry some unit of work if it failed, or you might want to display custom errors. All of this is outside of the scope of Hibernate and can be implemented in any way you like.

 

Can I commit the transaction before rendering the view?

Apparently, though never promoted in Hibernate documentation, some developers are using a variation of this pattern that keeps the Session open until the view has been rendered, but commits the database transaction before rendering of the view. Then, during rendering of the view, unloaded proxies or collections are accessed and have to be initialized. Because the Session is still open, this seems to work. Even the Session can be called and will do what it is told. The Session gets a connection and executes a prepared statement for a single operation.

 

However, this kind of access is non-transactional. You need to enable the auto-commit mode in Hibernate's configuration for this. If, and only if you configured Hibernate to enable auto-commit mode behavior, does Hibernate set the connection into auto-commit mode after it obtains it from the pool, for the single operation, and returns it to the pool with close() on the JDBC Connection object afterwards.

 

Also note that non-transactional access will/might work if you forget to enable the auto-commit mode in the Hibernate configuration. If you did not enable auto-commit mode in Hibernate, the connection is in whatever mode it is by default after obtained from the pool, and returned there without commit or rollback. The behavior is then undefined. Never do this, as the JDBC specification does not say what happens on close() with any potentially pending transaction (yes, it is possible that a database transaction will be started implicitly when Hibernate obtains the connection from the pool).

 

In fact, consider any non-transactional data access (without JTA/EJBs) a complete anti-pattern, as there is no performance, scalability, or other benefit to be gained from it. Some frameworks that rely on Hibernate also rely on this anti-pattern, avoid them. Always set clear transaction boundaries to group your statements into units of work. You can consider using two transactions, one for executing the event, one for rendering the view, with the same Session.

 

Can I use two transactions in one Session?

Yes, this is actually a better implementation of this pattern. One database transaction is used to read and write data during the processing of the request event. The second database transaction is only used to read data, during rendering of the view. No modifications to objects are made at this point. Hence, database locks are released early in the first transaction, allowing better scalability, and the second transaction can possibly be optimized (e.g. some databases require read-only transaction settings for best cleanup after transaction commit). To use two transactions you need a more powerful interceptor than a simple servlet filter - AOP is a good choice. The JBoss Seam frameworks uses this model.

 

Why can't Hibernate just load objects on demand?

Every month someone has the idea that Hibernate could instead of throwing a LazyInitializationException just open up a new connection to the database (effectively starting a new Session) and load the collection or initialize the proxy that has been touched on-demand. Of course, this idea, while brilliant at first, has several shortcomings that only appear if you start to think about the consequences of ad-hoc transactional access.

 

If Hibernate would, hidden from the developer and outside of any transaction demarcation, start random database connections and transactions, why have transaction demarcation at all? What happens when Hibernate opens a new database connection to load a collection, but the owning entity has been deleted meanwhile? (Note that this problem does not appear with the two-transaction strategy as described above - the single Session provides repeatable reads for entities.) Why even have a service layer when every object can be retrieved by simply navigating to it? How much memory should be consumed by this and which objects should be evicted first? All of this leads to no solution, because Hibernate is a service for online transaction processing (and certain kinds of batch operations) and not a "streaming objects from some persistent data store in undefined units of work"-service. Also, in addition to the n+1 selects problem, do we really need an n+1 transaction and connection problem?

 

The solution for this issue is of course proper unit of work demarcation and design, supported by possibly an interception technique as shown in the pattern here, and/or the correct fetch technique so that all required information for a particular unit of work can be retrieved with minimum impact, best performance, and scalability.

 

This is all very difficult, can't this be done easier?

Hibernate can only do so much as a persistence service, the problem discussed here is however the responsibility of the application infrastructure, or framework. The EJB3 programming model makes transaction and persistence context management very easy, use the Hibernate EntityManager to get this API. Either run your EJBs inside a full J2EE application server (previews available from several vendors) or in a lightweight embeddable EJB3 container, JBoss Embeddable EJB3, in any Java environment. The JBoss Seam framework has built-in support for automatic context management, including persistence and conversations, with only a few annotations in your source code.

Note: Some people still believe that this pattern creates a dependency between the presentation layer and Hibernate. It does not, see this thread on the forum.

你可能感兴趣的:(Hibernate中的Open Session In View的资料(英语版))